diff --git a/.vitepress/config.mts b/.vitepress/config.mts index 6806882..009fdc9 100644 --- a/.vitepress/config.mts +++ b/.vitepress/config.mts @@ -96,6 +96,10 @@ export default defineConfig({ text: "Core File", link: "/docs/plugin-essentials/core-file", }, + { + text: "Plugin Context (ctx)", + link: "/docs/plugin-essentials/plugin-context", + }, ], }, { @@ -118,6 +122,10 @@ export default defineConfig({ text: "EditorManager", link: "/docs/global-apis/editor-manager", }, + { + text: "Config", + link: "/docs/global-apis/config", + }, { text: "Other Global Utilities", link: "/docs/global-apis/global-utilities", @@ -324,6 +332,14 @@ export default defineConfig({ text: "Terminal", link: "/docs/advanced-apis/terminal", }, + { + text: "Executor", + link: "/docs/advanced-apis/executor", + }, + { + text: "System", + link: "/docs/advanced-apis/system", + }, { text: "LSP", link: "/docs/advanced-apis/lsp", diff --git a/docs/advanced-apis/executor.md b/docs/advanced-apis/executor.md new file mode 100644 index 0000000..8efe062 --- /dev/null +++ b/docs/advanced-apis/executor.md @@ -0,0 +1,159 @@ +# Executor + +The `Executor` API lets you run shell commands on the device without opening a visual terminal session. It supports one-off commands, long-running processes with real-time streaming, stdin writes, and background execution via a foreground service. + +> [!Warning] +> Prefer visible terminals for transparency. Avoid hiding work in the background and do not start long‑running processes without good reason. For interactive or long‑lived tasks, use a [terminal session](./terminal.md) instead. + +## Access + +The global `Executor` is an `Executor` instance (clobbered to `window.Executor` by the terminal plugin). It has a built-in `BackgroundExecutor` instance for background-mode processes. + +```js +const Executor = globalThis.Executor; // Executor instance +const background = Executor.BackgroundExecutor; // BackgroundExecutor instance +``` + +Both instances share the same methods. + +## One-off execution + +### `execute(command, alpine?)` + +- Purpose: Runs a single shell command and waits for it to finish. Output is returned after the process exits (no live streaming of output). +- Parameters: + - `command` (string): The command to run. + - `alpine` (boolean, optional): Run inside the Alpine sandbox when `true`; run in the Android environment when `false`. +- Returns: `Promise` that resolves with stdout on success, or rejects with an error/stderr on failure. + +```js +// Outputting hello on stdout +Executor.execute('echo hello') + .then(console.log) + .catch(console.error); +``` + +or with `async/await`: + +```js +const output = await Executor.execute('echo hello'); +console.log(output); +``` + +> [!Warning] +> Do not run things like an infinite loop or a shell because `execute()` waits for the process to exit and a shell never exits on its own, avoid running those commands with this function. + +## Long-running processes + +### `start(command, onData, alpine?)` + +- Starts a shell process and enables real-time streaming of `stdout`, `stderr`, and `exit`. +- Parameters: + - `command` (string): The command to run (e.g. `"sh"`, `"ls -al"`). + - `onData` (function): `(type, data) => void`. `type` is `"stdout"`, `"stderr"`, or `"exit"` (the process exit code); `data` is the output line or exit code. + - `alpine` (boolean, optional): Run inside the Alpine sandbox when `true`. +- Returns: `Promise` resolving to a unique process UUID used by `write()`, `stop()`, and `isRunning()`. + +```js +const uuid = await Executor.start("sh", (type, data) => { + console.log(`[${type}] ${data}`); +}); +Executor.write(uuid, "echo Hello World\r"); +Executor.stop(uuid); +``` + +### `write(uuid, input)` + +Sends input to a running process's stdin. + +- Returns: `Promise`. + +```js +await Executor.write(uuid, "ls /sdcard\r"); +``` + +### `stop(uuid)` + +Terminates a running process. + +- Returns: `Promise`. + +### `isRunning(uuid)` + +Checks whether a process is still running. + +- Returns: `Promise`. + +```js +if (await Executor.isRunning(uuid)) { + await Executor.stop(uuid); +} +``` + +### `spawnStream(cmd, callback, onError?)` + +Spawns a process and exposes it as a raw WebSocket stream. Once the process is ready the callback is invoked with the connected `WebSocket`; use `ws.send()` to write to stdin and `ws.onmessage` to read stdout. + +- Parameters: + - `cmd` (string[]): Command and arguments (e.g. `["sh", "-c", "echo hi"]`). + - `callback` (function): `(ws) => void`. + - `onError` (function, optional): error handler. + +## Managing processes + +### `listProcesses()` + +Lists the processes currently managed by this Executor. + +- Returns: `Promise>`. `background` is `true` for a `BackgroundExecutor`. + +### `listAllProcesses()` + +Lists all running OS processes under the app's user id. + +- Returns: `Promise>`. + +### `killProcess(pid)` + +Forcefully kills a process by its native PID. + +- Returns: `Promise`. + +## Service control + +### `moveToForeground()` / `moveToBackground()` + +Moves the Executor service between foreground (shows the notification) and background. + +- Returns: `Promise`. + +### `stopService()` + +Stops the Executor service completely. This does **not** guarantee that all running processes are killed - the service just stops being active. The processes will keep running until stopped. + +- Returns: `Promise`. + +## Advanced + +### `loadLibrary(path)` + +Loads a native library from the given path. + +- Returns: `Promise`. + +```js +await Executor.loadLibrary('/path/to/library.so'); +``` + +> [!Warning] +> `loadLibrary()` has been deprecated and is no longer supported on newer Acode versions. + +### `setProotDebug(enabled)` + +Toggles proot debug output (used for the Alpine sandbox). + +- Returns: `Promise`. + +## Related APIs + +- Visual terminal sessions: [Terminal](./terminal.md) diff --git a/docs/advanced-apis/system.md b/docs/advanced-apis/system.md new file mode 100644 index 0000000..923542c --- /dev/null +++ b/docs/advanced-apis/system.md @@ -0,0 +1,237 @@ +# System + +The `system` module wraps Acode's native Android bridge (`cordova-plugin-system`). It is clobbered to `window.system` and provides low-level device, file, storage, permission, intent, and shortcut utilities that Acode itself uses. + +```js +const system = window.system; +``` + +Most methods are callback-based (`(success, error) => void`). Wrap them with `helpers.promisify` when you prefer promises: + +```js +const helpers = acode.require("helpers"); +const filesDir = await helpers.promisify(system.getFilesDir); +``` + +## Files + +### `getFilesDir(success, error)` + +Resolves the app's internal files directory path. + +```js +const filesDir = await helpers.promisify(system.getFilesDir); +``` + +### `getParentPath(path, success, error)` + +Resolves the parent directory of `path`. + +### `listChildren(path, success, error)` + +Lists the children of a directory path. + +### `mkdirs(path, success, error)` + +Recursively creates directories. + +### `fileExists(path, countSymlinks, success, error)` + +Checks whether a file exists. `countSymlinks` is a boolean passed as a string. + +### `copyToUri(srcUri, destUri, fileName, success, error)` + +Copies a file to a destination uri under `fileName`. + +### `writeText(path, content, success, error)` + +Writes text content to a file path. + +### `deleteFile(path, success, error)` + +Deletes a file path. + +### `createSymlink(target, linkPath, success, error)` + +Creates a symlink at `linkPath` pointing to `target`. + +### `setExec(path, executable, success, error)` + +Marks a file path as executable (`executable` is a boolean passed as a string). + +### `extractAsset(assetName, destinationPath, success, error)` + +Extracts an app asset to a destination path. + +### `getNativeLibraryPath(success, error)` + +Resolves the directory where native libraries are stored. + +## Storage management + +### `isManageExternalStorageDeclared(success, error)` + +Checks whether the app declares all-files access in its manifest. + +### `hasGrantedStorageManager(success, error)` + +Checks whether the app has been granted "All files access". + +### `requestStorageManager(success, error)` + +Requests the "All files access" permission. + +### `manageAllFiles(success, error)` + +Opens the system screen to grant all-files access. + +### `isExternalStorageManager(success, error)` + +Checks whether the app is currently an external storage manager. + +## Permissions + +### `hasPermission(permission, success, error)` + +Checks whether a runtime permission is granted. + +### `requestPermission(permission, success, error)` + +Requests a single runtime permission. + +### `requestPermissions(permissions, success, error)` + +Requests multiple runtime permissions at once. + +## App & device info + +### `getAppInfo(success, error)` + +Resolves information about the Acode app. + +### `getInstaller(success, error)` + +Resolves the package that installed the app (used for `window.appInstallSource`). + +### `getAndroidVersion(success, error)` + +Resolves the Android OS version. + +### `getArch(success, error)` + +Resolves the device architecture (e.g. `arm64-v8a`). + +### `getWebviewInfo(success, error)` + +Resolves WebView information (used by the terminal's engine detection). + +### `isPowerSaveMode(success, error)` + +Checks whether the device is in power-save mode. + +### `getGlobalSetting(key, success, error)` + +Reads a global Android setting by key. + +### `clearCache(success, error)` + +Clears the app's cache. + +## File actions & sharing + +### `fileAction(fileUri, filename, action, mimeType, error?)` + +Launches an Android intent for a file. `action` is one of `VIEW`, `EDIT`, `SEND`, or `RUN` (the app prepends `android.intent.action.`). Arguments are flexible: `system.fileAction(uri, filename, action, mimeType, onFail)`. + +```js +system.fileAction(fileUri, filename, "VIEW", "text/plain"); +``` + +### `shareText(text, success, error)` + +Shares a text string through the system share sheet. + +### `openInBrowser(src)` + +Opens a url in the system browser. + +### `inAppBrowser(url, title, showButtons, disableCache)` + +Opens a url in Acode's in-app browser. Returns an object with `onOpenExternalBrowser` and `onError` callbacks that can be assigned: + +```js +const browser = system.inAppBrowser(url, title, true, false); +browser.onOpenExternalBrowser = (url) => console.log("opened externally", url); +``` + +### `launchApp(app, className, extras?, success?, error?)` + +Launches an Android activity by package and class name, optionally passing intent extras (string/number/boolean values). + +```js +system.launchApp( + "com.example.app", + "com.example.app.MainActivity", + { user: "example", premium: true }, + (msg) => console.log(msg), + (err) => console.error(err), +); +``` + +## Shortcuts + +### `addShortcut(shortcut, success, error)` + +Adds a home-screen shortcut. `shortcut` is `{ id, label, description, icon, action, data }`. + +### `removeShortcut(id, success, error)` + +Removes a shortcut by id. + +### `pinShortcut(id, success, error)` + +Pins a shortcut. + +### `pinFileShortcut(shortcut, success, error)` + +Pins a file shortcut. + +## Intents + +### `getCordovaIntent(success, error)` + +Resolves the intent that launched the app (for handling external open requests). + +### `setIntentHandler(handler, onerror)` + +Registers a handler for intents received while the app is running. `handler` receives the intent data. + +## Text comparison + +Used by the editor's dirty-tracking and file-change detection. Both methods compare in a background thread. + +### `compareFileText(fileUri, encoding, currentText): Promise` + +Reads the file at `fileUri` and compares it to `currentText`. Resolves `true` when the content **differs**, `false` when it matches. + +### `compareTexts(text1, text2): Promise` + +Compares two strings. Resolves `true` when they **differ**, `false` when equal. + +```js +const changed = await system.compareFileText(file.uri, file.encoding, text); +``` + +## UI + +### `setUiTheme(systemBarColor, theme, success?, error?)` + +Sets the Android system bar colors to match a theme. `systemBarColor` is a hex color; `theme` is the theme id. A pure white color is mapped to `#fffffe` so status bar icons stay visible. + +### `setInputType(type, success, error)` + +Changes the soft-keyboard input type. + +### `setNativeContextMenuDisabled(disabled, success, error)` + +Enables or disables the native context menu on the WebView. diff --git a/docs/advanced-apis/terminal.md b/docs/advanced-apis/terminal.md index 462f511..f99a8c9 100644 --- a/docs/advanced-apis/terminal.md +++ b/docs/advanced-apis/terminal.md @@ -155,27 +155,9 @@ This is useful when a plugin needs to decide whether it can use terminal-backed ## Background Execution (No Terminal) -Use the globally available `Executor` when you need to run a one‑off shell command without opening a visual terminal session. +Use the globally available `Executor` to run shell commands without opening a visual terminal session - one-off commands, long-running processes with streaming output, and background-mode execution. -> [!Warning] -> Prefer visible terminals for transparency. Avoid hiding work in the background and do not start long‑running processes via `Executor.execute`. For interactive or long‑lived tasks, use a terminal session instead. - -### `Executor.execute(command, alpine?)` - -- Purpose: Runs a single shell command and waits for it to finish. Output is returned after the process exits (no live streaming of output). -- Parameters: - - `command` (string): The command to run. - - `alpine` (boolean, optional): Run inside the Alpine sandbox when `true`; run in the Android environment when `false`. -- Returns: `Promise` that resolves with stdout on success, or rejects with an error/stderr on failure. - -#### Example - -```js -// Quick directory listing without opening a terminal UI -Executor.execute('ls -l') - .then(console.log) - .catch(console.error); -``` +See [Executor](./executor.md). ## Example: Themed Output Terminal diff --git a/docs/advanced-apis/webview.md b/docs/advanced-apis/webview.md index feba94c..d590077 100644 --- a/docs/advanced-apis/webview.md +++ b/docs/advanced-apis/webview.md @@ -144,7 +144,7 @@ Use `off(event, callback)` to remove a listener. - Modes: `fullscreen` hosts the WebView in its own activity; `hidden` is headless and never displayed, useful for background automation or scraping. - Back button: In fullscreen mode it navigates back through page history first; when nothing is left, the WebView closes and the `closed` event fires. - Hide/Show: `hide()` backgrounds the fullscreen activity without destroying it, so `show()` restores it with the page state intact. -- Cleanup: Instances are not tied to your plugin's lifecycle. Destroy every instance you create — ideally in your plugin's `destroy()` function — so hidden WebViews don't outlive the plugin. +- Cleanup: Instances are not tied to your plugin's lifecycle. Destroy every instance you create - ideally in your plugin's `destroy()` function - so hidden WebViews don't outlive the plugin. - Security: Hosted content is isolated. File and content scheme access is disabled, only `http(s)` URLs can load, and non-http(s) navigation (`file:`, `intent:`, `javascript:`, `tel:`, ...) is always blocked. When `allowNavigation` is `false`, all navigation is blocked. ## Example: Headless Title Fetcher diff --git a/docs/getting-started/create-plugin.md b/docs/getting-started/create-plugin.md index f972f93..d33d039 100644 --- a/docs/getting-started/create-plugin.md +++ b/docs/getting-started/create-plugin.md @@ -126,7 +126,7 @@ For local development, start a dev server using `npm run dev`. In Acode, use the It's more convenient to manage this from the sidebar. When you install a local plugin(either using url or selecting the zip), Acode will add a **reload** icon in the **Extensions** tab of the sidebar. This is useful because the server automatically builds the plugin ZIP when changes are made. Simply press the reload button to apply the latest changes instantly. -This makes plugin development a much smoother experience—previously, it was quite frustrating, but this feature was recently added to improve the workflow. +This makes plugin development a much smoother experience - previously, it was quite frustrating, but this feature was recently added to improve the workflow. ::: ## Creating Plugins with the CLI diff --git a/docs/getting-started/intro.md b/docs/getting-started/intro.md index c160722..c495c5b 100644 --- a/docs/getting-started/intro.md +++ b/docs/getting-started/intro.md @@ -13,7 +13,7 @@ title: Acode Plugins ### Language Flexibility -Acode plugins are primarily written in JavaScript, offering a familiar and widely-used language for developers. Additionally, for those who prefer TypeScript, **good news 🥳** — Acode supports `TypeScript` for plugin development, providing the benefits of static typing and improved developer experience. +Acode plugins are primarily written in JavaScript, offering a familiar and widely-used language for developers. Additionally, for those who prefer TypeScript, **good news 🥳** - Acode supports `TypeScript` for plugin development, providing the benefits of static typing and improved developer experience. ## Installing Acode Plugins diff --git a/docs/getting-started/understanding-plugin.md b/docs/getting-started/understanding-plugin.md index 6c1e65b..5aabeaf 100644 --- a/docs/getting-started/understanding-plugin.md +++ b/docs/getting-started/understanding-plugin.md @@ -27,11 +27,11 @@ If you skip `setPluginInit`, your script may load, but your plugin logic will no ## What You Get In `init` -Your init function receives: +The `init` callback registered with `setPluginInit` receives three arguments: -- `baseUrl`: internal base URL to your plugin files +- `baseUrl`: internal base URL to your plugin files (normalize it with a trailing slash, see below) - `$page`: a plugin page object for UI screens -- `cache`: object with: +- `options`: object with: - `cacheFileUrl` - `cacheFile` - `firstInit` @@ -39,37 +39,53 @@ Your init function receives: Use `firstInit` for one-time setup or migration. +`ctx` is your plugin's native-backed context: encrypted secret storage and permission checks. See [Plugin Context (`ctx`)](../plugin-essentials/plugin-context.md). + ## Recommended `main.js` Shape +The official templates structure your plugin as an `AcodePlugin` class with `init()` and `destroy()`: + ```js import plugin from "../plugin.json"; -function init(baseUrl, $page, cache) { - const commands = acode.require("commands"); - - commands.addCommand({ - name: "example.open", - description: "Open Example Panel", - exec: () => { - $page.innerHTML = "

Example Plugin

"; - $page.show(); - }, - }); -} +class AcodePlugin { + baseUrl = ""; -function unmount() { - const commands = acode.require("commands"); - commands.removeCommand("example.open"); + async init(_page, _cacheFile, _cacheFileUrl, _firstInit, _ctx) { + // plugin code + } + + async destroy() { + // plugin clean up + } } -acode.setPluginInit(plugin.id, init); -acode.setPluginUnmount(plugin.id, unmount); +if (window.acode) { + const acodePlugin = new AcodePlugin(); + + acode.setPluginInit(plugin.id, async (baseUrl, $page, { cacheFileUrl, cacheFile, firstInit, ctx }) => { + acodePlugin.baseUrl = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`; + await acodePlugin.init($page, cacheFile, cacheFileUrl, firstInit, ctx); + }); + + acode.setPluginUnmount(plugin.id, () => { + acodePlugin.destroy(); + }); +} ``` +Breaking that down: + +- `window.acode` is only present once Acode's API is ready, so registration is wrapped in a guard. +- `plugin.id` comes from your `plugin.json`, so the registration always matches the installed id. +- The `init` callback receives `(baseUrl, $page, options)`, where `options` is `{ cacheFileUrl, cacheFile, firstInit, ctx }`. Those are forwarded to your class's `init`. +- `baseUrl` is stored with a guaranteed trailing slash so you can build file paths with `Url.join` or string concatenation. +- `destroy()` is wired to `setPluginUnmount` so it runs on disable/reload/uninstall. `init` is awaited, so heavy setup can be done inside it. + ## What Happens On Disable / Enable / Uninstall - Disable: - - Acode calls `acode.unmountPlugin(id)` which triggers your unmount. + - Acode calls `acode.unmountPlugin(id)` which triggers your registered unmount (your class's `destroy()`). - Plugin runtime state is cleared (including plugin cache file). - Enable: - Acode loads the plugin again and runs init again. @@ -77,7 +93,7 @@ acode.setPluginUnmount(plugin.id, unmount); - Plugin files are removed. - Acode runs unmount cleanup for loaded resources. -Treat `init` as repeatable and `unmount` as mandatory cleanup. +Treat `init` as repeatable and `destroy` as mandatory cleanup. ## Failure Behavior You Should Know @@ -96,5 +112,5 @@ acode.clearBrokenPluginMark("com.example.plugin"); - Keep `init` fast; do heavy work lazily. - Register commands through `acode.require("commands")`. -- Always remove listeners, commands, intervals, and UI hooks in `unmount`. +- Always remove listeners, commands, intervals, and UI hooks in `destroy`. - Avoid storing important state only in memory; use cache/settings when needed. diff --git a/docs/global-apis/config.md b/docs/global-apis/config.md new file mode 100644 index 0000000..d02718a --- /dev/null +++ b/docs/global-apis/config.md @@ -0,0 +1,102 @@ +# Config + +The `config` module exposes Acode's internal read-only configuration: app constants, ports, URLs, and feature flags. + +Require it with `acode.require('config')`. + +```js +const config = acode.require("config"); +``` + +## Read-only proxy + +The module is a **read-only proxy** around the internal `config` object (`src/lib/config.js`). Any attempt to set, define, delete, or change the prototype of a property is blocked and logged to the console as a security warning. Values can be read at any time; they cannot be mutated by plugins. + +```js +config.API_BASE; // https://acode.app/api +config.FONT_SIZE; // /^[0-9\.]{1,3}(px|rem|em|pt|mm|pc|in)$/ +``` + +## Properties + +### App identity & API + +| Property | Type | Description | +|----------|------|-------------| +| `BASE_URL` | `string` | Root URL of the Acode website (`https://acode.app`) | +| `API_BASE` | `string` | Base URL of the Acode plugin API (`https://acode.app/api`) | +| `PLAY_STORE_URL` | `string` | Play Store listing URL for the current app package | +| `FEEDBACK_EMAIL` | `string` | Support email (`acode@foxdebug.com`) | +| `ERUDA_CDN` | `string` | CDN URL of the Eruda console (`https://cdn.jsdelivr.net/npm/eruda`) | + +### Pro / monetization + +| Property | Type | Description | +|----------|------|-------------| +| `HAS_PRO` | `boolean` | The real free/Pro flag. `true` when the user is on a Pro (paid) build or has Pro unlocked, `false` on the free build. **This is the replacement for the undocumented `IS_FREE_VERSION` global, which does not exist.** | +| `SKU_LIST` | `string[]` | Frozen array of purchase SKUs (`crystal`, `bronze`, `silver`, `gold`, `platinum`, `titanium`) | + +### Editor + +| Property | Type | Description | +|----------|------|-------------| +| `SUPPORTED_EDITOR` | `string` | Editor engine identifier (`"cm"` for CodeMirror 6) | +| `FILE_NAME_REGEX` | `RegExp` | Regex that matches valid file names | +| `FONT_SIZE` | `RegExp` | Regex that matches valid font-size CSS values | +| `DEFAULT_FILE_NAME` | `string` | Default name for a new file (`untitled.txt`) | +| `DEFAULT_FILE_SESSION` | `string` | Session id used for the default untitled tab (`default-session`) | +| `CUSTOM_THEME` | `string` | CSS selector for the custom theme (`body[theme="custom"]`) | + +### Ports + +| Property | Type | Description | +|----------|------|-------------| +| `CONSOLE_PORT` | `number` | Port used by the app console (`8159`) | +| `SERVER_PORT` | `number` | Port used by the local preview server (`8158`) | +| `PREVIEW_PORT` | `number` | Port used by the live preview (`8158`) | + +### Behaviour constants + +| Property | Type | Description | +|----------|------|-------------| +| `VIBRATION_TIME` | `number` | Short vibration duration in ms (`30`) | +| `VIBRATION_TIME_LONG` | `number` | Long vibration duration in ms (`150`) | +| `SCROLL_SPEED_SLOW` | `string` | Slow scroll speed constant (`"SLOW"`) | +| `SCROLL_SPEED_NORMAL` | `string` | Normal scroll speed constant (`"NORMAL"`) | +| `SCROLL_SPEED_FAST` | `string` | Fast scroll speed constant (`"FAST"`) | +| `SCROLL_SPEED_FAST_X2` | `string` | 2× fast scroll speed constant (`"FAST_X2"`) | +| `SIDEBAR_SLIDE_START_THRESHOLD_PX` | `number` | Drag distance in px before the sidebar starts sliding (`20`) | +| `LOG_FILE_NAME` | `string` | Name of the log file written to `DATA_STORAGE` (`Acode.log`) | + +### Social links + +| Property | Type | Description | +|----------|------|-------------| +| `DOCS_URL` | `string` | `https://docs.acode.app` | +| `GITHUB_URL` | `string` | `https://github.com/Acode-Foundation/Acode` | +| `TELEGRAM_URL` | `string` | `https://t.me/foxdebug_acode` | +| `DISCORD_URL` | `string` | `https://discord.gg/nDqZsh7Rqz` | +| `TWITTER_URL` | `string` | `https://x.com/foxbiz_io` | +| `INSTAGRAM_URL` | `string` | `https://www.instagram.com/foxbiz.io/` | +| `FOXBIZ_URL` | `string` | `https://foxbiz.io` | + +## Example + +```js +const config = acode.require("config"); + +// Feature-gate behaviour on Pro +if (config.HAS_PRO) { + // premium-only feature +} + +// Build a link to the plugin registry API +fetch(`${config.API_BASE}/plugin/com.example.plugin`); + +// Open the Play Store listing in the browser +system.openInBrowser(config.PLAY_STORE_URL); +``` + +## Related APIs + +- [Other Global Utilities](./global-utilities.md) - storage directories, `BuildInfo`, `window.log`, etc. diff --git a/docs/plugin-essentials/core-file.md b/docs/plugin-essentials/core-file.md index c251c41..9e5570c 100644 --- a/docs/plugin-essentials/core-file.md +++ b/docs/plugin-essentials/core-file.md @@ -24,7 +24,7 @@ To register your plugin, utilize the `acode.setPluginInit(pluginId: string, init 2. **init function:** - The function to be executed when the plugin is loaded. -Upon execution, the `init` function will receive three parameters: +Upon execution, the `init` function will receive three arguments: - **baseUrl (string):** - The base URL of the plugin, allowing access to files within the plugin directory. @@ -38,36 +38,64 @@ Upon execution, the `init` function will receive three parameters: - URL of the cached file. - **cacheFile (File):** - File object of the cached file, enabling file read/write operations. + - **firstInit (boolean):** + - `true` when the plugin is being installed/loaded for the first time. + - **ctx (PluginContext):** + - Your plugin's native context. Provides encrypted secret storage (`getSecret`, `setSecret`, `deleteSecret`, `clearAllSecrets`) and permission checks (`grantedPermission`, `listAllPermissions`). See [Plugin Context (`ctx`)](./plugin-context.md). ### Example main.js File -Here's an illustrative example of a `main.js` file: +The official templates structure the plugin as an `AcodePlugin` class. Here is an illustrative example of a `main.js` file: ```javascript -acode.setPluginInit('com.example.plugin', (baseUrl, $page, cache) => { - const commands = acode.require("commands"); - commands.addCommand({ - name: 'example-plugin', - bindKey: { win: 'Ctrl-Alt-E', mac: 'Command-Alt-E' }, - exec: () => { - $page.innerHTML = ` -

Example Plugin

-

This is an example plugin.

- `; - $page.show(); - }, - }); -}); +import plugin from "../plugin.json"; + +class AcodePlugin { + baseUrl = ""; + + async init($page, cacheFile, cacheFileUrl, firstInit, ctx) { + const commands = acode.require("commands"); + commands.addCommand({ + name: "example-plugin", + bindKey: { win: "Ctrl-Alt-E", mac: "Command-Alt-E" }, + exec: () => { + $page.innerHTML = ` +

Example Plugin

+

This is an example plugin.

+ `; + $page.show(); + }, + }); + } + + async destroy() { + const commands = acode.require("commands"); + commands.removeCommand("example-plugin"); + } +} + +if (window.acode) { + const acodePlugin = new AcodePlugin(); + + acode.setPluginInit(plugin.id, async (baseUrl, $page, { cacheFileUrl, cacheFile, firstInit, ctx }) => { + acodePlugin.baseUrl = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`; + await acodePlugin.init($page, cacheFile, cacheFileUrl, firstInit, ctx); + }); + + acode.setPluginUnmount(plugin.id, () => { + acodePlugin.destroy(); + }); +} ``` ## Plugin Unmount Function -The `main.js` file must also define an unmount function, which is called when the plugin is unloaded or uninstalled. This function allows you to perform cleanup operations associated with your plugin. +The `main.js` file must also define cleanup logic, which is called when the plugin is unloaded or uninstalled. This cleanup allows you to remove listeners, commands, intervals, and UI hooks associated with your plugin. In the class template this lives in the `destroy()` method, registered via `acode.setPluginUnmount`. ### Example Unmount Function ```javascript -acode.setPluginUnmount('com.example.plugin', () => { +acode.setPluginUnmount(plugin.id, () => { const commands = acode.require("commands"); commands.removeCommand('example-plugin'); }); @@ -80,5 +108,5 @@ For command registration APIs, see [Commands](../utilities/commands.md). ::: :::tip -You will not need to write this `unmount` or `initialize` functions for your plugin because templates comes with it , just you will need to write your plugin code inside the `AcodePlugin class` +You will not need to write these `init`/`destroy` registration functions for your plugin because the templates ship with them. You only need to write your plugin code inside the `AcodePlugin` class. ::: diff --git a/docs/plugin-essentials/plugin-context.md b/docs/plugin-essentials/plugin-context.md new file mode 100644 index 0000000..93aa489 --- /dev/null +++ b/docs/plugin-essentials/plugin-context.md @@ -0,0 +1,130 @@ +# Plugin Context (`ctx`) + +The plugin context (`ctx`) is the third argument of the options object passed to your plugin's `init` function. It is a native-backed handle for your plugin that provides **encrypted secret storage** and **permission checks**. + +Your `init` function receives it as `options.ctx`: + +```js +function init(baseUrl, $page, options) { + const ctx = options.ctx; +} +``` + +## Overview + +`ctx` is a `PluginContext` instance (`src/lib/pluginContext.js`). It is created by Acode for **your plugin id only** and is backed by a cryptographically signed token issued by the native `Tee` plugin. Because of this: + +- Secrets are scoped to your plugin id - another plugin cannot read them. +- The token is bound to the permissions declared in your `plugin.json` at install/load time. +- The object is `Object.freeze`d, so its properties cannot be replaced or extended. + +### `created_at`, `uuid`, `toString()` + +- `created_at` - timestamp (ms) when the context was created. +- `uuid` - the opaque token string for this context. +- `ctx.toString()` - returns the `uuid` string. The object coerces to the uuid for string operations (numeric coercion returns `NaN`). + +```js +String(ctx) === ctx.uuid; // true +``` + +## Secrets + +Secrets are key/value strings stored in an **EncryptedPreferenceManager** on the native side (scoped to your plugin id). They survive app restarts. Use them for API tokens, oauth state, or other sensitive data - never store secrets in `localStorage`. + +### `getSecret(key, defaultValue = ""): Promise` + +Resolves the stored value for `key`, or `defaultValue` when the key has not been set. + +```js +const token = await ctx.getSecret("github_token", ""); +if (!token) { + await ctx.setSecret("github_token", "ghp_..."); +} +``` + +### `setSecret(key, value): Promise` + +Stores `value` for `key`. + +```js +await ctx.setSecret("access_token", "abc123"); +``` + +### `deleteSecret(key): Promise` + +Removes a single key. + +```js +await ctx.deleteSecret("access_token"); +``` + +### `clearAllSecrets(): Promise` + +Removes every secret stored for your plugin. + +```js +await ctx.clearAllSecrets(); +``` + +## Permissions + +Permissions are declared in your `plugin.json` as an array: + +```json +{ + "id": "com.example.plugin", + "main": "dist/main.js", + "permissions": ["read", "write"] +} +``` + +The list is bound to your context's token when the plugin loads. The native side grants exactly the permissions listed; there is no runtime "request" dialog - a permission either is or is not present. + +### `grantedPermission(permission): Promise` + +Resolves `true` when your plugin was granted `permission`. + +```js +if (await ctx.grantedPermission("write")) { + // do something privileged +} +``` + +### `listAllPermissions(): Promise` + +Resolves the full list of permissions granted to your plugin. + +```js +const permissions = await ctx.listAllPermissions(); +``` + +## Full example + +```js +function init(baseUrl, $page, options) { + const ctx = options.ctx; + + (async () => { + console.log("permissions:", await ctx.listAllPermissions()); + + if (await ctx.grantedPermission("api-access")) { + const token = await ctx.getSecret("api_token"); + if (!token) { + await ctx.setSecret("api_token", prompt("Enter API token")); + } + } + })(); +} +``` + +## Notes + +- `ctx.invalidate()` exists but is used internally by Acode; plugins do not need to call it. +- If the trusted native session is not available (for example the token request fails), `ctx` may be `null` - guard against it if your plugin depends on it. +- Secrets are encrypted at rest and scoped per plugin id. + +## Related + +- [Manifest (`plugin.json`)](./manifest.md) - declaring `permissions` +- [Core File](./core-file.md) - where `ctx` is passed to `init` diff --git a/user-guide/command-palette.md b/user-guide/command-palette.md index a711c10..65a864c 100644 --- a/user-guide/command-palette.md +++ b/user-guide/command-palette.md @@ -13,7 +13,7 @@ You can open the Command Palette using the standard shortcut: ### Mobile Devices (QuickTools) -On mobile devices where a physical keyboard might not be present, Acode provides **QuickTools**—a toolbar above the keyboard that contains essential keys like `Ctrl`. +On mobile devices where a physical keyboard might not be present, Acode provides **QuickTools** - a toolbar above the keyboard that contains essential keys like `Ctrl`. ![QuickTools](/quicktools.png)