# PopClip Extension Development β Complete Documentation
> Every page of PopClip's extension developer docs, concatenated into
> one file. Individual pages are at the source URLs given below. The
> TypeScript type definitions for the JavaScript API and config format are
> at https://www.popclip.app/dev/popclip.d.ts.
---
# PopClip Extensions Developer Documentation π€
> Source: https://www.popclip.app/dev/index.md
## Getting started
Here is a complete PopClip extension. To install it, select the whole block
of text, and PopClip will offer an "Install" action:
```js
// #popclip
// name: Hello
// icon: iconify:mingcute:wave-hand-line
const greeting = "Hello, " + popclip.input.text;
popclip.showText(greeting);
```

That is a [snippet](https://www.popclip.app/dev/snippets.md): plain text that PopClip installs straight
from a text selection. This one defines a
[JavaScript action](https://www.popclip.app/dev/js-actions.md) β everything after the comment header is
code, run when the action is clicked, in PopClip's
[JavaScript environment](https://www.popclip.app/dev/js-environment.md), with access to the selected
text and to PopClip itself through the global `popclip` object. (Wherever
these docs say JavaScript, that includes
[TypeScript](https://www.popclip.app/dev/js-environment.md#typescript-support), which PopClip supports
natively.)
An extension defines one or more actions. The Hello extension above is the
simplest form: one action, whose code is the snippet body. From there, a
[module extension](https://www.popclip.app/dev/js-modules.md) can define everything in code β multiple
actions, options, and dynamic behavior, via `defineExtension()`.
## No-code actions
For common tasks, you don't need to write code at all. Four action types are
ready-made conveniences β declarative wrappers around things that can also be
done in JavaScript:
| Action Type | Description | JavaScript equivalent |
| ------------------------------------------------------------- | ------------------------------------------------------- | -------------------------- |
| [URL](https://www.popclip.app/dev/url-actions.md) | Open a URL, with the selected text inserted as a query. | `popclip.openUrl()` |
| [Key Press](https://www.popclip.app/dev/key-press-actions.md) | Press a key combination. | `popclip.pressKey()` |
| [Service](https://www.popclip.app/dev/service-actions.md) | Send the selected text to a macOS Service. | `popclip.performService()` |
| [Shortcut](https://www.popclip.app/dev/shortcut-actions.md) | Send the selected text to a macOS Shortcut. | `popclip.runShortcut()` |
## Classic script actions
Two further action types run a script that you provide to be run outside PopClip.
They predate the JavaScript environment, but they remain supported.
| Action Type | Description | JavaScript equivalent |
| ------------------------------------------------------------------- | -------------------------- | -------------------------- |
| [AppleScript](https://www.popclip.app/dev/applescript-actions.md) | Run an AppleScript script. | `popclip.runAppleScript()` |
| [Shell Script](https://www.popclip.app/dev/shell-script-actions.md) | Run a shell script. | `popclip.runShellScript()` |
## Snippets and Packages
A PopClip extension can be either a [snippet](https://www.popclip.app/dev/snippets.md) or a
[package](https://www.popclip.app/dev/packages.md). The following table summarizes the differences:
| | Snippet | Package |
| --------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| What is it? | Plain text: a script with a config comment header, or YAML config alone. | A folder containing a config file plus other files such as icons, source files, and a readme file. |
| Install method | PopClip can load it directly from a text selection. | Double-clicking it will open it in PopClip. |
| Distribution | Can be copied and pasted as text, e.g. on forums, pastebins, etc. | Can be downloaded as a file. |
| Signing | Not signed. | Can be signed. |
| Advantages | Easy to create and informally share. No need for separate files. | Easy for end user to install. Allows modular source code with complex functionality. |
| Disadvantages | Limited to what can be done with a single text file. | More complex to create. Steeper learning curve. |
| File extensions | None (direct selection); `.popcliptxt`, `.js`, `.ts`, `.yaml` (text files) | `.popclipext` (folder); `.popclipextz` (zipped folder) |
### Package signing
Packages published in the [directory](https://www.popclip.app/extensions/) are
digitally signed. Signing tells PopClip it can trust the extension. PopClip will install signed extensions without showing any
warning to the user.
If you create your own extension β whether snippet or package β it will be unsigned.
If an unsigned extension contains Shell Script actions or AppleScript actions, or has entitlements,
PopClip will display a warning dialog when you try to install it:

If an unsigned extension is purely JavaScript (with no entitlements)
or contains only the [no-code action types](#no-code-actions), PopClip installs the extension without showing the warning.
## Development environment
### Type definitions
The complete TypeScript definitions for PopClip's JavaScript API are published
as a single file, [popclip.d.ts](https://www.popclip.app/dev/popclip.d.ts). The same definitions are available as the
[`@popclip/types`](https://www.npmjs.com/package/@popclip/types) npm package,
and as browsable HTML in the
[JavaScript API Reference](https://www.popclip.app/dev/api/).
As well as the `popclip` object and other globals available to scripts, this
definitions file describes the extension config format itself.
To set up your editor with the definitions, for autocomplete and type
checking, see [TypeScript support](https://www.popclip.app/dev/js-environment.md#typescript-support).
### Turn off unsigned warning
If the unsigned extension warning gets annoying while you test your work, you
can turn it off. Run the following command at the Terminal, then Quit and
restart PopClip:
`defaults write com.pilotmoon.popclip LoadUnsignedExtensions -bool YES`
And if you are working on an extension with the `com.pilotmoon.` identifier prefix:
`defaults write com.pilotmoon.popclip AllowUnsignedReservedPrefixes -bool YES`
### Debug output
To help you when creating extensions, PopClip can send script outputs and other
debug info to the Console app. To enable it, run this command in Terminal, then
Quit and restart PopClip:
`defaults write com.pilotmoon.popclip EnableExtensionDebug -bool YES`
You can then view the debug output in the Console app.

To filter the Console to show just PopClip extensions, enter Process "PopClip"
and Category "Extension" in the Search field.
You can add this as a saved search by clicking the Save button in the toolbar:

## Show off your work
If you create an extension that others might find useful, you are welcome to
submit it for publication in the
[PopClip Extensions Directory](https://www.popclip.app/extensions/). See
[Submit an Extension](https://www.popclip.app/extensions/submit.md) for how it works.
## Getting help
If you have any questions or need help with developing an extension, post to
the [PopClip Forum](https://forum.popclip.app). I frequently check the forum
and will be happy to help you there.
---
# Snippets
> Source: https://www.popclip.app/dev/snippets.md
A snippet is the simplest kind of PopClip extension, because it is just plain
text. A snippet begins with a `#popclip` (or `# popclip`) marker line.
```javascript
// #popclip
// name: Title Case
// icon: scale=120 move-x=3 circle filled Tc
const titled = popclip.input.text.replace(
/\S+/g,
(word) => word[0].toUpperCase() + word.slice(1).toLowerCase(),
);
popclip.pasteText(titled);
```
When you select the text of a snippet, PopClip offers an "Install" action, as
shown in the [introduction](https://www.popclip.app/dev/index.md). (Try it!)
**Tip: Size limit, and snippet files**
When installed via text selection, snippets can be up to 5,000 characters long. Snippet files, on the other hand, have no maximum length.
To install a snippet from a file, save it as a text file with one of these extensions: `.ts`, `.js`, `.yaml` and send it
to PopClip using "Open With" in Finder, or drag the file onto PopClip's menu bar icon. A special file
extension, `.popcliptxt`, can also be used: PopClip opens it when you double-click it.
Snippets come in two forms:
- A **code snippet** is a script, with the extension's config in a comment
header.
- A **config snippet** is config alone, in YAML format β most useful for the
[no-code action types](https://www.popclip.app/dev/index.md#no-code-actions).
## Code snippets {#inverted-syntax}
Here is a complete code snippet:
```javascript
// #popclip
// name: Uppercase
// icon: square filled AB
popclip.pasteText(popclip.input.text.toUpperCase());
```
The config header is a run of comment lines starting at the `#popclip`
marker, containing the extension's [config](https://www.popclip.app/dev/config.md) as YAML. Everything
after the header is the script itself.
Code snippets (formerly called _inverted syntax_) are supported for
JavaScript, AppleScript and shell script actions. The whole text of the snippet becomes the `javaScriptFile`,
`module`, `appleScriptFile` or `shellScriptFile` for the extension, as
follows:
| To interpret as... | Include these fields... |
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `javaScriptFile` or `module` | Nothing needed: a code body under a `//` comment header is treated as TypeScript by default. (Specify `language: javascript` to treat as raw JavaScript instead.) A body that exports is loaded as `module` (see [Module detection](https://www.popclip.app/dev/js-modules.md#module-detection)), otherwise as `javaScriptFile`. |
| `appleScriptFile` | Nothing needed: a body under a `--` comment header is treated as AppleScript. |
| `shellScriptFile` | Specify `interpreter`, or start the snippet with a `#!` line. |
**Tip: Inference is new**
Language and module inference is new in PopClip 2026.8.1 (6221). If you try to install
a code snippet that specifies no `language`, `interpreter` or `module` on an older version of
PopClip, it will fail with the error message "Specify language or
interpreter".
### Non-JavaScript snippets
Code snippets are not just for JavaScript β they can also be used with shell scripts and with AppleScript.
The config header should be added using the appropriate comment style for the
source language, as in the examples below.
Here is a Python example, using `#` for the comment header:
```python
# #popclip
# name: Hello Python
# icon: circle hi
# after: show-result
# interpreter: python3
import os
print('Hello, ' + os.environ['POPCLIP_TEXT'] + '!', end='')
```
An alternative way to specify a shell script's interpreter is to put a shebang
(`#!`) line at the top of the snippet, before the `#popclip` marker line. Then the `interpreter` field is not needed:
```python
#!/usr/bin/env python3
# #popclip
# name: Hello Python (shebang)
# icon: circle hi
# after: show-result
import os
print('Hello again, ' + os.environ['POPCLIP_TEXT'] + '!', end='')
```
Using the `--` comment prefix without specifying an interpreter tells PopClip that the body is AppleScript:
```applescript
-- #popclip
-- name: LaunchBar
-- icon: LB
tell application "LaunchBar"
set selection to "{popclip text}"
end tell
```
## Config snippets
A config snippet is parsed as [YAML 1.2](https://yaml.org/spec/1.2.2/). The
body of the snippet defines the extension's [config dictionary](https://www.popclip.app/dev/config.md).
For example:
```yaml
#popclip
name: Urban Dictionary
icon: UD
url: https://www.urbandictionary.com/define.php?term=***
```
**Tip: Comments in snippets**
Note that `#` begins a YAML comment. Thus the entire snippet including the
`#popclip` line parses as valid YAML.
### More config snippet examples
A [Shortcuts](https://www.popclip.app/dev/shortcut-actions.md) example:
```yaml
# popclip shortcuts example
name: Run My Shortcut
icon: symbol:moon.stars # Apple SF Symbols
shortcutName: My Shortcut Name
```
A [Service](https://www.popclip.app/dev/service-actions.md) example (this time using flow-style YAML markup,
with braces):
```yaml
#popclip service example
name: Stickies
serviceName: Make Sticky
```
A [Key Press](https://www.popclip.app/dev/key-press-actions.md) example:
```yaml
#popclip key press example
name: Key Press Example
keyCombo: command option J
```
A [shell script](https://www.popclip.app/dev/shell-script-actions.md) example:
```yaml
#popclip shellscript example
name: Say
interpreter: zsh
shellScript: say -v Daniel $POPCLIP_TEXT
```
A [JavaScript](https://www.popclip.app/dev/js-actions.md) example, including multiple actions:
```yaml
#popclip js + multi action example
name: Markdown Formatting
requirements: [text, paste]
actions:
- title: Markdown Bold # note: actions have a `title`, not a `name`
icon: circle filled B
javaScript: popclip.pasteText('**' + popclip.input.text + '**')
- title: Markdown Italic
icon: circle filled I
javaScript: popclip.pasteText('*' + popclip.input.text + '*')
```
**Warning: #1 rule of YAML: Do not indent with tabs!**
When writing snippets in YAML with indented parts, as in the example above, do
not use tabs for indenting. YAML does not allow it β use spaces instead.
## Developing with snippets
PopClip will display any errors it encounters while trying to load the snippet
in the PopClip bar itself.

In the absence of an `identifier` field, the `name` acts as the identifier for
the extension. Installing a snippet with the same name as an existing snippet
will replace it.
A snippet can do everything that a [package](https://www.popclip.app/dev/packages.md) extension can do. The
only limitation is that it is completely self-contained: it can't refer to any additional files.
If you want to include a custom icon file, additional source files, or resource files, use a package instead.
---
# JavaScript actions
> Source: https://www.popclip.app/dev/js-actions.md
A JavaScript action runs code in PopClip's own
[JavaScript environment](https://www.popclip.app/dev/js-environment.md), with access to the selected text
and to PopClip itself through the global `popclip` object. It is the simplest
way to run code in PopClip. In a
[code snippet](https://www.popclip.app/dev/snippets.md#inverted-syntax), everything after the header is
the action's code, run when the action is clicked:
```javascript
// #popclip
// name: Uppercase
// icon: square filled AB
// after: paste-result
return popclip.input.text.toUpperCase();
```
## Properties
A JavaScript action is defined by a code snippet whose config header uses the `//` comment prefix.
Alternatively, a config snippet may define a `javaScript` or `javaScriptFile` field, as follows:
| Key | Type | Description |
| ---------------- | ------ | ------------------------------------------------------- |
| `javaScript` | String | A JavaScript text string to load. |
| `javaScriptFile` | String | Path to a `.js` or `.ts` file in the package directory. |
For example, here is a config snippet with the action's code inline in the
`javaScript` field:
```yaml
#popclip
name: Word Count
icon: square 123
javaScript: popclip.showText(popclip.input.text.split(/\s+/).length + " words")
```
A code snippet is equivalent to a config snippet whose `javaScriptFile` is the snippet itself.
### Script format
The script's entry point is at the top level of the file. Internally, PopClip
loads the provided script and wraps it as a function. When the action is run,
PopClip calls the function.
**Function wrapper detail**
As an example, imagine the following JavaScript is provided in the `javaScript`
field:
```javascript
return "foo";
```
Internally, this will be wrapped in an async arrow function definition like
this:
```javascript
const main = async () => {
return "foo";
};
```
When the action is run, PopClip calls this internal `main` function with no
arguments.
In addition to pure JavaScript, PopClip can load TypeScript from files named
with a `.ts` extension. See
[TypeScript support](https://www.popclip.app/dev/js-environment.md#typescript-support).
## Input and output
Scripts take their input from the
[global `popclip` object](https://www.popclip.app/dev/js-environment.md#global-popclip-object).
If the script exits by returning a string, it will be passed to the `after`
step.
**Note: Return type**
To return a value to the `after` step, it must be of type `string`. If the
script returns a value of any other type, such as `number` or `object`, PopClip
will ignore it.
## Indicating errors
Scripts should indicate success by completing normally (either by explicitly
returning a value, or implicitly returning `undefined`) and should indicate
failure by throwing an error. PopClip will catch any erros thrown by the script
and display the shaking-'X'.
To indicate an error with the user's settings, and pop up the extension's
settings UI, throw an error message starting with the specific words
`settings error` or `not signed in` (not case sensitive). For example:
```javascript
throw new Error("Settings error: missing API key");
```
## Examples
These examples are all complete [code snippets](https://www.popclip.app/dev/snippets.md#inverted-syntax) β
select the whole block to install one.
Paste the selected text, then press Return β two PopClip primitives chained
with `await`:
```javascript
// #popclip
// name: Paste & Enter
// icon: symbol:return
// requirements: [paste]
await popclip.pasteText(popclip.input.text);
await popclip.pressKey("return");
```
Look up the selected word in the macOS dictionary, then speak the definition
aloud through the `say` command β using the
[`$` shell tag](https://www.popclip.app/dev/external-scripts.md):
```javascript
// #popclip
// name: Speak Definition
// icon: symbol:character.book.closed
// entitlements: [script]
const word = popclip.input.text.trim();
const definition = util.getDictionaryDefinition(word) ?? "no definition found";
await $`say ${definition}`;
```
Fetch the page at the selected URL and show its title β network access, a
bundled module, and the `after` step working together:
```javascript
// #popclip
// name: Page Title
// icon: symbol:globe
// requirements: [url]
// entitlements: [network]
// after: show-result
const axios = require("axios");
const response = await axios.get(popclip.input.data.urls[0]);
return (
String(response.data).match(/
]*>([^<]*) Source: https://www.popclip.app/dev/js-modules.md
Module extensions let you use the full power of JavaScript
to define your PopClip extension. This allows you use code to construct
properties like `options` at load time, and to define `actions` dynamically, for
example to generate titles or icons in response to the input text.
If the extension's JS code exports anything via [`defineExtension()`](#module-format) (or `export`),
PopClip loads it as a module and looks for extension properties in the
exported object, after first loading static properties from YAML in the comment header.
(A file that exports nothing is
instead treated as a simple (non-module) JavaScript action, run at
click time β see [JavaScript actions](https://www.popclip.app/dev/js-actions.md).)
All properties exported by the module will be merged into the extension's
config, overriding any static properties with the same name (except for the
[static-only properties](#static-only-properties) which cannot be overriden).
The module can also define a population function to dynamically populate the
actions.
## Example
The following snippet defines a complete module extension:
```javascript
// #popclip
// name: Module Demo
// after: show-result
// this is only run once, at load time
const theNumber = String(Math.floor(Math.random() * 100));
defineExtension({
actions: [
{
title: "The Title",
icon: `square ${theNumber}`,
code: (input) => {
return `The number is ${theNumber}. Your text is: ${input.text}`;
},
},
],
});
```
Observe a few things:
- The extension's `name` and the action's `after` step, `show-result`, are
specified in the static config in the header.
- At load time, the module generates a random number and saves it in a variable.
- The action has an `icon` property, displaying the random number in a
square.
- The module defines the extension by passing an object with an `actions`
array to `defineExtension()`. See [Module actions](#module-actions).
### More examples
See the following examples from the PopClip Extensions Directory:
- _Paste and Match Style_,
[Config.ts](https://github.com/pilotmoon/PopClip-Extensions/blob/master/source/PasteAndMatch.popclipext/Config.ts)
- _Shuffle_,
[Config.ts](https://github.com/pilotmoon/PopClip-Extensions/blob/master/source/Shuffle.popclipext/Config.ts)
- _Brackets_,
[Config.ts](https://github.com/pilotmoon/PopClip-Extensions/blob/master/source/Brackets.popclipext/Config.ts)
## File format
### Comment header
In `Config.js` and `Config.ts` a YAML comment header must be provided defining
the extension's `name` and any other
[static-only properties](#static-only-properties). The header is in the same
format as for a snippet (see
[Code snippets](https://www.popclip.app/dev/snippets.md#inverted-syntax)). No `language` or
`module` keys are needed: the file suffix selects the language, and the code's
exports mark it as a module.
### Module detection
PopClip loads a JavaScript file (or snippet body) as a module
when the code contains ES module `export` syntax, a call to
`defineExtension()` or `define()`, or a reference to `module` or `exports`.
Comments and strings don't count, and top-level `import` alone does not make a
module β an action's code may import libraries too. Set `module: true` or
`module: false` in the header to override the detection.
### Module format
The module file may be written in JavaScript (`.js`) or TypeScript (`.ts`).
The recommended way to define the extension is to call `defineExtension()`,
passing the extension object. Because the parameter is typed, every property
of the object written inside the call is checked and autocompleted in your
editor, with no type annotations needed anywhere. This is the form we use
for our own extensions.
The exported property names and types are the same as defined in
[Top-level properties](https://www.popclip.app/dev/top-level-properties.md), with the exception of `actions` which has special handling β
see [Module actions](#module-actions).
#### Other export styles
Instead of `defineExtension()`, you can use
[ES module](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules)
`export` syntax. Export a single default extension object:
```javascript
export default { action: () => popclip.showText("hi!") };
```
or export individual properties as named exports:
```javascript
export const action = () => popclip.showText("hi!");
```
β one or the other, not both (mixing them is a load error).
CommonJS style (`module.exports = ...`, `exports.action = ...`) is also
supported. In fact, at runtime `defineExtension(obj)` is simply
`module.exports = obj` β the difference is entirely one of types.
#### Typed options
In TypeScript, specifying `defineExtension()`'s generic type parameter
extends type checking to the `options` parameter of action functions and the
population function. `InferOptions` derives that type from the options array
itself, so nothing is restated:
```typescript
// #popclip
// { name: Prefixer }
// the options array is declared first so its type can be inferred
const options = [
{ identifier: "prefix", type: "string", defaultValue: ">" },
] as const;
defineExtension>({
options,
action: (input, options) => {
// options.prefix is known to exist, and to be a string
popclip.pasteText(options.prefix + input.text);
},
});
```
## Specifying the module file
The module does not have to be loaded from `Config.js`/`Config.ts`.
Alternatively, you can provide static config in another format (e.g.
`Config.json`) and specify a module file name as follows:
| Key | Type | Description |
| -------- | ------ | ------------------------------------------ |
| `module` | String | The path to a `.js` or `.ts` file to load. |
## Static-only properties
Certain properties of the extension can only be defined in the static config,
and cannot be overriden by the module. These are `name`, `icon`, `identifier`,
`popclipVersion`, `macosVersion`, `entitlements`, `module`, `showAs` and
`offersMultipleInstances`.
## Module actions
**Tip: Detailed API reference**
A more detailed definition of the action object, action function and population
function may be found in the
[JavaScript API Reference](https://www.popclip.app/dev/api/),
or in [**popclip.d.ts**](https://www.popclip.app/dev/popclip.d.ts), which is the same API as a single
TypeScript definitions file.
A module defines its actions with the `actions` property of the extension
object, which can be either:
- an array of [action objects](#action-object), or
- a [population function](#population-function) returning an array of action
objects.
Note that a module always provides _all_ the actions for the extension. You
cannot mix regular actions and module actions in the same extension.
### Action object
Each action object takes the same [properties](https://www.popclip.app/dev/actions.md) as a regular
action, with one caveat and the additions below. The caveat: the action
flags β `title`, `icon`, `requirements`, `regex`, `before`, `after` and so
on β work as they do in static config, but the action-type properties such
as `url`, `keyCombo` and `shellScript` are static config only and are
ignored in module actions. A module action's behavior comes from its `code`
function.
| Key | Type | Description |
| --------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `code` | Function | A function to run when the action is invoked. See: [Action function](#action-function). |
| `regex` | RegExp Object | You may export a JavaScript [RegExp](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp), and PopClip will use this instead of a string regex. |
| `submenu` | Array or Function | An array of action objects to show in a submenu of this action, or a function generating them dynamically. See [Submenu functions](#submenu-functions). |
### Action function
The action function is called with the following arguments:
- `input`: same object as `popclip.input`
- `options`: same object as `popclip.options`
- `context`: same object as `popclip.context`
**synchronous**
```javascript
{
code: (input, options, context) => {
// ... do stuff ...
doSomething();
return someResult;
};
}
```
**with async/await**
```javascript
{
code: async (input, options, context) => {
// ... do stuff ...
await doSomethingAsync();
return someResult;
};
}
```
The function may return a string, which will be passed to the `after` step.
Otherwise it should return `undefined` or `null`.
The function may optionally be `async`, and use `await`.
The function may indicate an error by throwing an exception, as per
[JavaScript actions](https://www.popclip.app/dev/js-actions.md#indicating-errors).
### Population function
**Warning: Entitlement needed**
To use a population function, the `dynamic` entitlement must be present in the
`entitlements` array in the static config. This cannot be set if the `network`
or `script` entitlement is also being used.
The population function is set as the `actions` property of the extension
object. It dynamically supplies actions every time the PopClip bar appears.
The population function is called with the same arguments as the action
function, and it returns an array of action objects.
```javascript
// #popclip dynamic example
// { name: Dynamic Title, entitlements: [dynamic] }
defineExtension({
actions: (input, options, context) => {
return [
{
title: `<${input.text.slice(0, 10)}>`,
code: (input, options, context) => {
popclip.showText("Hi from Action");
},
},
];
},
});
```
As a keyless snippet this loads through the TypeScript pipeline, so
`defineExtension()` type-checks the population function and the actions it
returns.
### Restrictions during population
The population function has the following limitations:
- Cannot access the network β `XMLHttpRequest` is unavailable.
- Cannot call _functions_ on the `popclip` global object.
- Cannot call `sleep()`, `setTimeout()` or `setInterval()`.
- Cannot access `secret` options in `popclip.options`.
_Properties_ on the `popclip` global (`popclip.input`, `popclip.context`, `popclip.options` and `popclip.modifiers`)
_may_ be read during population β with the exception of `secret` options.
Functions on the [`util`](https://www.popclip.app/dev/js-environment.md#global-util-object) global
may be called freely during population.
### Submenu functions
An action object may define a `submenu` property, giving the action a submenu
of child actions β see [Submenus](https://www.popclip.app/dev/actions.md#submenus). The value may be a
static array of action objects, or a function.
If a function is supplied, it is called at the moment the submenu opens, to
generate the submenu's actions dynamically. It has the same signature and
limitations as a [population function](#population-function), and likewise
requires the `dynamic` entitlement.
```typescript
// #popclip submenu function example
// { name: Sub Demo, icon: circle filled 3, entitlements: [dynamic] }
defineExtension({
actions: [
{
title: "Word Menu",
// called when the submenu opens: one child action per word, capped at 3
submenu: (input) => {
return input.text
.split(/\s+/)
.slice(0, 3)
.map((word) => ({
title: word,
code: () => popclip.showText(`You chose: ${word}`),
}));
},
},
],
});
```
## Abbreviated forms
### The `action` property
If the extension defines only a single action, it may be given as the
`action` property instead of in an `actions` array. For example:
```javascript
// #popclip
// { name: Single Action}
defineExtension({
action: {
code: () => {
popclip.showText("hi mom!");
},
},
});
```
### Action function shorthand
If the action object has only a `code` property, it may be given as a
function instead of an object. For example:
```javascript
// #popclip
// { name: Action Function}
defineExtension({
action: () => {
popclip.showText("hi mom!");
},
});
```
---
# JavaScript environment
> Source: https://www.popclip.app/dev/js-environment.md
JavaScript actions and module extensions run inside PopClip's JavaScript
environment. This environment provides properties and functions that
let your scripts interact with PopClip. Scripts run in a secure JavaScript
sandbox that cannot access the filesystem.
## PopClip globals
PopClip predefines several global objects and functions in the JavaScript
environment for extensions to use. These are documented in detail in the
[JavaScript API Reference](https://www.popclip.app/dev/api/).
The following is a summary of the commonly needed parts.
**Tip: Complete definitions in one file**
The same API is defined in
[**popclip.d.ts**](https://www.popclip.app/dev/popclip.d.ts), a single TypeScript definitions file.
Use it for editor autocomplete, or hand it to an AI coding assistant.
### Global `popclip` object
#### Readonly Properties
Scripts can access the selected text and other input via properties of the
[`popclip global`](https://www.popclip.app/dev/api/interfaces/PopClip.html).
Commonly used properties are:
- `popclip.input.text`: the full plain text selection
- `popclip.input.matchedText`: the part of the text matching the requirement or
regex
- `popclip.input.regexResult`: if regex was specified, this is an array
containing the full result of the match, including any capture groups
- `popclip.input.html`: the html backing the selection (if `captureHtml` is
set)
- `popclip.input.markdown`: the markdownified html (if `captureHtml` is set)
- `popclip.input.data.urls`: array of detected web URLs
- `popclip.context.browserUrl`, `popclip.context.browserTitle`: browser page URL
and title, if available
- `popclip.context.appName`, `popclip.context.appIdentifier`: app name and
bundle identifier
- `popclip.modifiers.command`, `popclip.modifiers.option`,
`popclip.modifiers.shift`, `popclip.modifiers.control`: booleans for modifier
keys pressed
- `popclip.options`: an object with properties for each option, where the
property name is the option's identifier. Option values can be either strings
or booleans
#### Methods
Scripts can perform actions via calling methods on the
[`popclip`](https://www.popclip.app/dev/api/interfaces/PopClip.html)
global:
- [`popclip.pasteText()`](https://www.popclip.app/dev/api/interfaces/PopClip.html#pastetext):
paste a given string (similar to `paste-result`)
- [`popclip.copyText()`](https://www.popclip.app/dev/api/interfaces/PopClip.html#copytext):
copy a string to the clipboard (similar to `copy-result`)
- [`popclip.showText()`](https://www.popclip.app/dev/api/interfaces/PopClip.html#showtext):
show a string in the PopClip bar (similar to `show-result`)
- [`popclip.openUrl()`](https://www.popclip.app/dev/api/interfaces/PopClip.html#openurl):
open a URL (similar to a URL action)
- [`popclip.pressKey()`](https://www.popclip.app/dev/api/interfaces/PopClip.html#presskey):
presses a key combo (similar to a key press extension)
- [`popclip.pressKeys()`](https://www.popclip.app/dev/api/interfaces/PopClip.html#presskeys):
presses a sequence of key combos, with optional waits between them
- [`popclip.runAppleScript()`](https://www.popclip.app/dev/api/interfaces/PopClip.html#runapplescript),
[`popclip.runAppleScriptFile()`](https://www.popclip.app/dev/api/interfaces/PopClip.html#runapplescriptfile):
run an AppleScript, from source text or from a file in the extension package
(requires the `script` entitlement)
- [`popclip.performCommand()`](https://www.popclip.app/dev/api/interfaces/PopClip.html#performcommand):
perform a cut, copy or paste command in the foreground app (simlar to the
`before` and `after` steps)
- [`popclip.runShortcut()`](https://www.popclip.app/dev/api/interfaces/PopClip.html#runshortcut):
run a macOS Shortcut by name (similar to a
[Shortcut action](https://www.popclip.app/dev/shortcut-actions.md))
- [`popclip.revealFile()`](https://www.popclip.app/dev/api/interfaces/PopClip.html#revealfile):
show a file or folder in the Finder
- [`popclip.showSuccess()`](https://www.popclip.app/dev/api/interfaces/PopClip.html#showsuccess),
[`popclip.showFailure()`](https://www.popclip.app/dev/api/interfaces/PopClip.html#showfailure),
[`popclip.showSettings()`](https://www.popclip.app/dev/api/interfaces/PopClip.html#showsettings):
show a check mark, shaking-X, or Pop up the extension's settings
- [`popclip.signInRequiredError()`](https://www.popclip.app/dev/api/interfaces/PopClip.html#signinrequirederror),
[`popclip.settingsRequiredError()`](https://www.popclip.app/dev/api/interfaces/PopClip.html#settingsrequirederror):
construct errors that the action can throw to indicate that the user needs to
sign in or adjust the extension's settings
### Global `util` object
Where the methods on `popclip` _do_ something, the functions on the
[`util`](https://www.popclip.app/dev/api/interfaces/Util.html) global
are passive. They include general helpers β randomization, encoding, hashing, locale
and time zone information, and macOS dictionary and spelling lookups.
Unlike the methods on `popclip`, these can be called from a
[population function](https://www.popclip.app/dev/js-modules.md#population-function) β see
[Restrictions during population](https://www.popclip.app/dev/js-modules.md#restrictions-during-population).
### Global `pasteboard` object
Scripts can also have direct read/write access the macOS clipboard via the
[`pasteboard`](https://www.popclip.app/dev/api/interfaces/Pasteboard.html)
global:
- `pasteboard.text` - the current plain text content of the clipboard, a
read/write property.
### Global `print()` function
There is a global function
[`print()`](https://www.popclip.app/dev/api/functions/print.html) for
debug output. You can
[view the debug output in the Console.app](https://www.popclip.app/dev/index.md#debug-output) and also in the
[test harness](#test-harness).
## Language version and libraries
PopClip's JavaScript engine is Apple's
[JavaScriptCore](https://developer.apple.com/documentation/javascriptcore),
which is part of macOS. Language features will vary depending on the macOS
version PopClip is running on. However, you can assume availability of language
features up to at least ES2023 on all macOS versions that PopClip supports
(macOS 13 and later).
**Tip: JavaScript reference**
The website I use and recommend to learn about the JavaScript language, the
Standard Library and other APIs, is
[MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference).
### Standard built-in objects
For the
[Standard Library](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects),
PopClip supplements the built-in JavaScript objects provided by macOS with
polyfills from [core-js](https://github.com/zloirock/core-js). This means that
you can use the latest features up to ES2023 on all macOS versions.
### Web APIs and Node globals
PopClip provides a limited subset of the standard
[Web APIs](https://developer.mozilla.org/en-US/docs/Web/API) that are normally
available in a browser environment:
- [URL](https://www.popclip.app/dev/api/classes/URL.html) and
[URLSearchParams](https://www.popclip.app/dev/api/classes/URLSearchParams.html)
- [XMLHttpRequest](https://www.popclip.app/dev/api/classes/XMLHttpRequest.html)
- [setTimeout](https://www.popclip.app/dev/api/functions/setTimeout.html) and
[clearTimeout](https://www.popclip.app/dev/api/functions/clearTimeout.html)
- [setInterval](https://www.popclip.app/dev/api/functions/setInterval.html) and
[clearInterval](https://www.popclip.app/dev/api/functions/clearInterval.html)
- [structuredClone](https://www.popclip.app/dev/api/functions/structuredClone.html)
Additionally, from the Node.js environment:
- [Buffer](https://www.popclip.app/dev/api/classes/Buffer.html)
Some further globals are present only as compatibility shims to support the bundled modules:
`Blob`, `TextEncoder`, `atob` and `btoa`. These are
reduced implementations, not recommended for direct use β prefer `Buffer` and
the `util` encoding functions.
### Bundled libraries
Some libraries from [NPM](https://www.npmjs.org/) are bundled within the PopClip
app itself, and are available to load by scripts. These are:
| Library | Version | Description |
| ---------------------------- | ------- | ---------------------------------------- |
| `axios` | 1.12.2 | HTTP client |
| `buffer` | 6.0.3 | Node-compatible `Buffer` implementation |
| `case-anything` | 2.1.13 | Case conversion utilities |
| `content-type` | 1.0.5 | Parse HTTP `Content-Type` headers |
| `dom-serializer` | 2.0.0 | Serialize DOM nodes to HTML |
| `emoji-regex` | 10.6.0 | Regular expression matching emojis |
| `entities` | 7.0.0 | HTML entity encoder/decoder |
| `fast-json-stable-stringify` | 2.1.0 | Deterministic JSON stringify |
| `fast-plist` | 0.1.3 | Parse and serialize macOS property lists |
| `htmlparser2` | 10.0.0 | HTML parser |
| `js-yaml` | 4.1.0 | YAML parser |
| `linkedom` | 0.18.12 | Lightweight DOM implementation |
| `linkifyjs` | 4.3.3 | Detect and linkify URLs in text |
| `oauth-1.0a` | 2.2.6 | OAuth 1.0a signing helpers |
| `rot13-cipher` | 1.0.0 | ROT13 encoder/decoder |
| `sanitize-html` | 2.17.0 | HTML sanitizer |
| `sucrase` | 3.35.1 | Fast TypeScript/JS transformer |
| `turndown` | 7.2.1 | HTML to Markdown converter |
| `valibot` | 1.1.0 | Validation and parsing library |
Library modules are imported by name β see below.
## Importing other modules {#using-require}
A script can import the [bundled libraries](#bundled-libraries), and other
files from the extension package, using `import` syntax:
```javascript
import axios from "axios"; // a bundled library
import { helper } from "./helper.js"; // another file in the package
import strings from "./data/strings.json"; // JSON parses to an object
```
Equivalently, you can call the `require()` function β `import` statements are
converted to `require()` calls under the hood:
```javascript
const axios = require("axios");
```
### Module resolution
The module specifier string is interpreted as follows:
- If it starts with `./` or `../`, it is a path to a file in the package
directory, relative to the current file.
- Otherwise, it is tried as a path relative to the root of the package
directory; if no file is found there, it is then matched against the names
of the [bundled libraries](#bundled-libraries).
Paths beginning with `/`, or using `..` to go up outside the package
directory, are not valid.
The imported value is the module's exported value, or the parsed JSON object.
Results are cached: importing the same specifier again returns the same
instance. If nothing is found, or the path is invalid, the value is
`undefined`.
### Supported file types
The module loader can load the following file types:
| File extension | Description |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `.js` | A JavaScript module, in [ES module](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) or [CommonJS](https://www.typescriptlang.org/docs/handbook/2/modules.html#commonjs-syntax) format. |
| `.ts` | A TypeScript module, likewise in either format. |
| `.json` | A JSON file parsed into a JavaScript object. |
If no file name extension is specified, PopClip will try `.js`, `.ts`, `.json`
in order.
## Asynchronous operations and async/await
Asynchronous operations are fully supported: your functions can be `async`,
and you can use the `await` keyword when calling any function that returns a
Promise. If a script starts asynchronous work β a network request, a timer β
PopClip shows its spinner and waits until the last operation has finished.
Clicking the spinner cancels all current operations.
The action's result is always the script's own return value; values produced
inside callbacks or timers do not become the result.
As a convenience, PopClip supplies a global function `sleep()`, a
promise-based wrapper around `setTimeout()`:
```javascript
// #popclip
// name: Await Test
await sleep(5000); // 5 second delay
popclip.showText("Boo!");
```
## Network access from JavaScript
**Warning: Entitlement needed**
To use XHR, the `network` entitlement must be present in the `entitlements`
array in the extension's config.
PopClip provides its own implementation of
[`XMLHttpRequest`](https://www.popclip.app/dev/api/classes/XMLHttpRequest.html)
(XHR). This is the only way for JavaScript code to access the network.
PopClip is also bundled with the HTTP library
[axios](https://axios-http.com/), which is an easier to use wrapper
around XHR.
Due to macOS's App Transport Security, requests to a named host must use
`https:` β plain `http:` URLs throw a network error. The exception is that
`http:` works for `localhost` and for numeric IP addresses, which is handy
for talking to a server on the local machine or network.
Here's an example extension snippet that downloads a selected URL's contents,
and copies it to the clipboard:
```javascript
// #popclip
// name: Download Text
// icon: symbol:square.and.arrow.down.fill
// requirements: [url]
// entitlements: [network]
// after: copy-result
import axios from "axios";
const response = await axios.get(popclip.input.data.urls[0]);
/* note: there is no particular need to check the return status here.
axios calls will throw an error if the HTTP status is not 200/2xx. */
return response.data;
```
For a more substantial axios example, see for example
[Instant Translate](https://github.com/pilotmoon/PopClip-Extensions/tree/master/source/InstantTranslate.popclipext).
## TypeScript support
PopClip has built-in support for [TypeScript](https://www.typescriptlang.org/).
You can supply TypeScript source in any place where a JavaScript file can be
specified. PopClip loads files with a `.js` extension as raw JavaScript, and
loads files with a `.ts` extension as TypeScript.
At load time, PopClip transpiles TypeScript files into JavaScript source.
PopClip does not do any type validation on the TypeScript source.
### TypeScript configuration
When working with TypeScript files you'll want to provide a
[tsconfig.json](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html)
file. For my current recommended `compilerOptions`, see the one in the
PopClip-Extensions repo:
- [Example tsconfig.json for PopClip extensions](https://github.com/pilotmoon/PopClip-Extensions/blob/master/tsconfig.json)
### PopClip types package
I have published the NPM package
[`@popclip/types`](https://www.npmjs.com/package/@popclip/types), a TypeScript
type definitions package to assist in developing extensions. This will enable
autocomplete and type-checking in TypeScript-aware editors.
Use an NPM-compatible JavaScript package manager to install both `typescript`
itself and the types package in the directory where you are writing your
extension code:
```bash
npm install -D typescript @popclip/types
```
And then, in your `tsconfig.json` file, add an explicit reference to the types:
```json
{
"compilerOptions": {
"types": ["@popclip/types"]
}
}
```
Once this is done, you should get autocomplete and type-checking in your editor
and TypeScript's `tsc` will check your code for type errors:
```bash
npx tsc --noEmit
```
## Test Harness
PopClip has a command-line mode that loads a JavaScript file into
the PopClip environment and runs it. Optionally, if the file is a module, it can
then call one of the module's exported functions.
It is useful for running tests of your code in PopClip's environment, with the
same libraries, globals etc.
The test harness is activated by calling PopClip's executable (inside the
PopClip.app package) with the parameter `run` followed by the filename to load
and an optional function name to call. For example:
```bash
/Applications/PopClip.app/Contents/MacOS/PopClip run myfile.js myfunc
```
If a function name is supplied, it will be called with no parameters. If the
function is an `async` function or returns a `Promise`, the test harness will
wait for the function to complete before exiting. If the function completes
successfully, the return value of the function is printed to the console.
The shell exit status will be:
- 0 if the scipt loads and runs without error and the called function (if any)
completes normally;
- 1 if an error occurs (e.g. file not found, syntax error), or if the function
throws an exception.
Some notes:
- Scripts can output strings with the global `print()` function (not
`console.log()`).
- When running in the test harness, the `popclip` object's properties will
return blank data. Its methods can be called but some will not have any
effect.
- Scripts running in the test harness always have the network access
entitlement.
- The test harness is a somewhat experimental feature at present. Please reach
out to me if something does not seem to work as expected.
### Example
'foo.ts':
```typescript
print("file loading now");
function sayHi(x: string) {
print(`hello ${x}`);
}
export async function test() {
sayHi("there");
await sleep(500);
sayHi("again");
return "that's all folks";
}
```
Test harness output:

---
# Calling external scripts
> Source: https://www.popclip.app/dev/external-scripts.md
JavaScript code in PopClip can call out to shell scripts and AppleScript.
This is often the easiest way to use a command-line tool or automate another
app from within a [JavaScript action](https://www.popclip.app/dev/js-actions.md) or
[module extension](https://www.popclip.app/dev/js-modules.md), without needing a whole
[Shell Script](https://www.popclip.app/dev/shell-script-actions.md) or
[AppleScript](https://www.popclip.app/dev/applescript-actions.md) action.
All the facilities on this page require the
`script` [entitlement](https://www.popclip.app/dev/top-level-properties.md) in the extension's config, and may only
be used during the action phase β that is, from an action's code, not at load
or population time. There is no timeout: a run ends when the script exits, or
when the user cancels the action by clicking the spinner, which kills the
script.
## The `$` shell tag
The global [`$`](https://www.popclip.app/dev/api/interfaces/ShellTag.html) is the convenient way to
run a shell command. Write the command as a
[template literal](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals),
and await the result. For example, here is an extension to paste the Mac's
local IP address β information that JavaScript alone cannot reach:
```javascript
// #popclip
// name: Paste IP Address
// icon: IP
// entitlements: [script]
// requirements: [paste]
const ip = await $`ipconfig getifaddr en0 || ipconfig getifaddr en1`;
popclip.pasteText(`${ip}`);
```
The command runs with `/bin/zsh` in strict mode (`set -euo pipefail`). The
result converts to a string as the command's output with trailing newlines
stripped β the same rule as shell command substitution.
The tag's key property is that **interpolated values are shell-escaped**:
each `${...}` value arrives in the command as a literal word, so selected
text, file paths β anything β can never become shell syntax. Don't put your
own quotes around an interpolation; it arrives already quoted.
```javascript
// #popclip
// name: Speak Definition
// icon: symbol:character.book.closed
// entitlements: [script]
const word = popclip.input.text.trim();
const definition = util.getDictionaryDefinition(word) ?? "no definition";
await $`say ${definition}`;
```
The template text itself is used **raw** β everything between the backticks
goes to the shell exactly as written, so backslashes survive and JavaScript
escape sequences are not interpreted. Write shell variables as `$VAR` rather
than `${VAR}` (which JavaScript would claim).
A result can be interpolated into a later command, splicing in as its output
text β so one command's output feeds the next. Arrays splice in as separate
words.
Calling `$` with an options object returns a configured tag, which can be
kept and reused β even for other interpreters:
```javascript
// #popclip
// name: Python Upper
// entitlements: [script]
const py = $({ interpreter: "python3", quote: JSON.stringify });
const result = await py`print(${popclip.input.text}.upper())`;
popclip.showText(`${result}`);
```
See [ShellTag](https://www.popclip.app/dev/api/interfaces/ShellTag.html) for the full options.
## Shell script functions
For more control than the `$` tag β or to run a script file shipped in the
extension package β use
[`popclip.runShellScript()`](https://www.popclip.app/dev/api/interfaces/PopClip.html#runshellscript)
and
[`popclip.runShellScriptFile()`](https://www.popclip.app/dev/api/interfaces/PopClip.html#runshellscriptfile).
`runShellScript()` takes the script as source text, and any `interpreter` β
not just shells:
```javascript
const { stdout } = await popclip.runShellScript("print(2 ** 100)", {
interpreter: "python3",
});
```
`runShellScriptFile()` takes the package-relative path of a script file, and
also accepts `stdin` and positional `arguments`:
```javascript
const { stdout } = await popclip.runShellScriptFile("scripts/convert.sh", {
arguments: [popclip.input.text],
stdin: popclip.input.html,
});
```
Points to note:
- By default the interpreter is executed directly, with no shell involved and
a minimal, deterministic environment; the `shellMode` option can route the
run through the user's shell instead (as a login or non-login shell), the
same as the classic Shell Script action's
[`shellMode`](https://www.popclip.app/dev/shell-script-actions.md#shell-mode) key.
- To pass data into the script, use the `env`, `stdin` or `arguments` options
β they need no escaping. Avoid composing data into the script source
itself; that is the `$` tag's job, since it escapes its interpolations.
- A successful run resolves with `{ stdout, stderr, status }`. A script that
exits nonzero (or is killed by a signal) rejects the promise, with the same
fields carried on the error.
See [ShellScriptOptions](https://www.popclip.app/dev/api/interfaces/ShellScriptOptions.html) for the
full options.
## AppleScript functions
To run AppleScript, use
[`popclip.runAppleScript()`](https://www.popclip.app/dev/api/interfaces/PopClip.html#runapplescript)
(source text) and
[`popclip.runAppleScriptFile()`](https://www.popclip.app/dev/api/interfaces/PopClip.html#runapplescriptfile)
(a package-relative `.applescript` or `.scpt` file).
Rather than composing values into the script source, name a `handler`
(subroutine) in the script and pass values as `parameters` β no escaping
worries:
```javascript
// #popclip
// name: Add Reminder
// icon: symbol:list.bullet.clipboard
// entitlements: [script]
const script = `
on addReminder(theName)
tell application id "com.apple.reminders"
make new reminder with properties {name:theName}
end tell
end addReminder`;
await popclip.runAppleScript(script, {
handler: "addReminder",
parameters: [popclip.input.text],
permissions: ["reminders"],
});
```
The `permissions` option names system permissions the script needs, so
PopClip can show the consent prompt and, if access is denied, direct the user
to the right System Settings pane.
The promise resolves with the script's return value. A script that errors
rejects the promise with an error carrying the AppleScript error number as
its `errorNumber` property.
See [AppleScriptOptions](https://www.popclip.app/dev/api/interfaces/AppleScriptOptions.html) for the
full options.
## Related functions
Two neighboring functions need no `script` entitlement:
- [`popclip.runShortcut()`](https://www.popclip.app/dev/api/interfaces/PopClip.html#runshortcut)
runs a macOS Shortcut by name.
- [`popclip.performService()`](https://www.popclip.app/dev/api/interfaces/PopClip.html#performservice)
performs a macOS Service by name.
---
# Open URL actions
> Source: https://www.popclip.app/dev/url-actions.md
In an Open URL action, PopClip will ask macOS to open a URL generated from a template that you provide.
If the URL scheme is `http:` or `https:` and the current app is a [known browser](https://www.popclip.app/kb/browsers.md), PopClip will ask
the current app to open the URL.
In all other cases, PopClip will ask macOS to open the URL in the default app app for its URL scheme.
**Tip: Opening a URL from JavaScript**
You can also use
[`popclip.openUrl()`](https://www.popclip.app/dev/api/interfaces/PopClip.html#openurl)
within a [JavaScript action](https://www.popclip.app/dev/js-actions.md).
## Properties
An Open URL action is defined by the presence of a `url` field, plus additional
optional fields, as follows:
| Key | Type | Description |
| -------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `url` | String | The URL to open when the user clicks the action. Use either `{popclip text}` or `***` as placeholder for the selected text. |
| `cleanQuery` | Boolean (Optional) | If `true`, newlines and tabs in the text will be replaced with a space, and consecutive spaces will be collapsed to a single space. Default is `false`. |
| `spacesAsPlus` | Boolean (Optional) | If `true`, spaces in the inserted text are encoded as `+` instead of `%20`. Some search engines (for example Amazon) expect this format. Default is `false`. |
**Note: Verbatim search with the Option key**
If the user holds Option (β₯) when invoking the action, PopClip wraps the
inserted text in double quotes, so that search engines treat it as an
exact-phrase search.
_The `alternateUrl` property supported by earlier versions of PopClip was
removed in PopClip 2026.7. If present in a config, it is now ignored._
## Input and output
The selected plain text will be inserted into the URL, replacing the
`{popclip text}` or `***` placeholder if present. PopClip will always trim leading and
trailing whitespace and newlines, and URL-encode the text. Optionally, PopClip
will perform further whitespace cleanup with the `cleanQuery` flag.
Option parameters can be inserted in the URL, in the same format as for
AppleScript actions. See [example](#use-of-option-parameter).
URL actions never return any output.
**Tip: Advanced behaviours**
If a plain Open URL action isn't enough, use a [JavaScript action](https://www.popclip.app/dev/js-actions.md). There are two functions:
- [`popclip.openUrl()`](https://www.popclip.app/dev/api/interfaces/PopClip.html#openurl) opens a URL you have built yourself.
- [`popclip.openTemplateUrl()`](https://www.popclip.app/dev/api/interfaces/PopClip.html#opentemplateurl) takes the same `***` placeholder as the `url` property and does the encoding for you.
```javascript
await popclip.openTemplateUrl(
"https://example.com/?q=***",
popclip.input.text,
{
app: "com.google.Chrome",
},
);
```
Both take the same options, and both return a promise that resolves once the URL has been handed to the browser.
## Examples
### Simple web search
The following snippet defines an extension with a single URL action that opens a
search for the selected text on the movie review site, Rotten Tomatoes:
```yaml
#popclip extension to search Rotten Tomatoes
name: Rotten Tomatoes
icon: iconify:simple-icons:rottentomatoes
url: https://www.rottentomatoes.com/search?search=***
```
### Custom URL scheme
The following snippet opens a custom URL scheme, in this case `maps:` for Apple
Maps:
```yaml
#popclip custom URL scheme example, Apple Maps
name: Maps
icon: iconify:material-symbols:map-outline
url: maps://?q={popclip text}
```
### Use of option parameter
The following snippet opens a Wiktionary search page, with the site domain
specified as an option parameter:
```yaml
#popclip Wiktionary search with subdomain option
name: Wiktionary
icon: iconify:ooui:logo-wiktionary
url: https://{popclip option subdomain}.wiktionary.org/wiki/{popclip text}
options:
- type: string
identifier: subdomain
label: Site subdomain
defaultValue: en
```
```json
#popclip Wiktionary search with subdomain option
{
"name": "Wiktionary",
"icon": "iconify:ooui:logo-wiktionary",
"url": "https://{popclip option subdomain}.wiktionary.org/wiki/{popclip text}",
"options": [
{
"type": "string",
"identifier": "subdomain",
"label": "Site subdomain",
"defaultValue": "en"
}
]
}
```
---
# Key Press actions
> Source: https://www.popclip.app/dev/key-press-actions.md
In a Key Press action, PopClip will simulate a key press, or sequence of
presses, as if it was performed by the user.
**Tip: Pressing a key from JavaScript**
You can also use
[`popclip.pressKey()`](https://www.popclip.app/dev/api/interfaces/PopClip.html#presskey) β or
[`popclip.pressKeys()`](https://www.popclip.app/dev/api/interfaces/PopClip.html#presskeys), for a
sequence of combos with optional waits β
within a [JavaScript action](https://www.popclip.app/dev/js-actions.md).
## Properties
A Key Press action is defined by the presence of a `keyCombo` or `keyCombos`
field, as follows:
| Key | Type | Description |
| ---------------- | ------ | --------------------------------------------------------------------------------------------------------------- |
| `keyCombo` | String | The key combination to press, as defined in [String format](#string-format). |
| `keyCombos` | Array | Instead of a single key combo, you can supply array of them. PopClip will press all the key combos in sequence. |
| `keyComboTarget` | String | Where to post the presses: `session` (the default), `app` or `hid`. See [Target](#target). |
## Target
The `keyComboTarget` field says where PopClip posts the key events:
| Value | Description |
| --------- | -------------------------------------------------------------------------------------- |
| `session` | To the session event tap, `kCGSessionEventTap`. This is the default. |
| `app` | To the process of the application the action is acting on, using `CGEventPostToPid()`. |
| `hid` | To the HID event tap, `kCGHIDEventTap`. |
Use the `app` target if the key combination is intended only for the target app β an example would be
a formatting extension that presses βB, βI and βU. Keep the default `session` if the key combination is
intended to activate a global shortcut. Posting to `hid` is not normally needed, but it may work in
some cases where posting to `session` fails.
```yaml
#popclip
name: Bold
keyCombo: command b
keyComboTarget: app
stayVisible: true
```
## Input and output
Key Press actions do not receive any input or return any output.
## String format
The string format is a convenient human-readable format that can specify a key
and modifiers. For example:
- `command b` or `command B`- _Hold command, and press 'b' key_
- `option shift .` - _Hold option and shift, and press the dot key_
- `command space` - _Hold command, and press space bar_
- `f1` - _The F1 key on its own with no modifiers_
- `option numpad /` - _Hold option, press '/' key on numeric keypad_
- `0x74` - _0x74 is the hex numeric code for the Page Up key_
The format is: ` `, where:
- `` is optional, and can be any combination of:
| Modifier | Keyword |
| -------------- | ----------------- |
| Command (β) | `command`, `cmd` |
| Option (β₯) | `option`, `opt` |
| Control (β) | `control`, `ctrl` |
| Shift (β§) | `shift` |
| Numeric Keypad | `numpad` |
- `` is the key to press, either:
- A character, as printed on the key. Not case-sensitive. Examples: `A`, `a`,
`;`, `9`.
- A key name. The following are supported: `return`, `space`, `delete`,
`escape`, `left`, `right`, `down`, `up`, and `f1` ... `f20`.
- A hexadecimal key code, starting with `0x`. See list of codes below.
**List of virtual key codes (from Apple's `Events.h`)**
```c
/*
* Summary:
* Virtual keycodes
*
* Discussion:
* These constants are the virtual keycodes defined originally in
* Inside Mac Volume V, pg. V-191. They identify physical keys on a
* keyboard. Those constants with "ANSI" in the name are labeled
* according to the key position on an ANSI-standard US keyboard.
* For example, kVK_ANSI_A indicates the virtual keycode for the key
* with the letter 'A' in the US keyboard layout. Other keyboard
* layouts may have the 'A' key label on a different physical key;
* in this case, pressing 'A' will generate a different virtual
* keycode.
*/
enum {
kVK_ANSI_A = 0x00,
kVK_ANSI_S = 0x01,
kVK_ANSI_D = 0x02,
kVK_ANSI_F = 0x03,
kVK_ANSI_H = 0x04,
kVK_ANSI_G = 0x05,
kVK_ANSI_Z = 0x06,
kVK_ANSI_X = 0x07,
kVK_ANSI_C = 0x08,
kVK_ANSI_V = 0x09,
kVK_ANSI_B = 0x0B,
kVK_ANSI_Q = 0x0C,
kVK_ANSI_W = 0x0D,
kVK_ANSI_E = 0x0E,
kVK_ANSI_R = 0x0F,
kVK_ANSI_Y = 0x10,
kVK_ANSI_T = 0x11,
kVK_ANSI_1 = 0x12,
kVK_ANSI_2 = 0x13,
kVK_ANSI_3 = 0x14,
kVK_ANSI_4 = 0x15,
kVK_ANSI_6 = 0x16,
kVK_ANSI_5 = 0x17,
kVK_ANSI_Equal = 0x18,
kVK_ANSI_9 = 0x19,
kVK_ANSI_7 = 0x1A,
kVK_ANSI_Minus = 0x1B,
kVK_ANSI_8 = 0x1C,
kVK_ANSI_0 = 0x1D,
kVK_ANSI_RightBracket = 0x1E,
kVK_ANSI_O = 0x1F,
kVK_ANSI_U = 0x20,
kVK_ANSI_LeftBracket = 0x21,
kVK_ANSI_I = 0x22,
kVK_ANSI_P = 0x23,
kVK_ANSI_L = 0x25,
kVK_ANSI_J = 0x26,
kVK_ANSI_Quote = 0x27,
kVK_ANSI_K = 0x28,
kVK_ANSI_Semicolon = 0x29,
kVK_ANSI_Backslash = 0x2A,
kVK_ANSI_Comma = 0x2B,
kVK_ANSI_Slash = 0x2C,
kVK_ANSI_N = 0x2D,
kVK_ANSI_M = 0x2E,
kVK_ANSI_Period = 0x2F,
kVK_ANSI_Grave = 0x32,
kVK_ANSI_KeypadDecimal = 0x41,
kVK_ANSI_KeypadMultiply = 0x43,
kVK_ANSI_KeypadPlus = 0x45,
kVK_ANSI_KeypadClear = 0x47,
kVK_ANSI_KeypadDivide = 0x4B,
kVK_ANSI_KeypadEnter = 0x4C,
kVK_ANSI_KeypadMinus = 0x4E,
kVK_ANSI_KeypadEquals = 0x51,
kVK_ANSI_Keypad0 = 0x52,
kVK_ANSI_Keypad1 = 0x53,
kVK_ANSI_Keypad2 = 0x54,
kVK_ANSI_Keypad3 = 0x55,
kVK_ANSI_Keypad4 = 0x56,
kVK_ANSI_Keypad5 = 0x57,
kVK_ANSI_Keypad6 = 0x58,
kVK_ANSI_Keypad7 = 0x59,
kVK_ANSI_Keypad8 = 0x5B,
kVK_ANSI_Keypad9 = 0x5C
};
/* keycodes for keys that are independent of keyboard layout*/
enum {
kVK_Return = 0x24,
kVK_Tab = 0x30,
kVK_Space = 0x31,
kVK_Delete = 0x33,
kVK_Escape = 0x35,
kVK_Command = 0x37,
kVK_Shift = 0x38,
kVK_CapsLock = 0x39,
kVK_Option = 0x3A,
kVK_Control = 0x3B,
kVK_RightCommand = 0x36,
kVK_RightShift = 0x3C,
kVK_RightOption = 0x3D,
kVK_RightControl = 0x3E,
kVK_Function = 0x3F,
kVK_F17 = 0x40,
kVK_VolumeUp = 0x48,
kVK_VolumeDown = 0x49,
kVK_Mute = 0x4A,
kVK_F18 = 0x4F,
kVK_F19 = 0x50,
kVK_F20 = 0x5A,
kVK_F5 = 0x60,
kVK_F6 = 0x61,
kVK_F7 = 0x62,
kVK_F3 = 0x63,
kVK_F8 = 0x64,
kVK_F9 = 0x65,
kVK_F11 = 0x67,
kVK_F13 = 0x69,
kVK_F16 = 0x6A,
kVK_F14 = 0x6B,
kVK_F10 = 0x6D,
kVK_F12 = 0x6F,
kVK_F15 = 0x71,
kVK_Help = 0x72,
kVK_Home = 0x73,
kVK_PageUp = 0x74,
kVK_ForwardDelete = 0x75,
kVK_F4 = 0x76,
kVK_End = 0x77,
kVK_F2 = 0x78,
kVK_PageDown = 0x79,
kVK_F1 = 0x7A,
kVK_LeftArrow = 0x7B,
kVK_RightArrow = 0x7C,
kVK_DownArrow = 0x7D,
kVK_UpArrow = 0x7E
};
/* ISO keyboards only*/
enum {
kVK_ISO_Section = 0x0A
};
/* JIS keyboards only*/
enum {
kVK_JIS_Yen = 0x5D,
kVK_JIS_Underscore = 0x5E,
kVK_JIS_KeypadComma = 0x5F,
kVK_JIS_Eisu = 0x66,
kVK_JIS_Kana = 0x68
};
```
## Wait between key presses
By default, PopClip does not wait between key presses. To add a delay, put
`wait ` in the `keyCombos` array. For example, `wait 100` will
wait 100 milliseconds. (See example below.)
## Examples
A simple key press to make text bold in most editors:
```yaml
#popclip
name: Bold
icon: B
keyCombo: command b
```
Pressing a sequence of keys:
```yaml
#popclip
name: Paste and Enter
icon: square monospaced β΅
requirements: [paste] # only show action when there is something to paste
keyCombos:
- command v
- return
```
Pressing a sequence of keys, with a wait included:
```yaml
#popclip
name: Spotlight
before: copy # puts selected text on the clipboard
keyCombos:
- command space
- wait 50 # waits 50 milliseconds
- command v
```
A "Superscript" extension, supporting a couple of different apps:
```yaml
#popclip snippet to change to superscript in MS Word and Pages
name: Superscript
icon: iconify:tabler:superscript
actions:
- requiredApps: [com.microsoft.Word]
keyCombo: command shift =
- requiredApps: [com.apple.iWork.Pages]
keyCombo: command control +
```
---
# Service actions
> Source: https://www.popclip.app/dev/service-actions.md
In a Service action, PopClip will invoke a macOS [Service](https://support.apple.com/en-gb/guide/mac-help/mchlp1012/mac) by name.
**Tip: Calling a macOS Service from JavaScript**
You can also call
[`popclip.performService()`](https://www.popclip.app/dev/api/interfaces/PopClip.html#performservice) from a
[JavaScript action](https://www.popclip.app/dev/js-actions.md).
```javascript
// #popclip service js example
// name: Dated Sticky
const today = new Date().toLocaleDateString();
const note = `${popclip.input.text}\n\nClipped ${today}`;
await popclip.performService("Make Sticky", note);
```
## Properties
A service action is defined by the presence of a `serviceName` field, as follows:
| Key | Type | Description |
| ------------- | ------ | -------------------------------------- |
| `serviceName` | String | The name of the macOS service to call. |
**Tip: Service names**
The service name is usually exactly as shown in the Services menu, for example `Add to Deliveries`. However, in some cases you may need to look into the Info.plist of the application to find the name defined in there under `NSServices` β `NSMenuItem`. An example of this is the `Make New Sticky Note` service which must be called as `Make Sticky`.
## Input and output
The selected plain text will be sent as input to the service. If `captureHtml` is set to `true`, then the HTML version of the selected text will also be sent as input to the service.
Service actions never return any output.
## Examples
Simple snippet calling a service:
```yaml
#popclip
name: "Deliveries"
serviceName: "Add to Deliveries"
```
The following defines an extension that makes a new Stickies note from the
selected text, using the `Make Sticky` service mentioned above. (This is the
same action as the published
[Make Sticky](https://github.com/pilotmoon/PopClip-Extensions/tree/master/source/MakeSticky.popclipext)
extension.)
```yaml
#popclip
name: Make Sticky
icon: symbol:note.text
serviceName: Make Sticky
```
---
# Shortcut actions
> Source: https://www.popclip.app/dev/shortcut-actions.md
In a Shortcut action, PopClip will invoke a macOS [Shortcut](https://support.apple.com/en-gb/guide/shortcuts-mac/apdf22b0444c/mac) by name.
An extension can only invoke shortcuts the user has built or installed themselves.
**Tip: Running a shortcut from JavaScript**
You can also use
[`popclip.runShortcut()`](https://www.popclip.app/dev/api/interfaces/PopClip.html#runshortcut)
within a [JavaScript action](https://www.popclip.app/dev/js-actions.md).
```javascript
// #popclip shortcut js example
// name: Summarize
const summary = await popclip.runShortcut("Summarize Text", {
input: popclip.input.text,
});
popclip.pasteText(`Summary:\n${summary}\n\nFull text:\n${popclip.input.text}`);
```
## Properties
A shortcut action is defined by the presence of a `shortcutName` field, as follows:
| Key | Type | Description |
| -------------- | ------ | ---------------------------------------------------------------------------------------------- |
| `shortcutName` | String | The name of the macOS Shortcut to call. This must exactly match its name in the Shortcuts app. |
## Input and output
The selected plain text will be sent as input to the shortcut. Any plain text returned by the shortcut will be available to the `after` step.
## Example
The following example snippet defines an extension with a single shortcut action that calls a shortcut called `My Shortcut Name`:
```yaml
#popclip shortcut example
name: Run My Shortcut
shortcutName: My Shortcut Name
```
---
# AppleScript actions
> Source: https://www.popclip.app/dev/applescript-actions.md
A classic AppleScript action runs AppleScript code. AppleScript's strength is in
automation, since it can be used to control other apps.
**Tip: Running AppleScript from JavaScript**
To run just a little bit of AppleScript as part of a larger extension, call
[`popclip.runAppleScript()`](https://www.popclip.app/dev/api/interfaces/PopClip.html#runapplescript) or
[`runAppleScriptFile()`](https://www.popclip.app/dev/api/interfaces/PopClip.html#runapplescriptfile) from a
[JavaScript action](https://www.popclip.app/dev/js-actions.md) with the `script` entitlement declared.
See [Calling external scripts](https://www.popclip.app/dev/external-scripts.md) for the full story.
## Properties
An AppleScript action is defined by a code snippet whose config header uses
the `--` comment prefix. Alternatively, a config snippet may define an
`appleScript` or `appleScriptFile` field, with optional `appleScriptCall`
field, as follows:
| Key | Type | Description |
| ----------------- | --------------------- | ------------------------------------------------------------------- |
| `appleScript` | String | A text string to interpret directly as AppleScript source. |
| `appleScriptFile` | String | Path to an `.applescript` or `.scpt` file in the package directory. |
| `appleScriptCall` | Dictionary (optional) | A named handler to call. |
A code snippet is equivalent to a config snippet whose `appleScriptFile` is
the snippet itself.
### The `appleScriptCall` dictionary
The `appleScriptCall` dictionary lets you call a named handler within the
script.
| Key | Type | Description |
| ------------ | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `handler` | String | Name of a handler within the script to call. |
| `parameters` | Array (optional) | Array of strings specifying names of values to pass as parameters to the handler, as defined in [Script variables](https://www.popclip.app/dev/script-variables.md). The number and order of parameters must match exactly what the handler expects to receive. Omit or leave empty if there are no parameters. |
### AppleScript format
PopClip can execute an AppleScript supplied either as a **plain text script**
(`.applescript` file), or as a **compiled script** (`.scpt` file, created in the
Script Editor app). The ways you can pass values to the script differ depending
on the script type (see examples below).
The script may optionally return a string (e.g. `return "foo"`), and act on it
with an `after` key. For returning errors, see
[Indicating Errors](#indicating-errors).
## Input and output
Within a plain text script, use `{popclip text}` as a placeholder for the
selected text. PopClip will replace the placeholder with the actual text before
executing the script. Other placeholders are also available; see
[Script variables](https://www.popclip.app/dev/script-variables.md).
Within a compiled script (`.scpt`), you cannot use placeholder strings. Instead,
you need to put your code in a handler and pass values to it. See
[Compiled `.scpt` file](#compiled-scpt-file) example.
Any text returned by the script will be made available to the `after` step.
## Indicating errors
AppleScripts should indicate success by exiting normally, and should indicate
failure by signalling an error. On error, PopClip will display the shaking-'X'.
To indicate an error with the user's settings, and pop up the extension's
options UI, signal the specific error code `502`. For example:
```applescript
error "Missing foo parameter" number 502
```
## Examples
### Snippet examples
Scripting another app:
```applescript
-- #popclip
-- name: LaunchBar
-- icon: LB
tell application "LaunchBar"
set selection to "{popclip text}"
end tell
```
Returning text from the script:
```applescript
-- #popclip
-- name: AppleScript HTML
-- captureHtml: true
-- after: show-result
return "Your HTML: " & "{popclip html}"
```
### Package examples
#### Plain text `.applescript` file
**TextEditClip.applescript**
```applescript
tell application "TextEdit"
activate
set theDocument to make new document
set text of theDocument to ("{popclip text} - Clipped from {popclip browser url}")
end tell
```
**Config.json**
```json
{
"name": "TextEdit Clip",
"appleScriptFile": "TextEditClip.applescript"
}
```
#### Compiled `.scpt` file
When using a `.scpt` file, parameters must be passed by calling a handler.
**TextEditClip.scpt**
```applescript
on newDocument(theText, theUrl) --this is a handler
tell application "TextEdit"
activate
set theDocument to make new document
set text of theDocument to (theText & " - Clipped from " & theUrl)
end tell
end newDocument
```
**Config.json**
```json
{
"name": "TextEdit Clip",
"appleScriptFile": "TextEditClip.scpt",
"appleScriptCall": {
"handler": "newDocument",
"parameters": ["text", "browser url"]
}
}
```
## Using JXA Scripts
Note that when using a compiled script, these can be be JavaScript for
Automation (JXA) scripts instead of AppleScripts. Everything works the same
except handlers correspond to top level JXA functions. JXA cannot be used in
plain text scripts.
An example of an extension using a JXA script is
[TaskPaper](https://github.com/pilotmoon/PopClip-Extensions/tree/master/source/TaskPaper.popclipext).
---
# Shell Script actions
> Source: https://www.popclip.app/dev/shell-script-actions.md
A classic Shell Script action runs a shell script, either directly or from a file. The
script can be written in any language that can be executed from the command
line, such as Zsh, Python, Ruby, Perl, etc.
**Tip: Running a shell script from JavaScript**
JavaScript actions can call shell scripts using the `$` syntax:
```javascript
// #popclip shell js example
// name: Print in Uppercase
// entitlements: [script]
const printMe = popclip.input.text.trim().toUpperCase();
const { stdout } = await $`lp <<< ${printMe}`;
// e.g. "request id is Office_Printer-294 (1 file(s))"
const requestId = stdout.match(/request id is (\S+)/)?.[1] ?? "unknown";
popclip.showText(`Printing: ${requestId}`);
```
See [Calling external scripts](https://www.popclip.app/dev/external-scripts.md) for the full story.
**Warning: Submitting to the directory**
Extensions submitted to the [Extensions Directory](https://www.popclip.app/extensions/) should use
JavaScript actions in preference to Shell Script actions. A submission with a
Shell Script action must include a
[`shellScriptRationale`](https://www.popclip.app/extensions/submit.md#shell-script-policy) in its Config.
## Properties
A Shell Script action is defined by a code snippet that specifies an
`interpreter` in its config header, or starts with a `#!` line.
Alternatively, a config snippet may define a `shellScript` or
`shellScriptFile` field, as follows:
| Key | Type | Description |
| ----------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `shellScript` | String | A string to be run as a shell script. The string will be passed via standard input to the specified `interpreter`, invoked without arguments. |
| `shellScriptFile` | String | The name of a file in the extension's package directory. See [Shell script file execution](#shell-script-file-execution) for more details. |
| `interpreter` | String (optional) | Specify the interpreter to use for `shellScript` or `shellScriptFile`. You can specify a bare executable name, for example `ruby`, and PopClip will look for it in the `PATH` of the user's default shell. Alternatively, you can specify an absolute path such as `/bin/zsh`. |
| `stdin` | String (optional) | For script specified as `shellScriptFile` only. Set the name of a [script variable](https://www.popclip.app/dev/script-variables.md) to pass via standard input (stdin). If omitted, no standard input is provided to the script. |
| `shellMode` | String (optional) | How the script is executed: `login` (the default), `nonlogin` or `none`. See [Shell mode](#shell-mode). |
### Shell script file execution
The `shellScriptFile` will be executed as follows:
- If an `interpreter` is specified, then PopClip will call this interpreter with
the script file path as argument.
- Otherwise, if the script file has executable permissions set (with `chmod +x`)
and the first line of the file starts with `#!`, then PopClip will execute the
file directly.
- Otherwise, if the extension has a `popclipVersion` and it is set to a value
less than `4035`, or if the script file name ends with `.sh`, the script will
be executed with `/bin/sh`. (This behaviour is for backward compatibility with
existing extensions.)
- If none of the above conditions are met, the extension will fail to load
because no interpreter has been specified.
The current working directory will be set to the package directory.
### Shell mode
The `shellMode` field controls how the script run is executed:
- `login` (the default): via the user's default shell as a login shell (`-l`), so the
script sees the user's usual `PATH` and profile environment.
- `nonlogin`: via the user's shell without `-l` β for environments configured in
`.zshenv` alone, without profile side effects.
- `none`: no shell at all β the interpreter (or the executable script file itself) is
executed directly, with a minimal environment (`PATH=/usr/bin:/bin:/usr/sbin:/sbin`
plus the `POPCLIP_*` variables). Fastest and most predictable.
## Input and output
Within the script, access the selected text with the shell variable
`POPCLIP_TEXT`. Many other variables are also available, as listed in
[Script variables](https://www.popclip.app/dev/script-variables.md).
Optionally, the script may read from standard input (stdin). If the `stdin`
field is set, the script will receive the contents of the specified variable via
stdin. For example, if `stdin` is set to `text`, the script will receive the
contents of the `POPCLIP_TEXT` variable via stdin.
Any text returned by the script via standard output (stdout) will be available
to the `after` step.
## Indicating errors
Shell scripts should indicate success with an exit code of `0`, and should
indicate failure with a non-zero exit code. On failure, PopClip will display the
shaking-'X'.
Scripts may signal that there is an error with the user's settings with specific
error code `2`. In this case, PopClip will pop up the extension settings UI.
## Examples
### Package example
The
[Say](https://github.com/pilotmoon/PopClip-Extensions/tree/master/source/Say.popclipext)
extension demonstrates a packaged shell script extension.
### Snippet examples
**Note: About these examples**
The examples are given as [code snippets](https://www.popclip.app/dev/snippets.md#inverted-syntax).
Examples of passing the selected text to the `say` command to be spoken aloud:
**Using shell variable**
```zsh
#!/bin/zsh
# #popclip
# name: Say (variable)
say $POPCLIP_TEXT
```
**Using stdin**
```zsh
#!/bin/zsh
# #popclip
# name: Say (stdin)
# stdin: text
say
```
**With option**
```zsh
#!/bin/zsh
# #popclip
# name: Say (option)
# stdin: text
# options:
# - { identifier: voice, type: string, label: Voice, defaultValue: Daniel }
say -v $POPCLIP_OPTION_VOICE
```
Some examples of returning a string back to PopClip via stdout, in different
languages:
```zsh
#!/bin/zsh
# #popclip
# name: Helloworld in zsh
# after: show-result
echo -n "Hello, ${POPCLIP_TEXT}!" # `-n` for no newline at end
```
```python
#!/usr/bin/env python3
# #popclip
# name: Helloworld in python
# after: show-result
import os
print('Hello, ' + os.environ['POPCLIP_TEXT'] + '!', end='')
# `end=''` for no newline at end
```
```ruby
#!/usr/bin/env ruby
# #popclip
# name: Helloworld in ruby
# after: show-result
print 'Hello, ' + ENV['POPCLIP_TEXT'] + '!'
```
```perl
#!/usr/bin/env perl
# #popclip
# name: Helloworld in perl
# after: show-result
print "Hello, $ENV{'POPCLIP_TEXT'}!\n";
```
```swift
#!/usr/bin/env swift
// #popclip
// name: Helloworld in swift
// after: show-result
import Foundation
let text = ProcessInfo.processInfo.environment["POPCLIP_TEXT"]!
print("Hello, \(text)!")
```
A more substantial example:
**Example snippet: Download an Iconify icon as SVG**
```zsh
#!/bin/zsh
# Download an Iconify icon to Downloads folder as SVG
# Example input: simple-icons:vivaldi
#
# #popclip
# popclip version: 4050
# name: GetIcon
# regex: ([a-z0-9]+(?:-[a-z0-9]+)*):([a-z0-9]+(?:-[a-z0-9]+)*)
# stdin: text
# after: copy-result
#
set -e # exit on errors
eval "$(/opt/homebrew/bin/brew shellenv)"
log() { # print named params to stderr
for name in $*; do
echo ${(r:8:)name} ${(P)name} >>/dev/stderr
done
}
# get input from stdin
input=$(cat); log input
# parse the input
parts=(${(s(:))input}) # split on :
prefix=$parts[1]
icon=$parts[2]
url="https://api.iconify.design/${prefix}.json?icons=${icon}"; log url
# get svg string (`brew install httpie`, `brew install jq`)
svg=$(http get $url | jq -r ".icons.\"$icon\".body")
# wrap in svg tag
svg="${svg} "
# save to file
svg_name="${prefix}-${icon}.svg"
out_file="${HOME}/Downloads/${svg_name}"; log out_file
echo -n $svg > $out_file
# return the file name
echo -n $svg_name
```
## Script development tips
While developing a script, you can test it from the command line by setting any
required variables in the call. For example:
```zsh
POPCLIP_TEXT="my test text" POPCLIP_OPTION_FOO="foo" ./myscript
```
Or export them before calling the script:
```zsh
export POPCLIP_TEXT="my test text"
export POPCLIP_OPTION_FOO="foo"
./myscript
```
When testing a script that uses the `stdin` field, you can pipe in a string from
the command line:
```zsh
echo "my test text" | ./myscript
```
---
# Script variables
> Source: https://www.popclip.app/dev/script-variables.md
When calling a shell script or AppleScript from a classic PopClip script action (not from JavaScript),
the script receives a set of variables that describe the input text and the context in which the action was triggered.
**Note: Variables in JavaScript**
In JavaScript, variables are on the [`popclip` global object](https://www.popclip.app/dev/js-environment.md).
## Shell Script variables
All values are provided as strings. Where no value is available, it will be set
to an empty string. PopClip sets script variables named like this:
`POPCLIP_TEXT`, `POPCLIP_BROWSER_TITLE`, `POPCLIP_OPTION_FOO`, etc.
```shell
open "https://translate.google.com/?text=${POPCLIP_URLENCODED_TEXT}"
```
## AppleScript variables
Within an AppleScript, PopClip pre-processes the script to replace placeholders
with strings. Placeholders look like this: `{popclip text}`,
`{popclip browser title}`, `{popclip option foo}`, etc.
```applescript
display dialog "{popclip text}" with title "Selected in {popclip app name}"
```
## Available variables
| Shell Script | AppleScript | Description |
| ------------------------------ | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `POPCLIP_TEXT` | `{popclip text}` | The part of the selected plain text matching the specified regex or requirement. |
| `POPCLIP_FULL_TEXT` | `{popclip full text}` | The selected plain text in its entirety. |
| `POPCLIP_HTML` | `{popclip html}` | Sanitized HTML for the selection. CSS is removed, potentially unsafe tags are removed and markup is corrected. (`captureHtml` must be specified.) |
| `POPCLIP_URLENCODED_TEXT` | `{popclip urlencoded text}` | URL-encoded form of the matched text. |
| `POPCLIP_RAW_HTML` | `{popclip raw html}` | The original unsanitized HTML, if available. (`captureHtml` must be specified.) |
| `POPCLIP_MARKDOWN` | `{popclip markdown}` | A conversion of the HTML to Markdown. (`captureHtml` must be specified.) |
| `POPCLIP_URLS` | `{popclip urls}` | Newline-separated list of web URLs that PopClip detected in the selected text. |
| `POPCLIP_MODIFIER_FLAGS` | `{popclip modifier flags}` | Modifier flags for the keys held down when the extension's button was clicked in PopClip. Values are as defined in [Modifier values](#modifier-values). For example, `0` for no modifiers, or `131072` if shift is held down. |
| `POPCLIP_BUNDLE_IDENTIFIER` | `{popclip bundle identifier}` | Bundle identifier of the app the text was selected in. For example, `com.apple.Safari`. |
| `POPCLIP_APP_NAME` | `{popclip app name}` | Name of the app the text was selected in. For example, `Safari`. |
| `POPCLIP_BROWSER_TITLE` | `{popclip browser title}` | The title of the web page that the text was selected from. (Supported browsers only.) |
| `POPCLIP_BROWSER_URL` | `{popclip browser url}` | The URL of the web page that the text was selected from. (Supported browsers only.) |
| `POPCLIP_OPTION_*` | `{popclip option *}` | One such value is generated for each option specified in the extension's `options`, where `*` represents the option's `identifier`. For boolean options, the value will be a string, either `0` or `1`. |
| `POPCLIP_EXTENSION_IDENTIFIER` | `{popclip extension identifier}` | This extension's identifier. |
| `POPCLIP_ACTION_IDENTIFIER` | `{popclip action identifier}` | The identifier specified in the action's configuration, if any. |
## Modifier values
This table gives the numeric value for every possible modifier combination.
| Keys | Value |
| ---- | ------- |
| none | 0 |
| β§ | 131072 |
| β | 262144 |
| ββ§ | 393216 |
| β₯ | 524288 |
| β₯β§ | 655360 |
| ββ₯ | 786432 |
| ββ₯β§ | 917504 |
| β | 1048576 |
| β§β | 1179648 |
| ββ | 1310720 |
| ββ§β | 1441792 |
| β₯β | 1572864 |
| β₯β§β | 1703936 |
| ββ₯β | 1835008 |
| ββ₯β§β | 1966080 |
---
# Config format
> Source: https://www.popclip.app/dev/config.md
Every extension is defined by a configuration dictionary. This can be provided either
by a [snippet](https://www.popclip.app/dev/snippets.md) or a [package](https://www.popclip.app/dev/packages.md), but in each case the
underlying structure is the same.
This page describes the format itself β how keys are named and how values are
interpreted. The properties themselves are documented on the
[Top-level properties](https://www.popclip.app/dev/top-level-properties.md), [Action properties](https://www.popclip.app/dev/actions.md)
and [Options](https://www.popclip.app/dev/options.md) pages.
## Formats
PopClip supports 3 config formats: [YAML](#yaml), [JSON](#json) and [plist](#plist).
**The recommended format is YAML**. It is the
most versatile: it works as a standalone config file in a package
(`Config.yaml`), as a [config snippet](https://www.popclip.app/dev/snippets.md#config-snippets), and as
the comment header of a [code snippet](https://www.popclip.app/dev/snippets.md#inverted-syntax) or
[module](https://www.popclip.app/dev/js-modules.md) file. The examples in this documentation are YAML.
[JSON](#json) and [plist](#plist) are also supported for package config
files.
## Example
Let's look at an example `Config.yaml` for
a published extension. This is based on the
[Yoink extension](https://github.com/pilotmoon/PopClip-Extensions/tree/master/source/Yoink.popclipext):
```yaml
identifier: at.EternalStorms.Yoink.PopClipExtension
popclipVersion: 3785
name: Yoink
icon: yoink.png
app:
name: Yoink
link: https://eternalstorms.at/yoink/mac
checkInstalled: true
bundleIdentifiers:
- at.EternalStorms.Yoink
- at.EternalStorms.Yoink-setapp
- at.EternalStorms.Yoink-demo
serviceName: Add Selected Text to Yoink
captureHtml: true
description: Add the selected text to Yoink.
```
Not all of those fields are strictly needed. As we have already seen in
[Snippets](https://www.popclip.app/dev/snippets.md), we can also express a similar extension very
minimally, at the loss of some of the niceties that the fleshed-out version
provides:
```yaml
name: Yoink
serviceName: Add Selected Text to Yoink
```
**Tip: Minimal or maximal?**
In general, if you're writing an extension for your own use, you can freely omit
any fields that you don't need. But if you're preparing an extension for
publication, you should flesh out the config as much as possible, to provide the
best user experience for your extension.
## Localized strings
Fields shown as "String (Localizable)" type may be either a string or a
dictionary. If you supply a string, that string is always used. Alternatively,
you can supply a dictionary mapping language codes to strings, and PopClip will
display the string for the user's preferred language if possible, with fallback
to the `en` string, which is always required.
The following language codes are supported:
**Language codes table**
| Language Code | Language Name |
| ------------- | --------------------- |
| `en` | English |
| `en-gb` | English (UK) |
| `da` | Danish |
| `de` | German |
| `es` | Spanish |
| `fr` | French |
| `it` | Italian |
| `ja` | Japanese |
| `ko` | Korean |
| `nl` | Dutch |
| `pl` | Polish |
| `pt-br` | Portuguese (BR) |
| `ru` | Russian |
| `sk` | Slovak |
| `tr` | Turkish |
| `vi` | Vietnamese |
| `zh-hans` | Chinese (Simplified) |
| `zh-hant` | Chinese (Traditional) |
**Note: Example of localized string**
```yaml
name:
en: My Extension
fr: Mon Extension
zh-hans: ζηζ©ε±
```
## Format details
### YAML
PopClip's YAML parser expects [YAML 1.2](https://yaml.org). A package config
file written in YAML should be named `Config.yaml`. Example:
```yaml
name: Yoink
serviceName: Add Selected Text to Yoink
```
### JSON
A package config file may be written in
[JSON](https://www.json.org/json-en.html), named `Config.json`. Example:
```json
{
"name": "Yoink",
"serviceName": "Add Selected Text to Yoink"
}
```
### Plist
Plist was the original config format for PopClip extensions. It is Apple's own
[XML Property List](https://en.wikipedia.org/wiki/Property_list) format, and
many of the older extensions in the
[PopClip-Extensions repo](https://github.com/pilotmoon/PopClip-Extensions)
still use it, named `Config.plist`. It remains fully supported, but it is a
legacy format β verbose, and harder to read and edit than YAML β and I don't
recommend it for new extensions.
```xml
Name
Yoink
Service Name
Add Selected Text to Yoink
```
One plist quirk to know about: plist has no native way to represent the
`null` value of JSON and YAML. Use ` ` in a plist where these docs call for
`null`.
## Compatibility
To preserve compatibility with old extension formats,
PopClip allows properties in config files to be named in different ways.
### Key naming
PopClip is very flexible about how you name keys. These docs name every key in
camelCase, for example `keyName` β but PopClip treats `key name`, `Key Name`,
`KeyName`, `key_name`, `key-name` and `KEY_NAME` as equivalents, so configs
written in any of those styles work identically.
### Key name mapping
Some field names were different in older versions of PopClip. Others have
alternative allowable spellings.
To preserve backwards compatibility, key names in the config (all formats) are transformed as
follows:
1. First, the naming convention is standardized to lowercase with spaces. For
example, `RequiredApps` becomes `required apps`.
2. Then, if the first word is `extension` or `option` (which were
expected by older versions of PopClip), it is removed.
3. Finally, PopClip applies the following mapping:
**Key name mapping table**
| Alternative name | Canonical name |
| ------------------------- | ---------------- |
| apple script | applescript |
| apple script call | applescript call |
| apple script file | applescript file |
| blocked apps | excluded apps |
| flip horizontal | flip x |
| flip vertical | flip y |
| id | identifier |
| image file | icon |
| java script | javascript |
| java script file | javascript file |
| js | javascript |
| lang | language |
| mac os version | macos version |
| params | parameters |
| pass html | capture html |
| pop clip version | popclip version |
| preserve image color | preserve color |
| regular expression | regex |
| required os version | macos version |
| required software version | popclip version |
| script interpreter | interpreter |
**Note: Example**
An old extension uses the key `Extension Image File` to define its icon. PopClip
will first standardize the case to `extension image file`. Then it will remove
the word `extension`, leaving `image file`. Then it will map this to `icon`.
---
# Top-level properties
> Source: https://www.popclip.app/dev/top-level-properties.md
The following keys are used at the top level of the [config](https://www.popclip.app/dev/config.md) to
define properties of the extension itself. All properties are optional except
`name`.
| Key | Type | Description |
| ------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name` (Required) | String (Localizable) | A short, human-readable display name for this extension. |
| `icon` | String | See [Icons](https://www.popclip.app/dev/icons.md). If you omit this field, the icon for the first action will be used (if any), or else no icon will be displayed. |
| `identifier` | String | You may provide a string to uniquely identify this extension. See [The `identifier` field](#the-identifier-field). |
| `description` | String (Localizable) | A short, human readable description of this extension. Appears in the [directory](https://www.popclip.app/extensions/) but not in the app. |
| `keywords` | String | Space-separated words to help people find your extension in the [directory](https://www.popclip.app/extensions/), whose search matches case-insensitively against the name and keywords but not the description. |
| `macosVersion` | String | Minimum macOS version needed by this extension. For example `14.0`. |
| `popclipVersion` | Integer | Minimum PopClip version required. This is the integer build number e.g. `4151`. Specifying the current PopClip version here can help preserve your extension's functionality in future, because PopClip applies backward-compatibility rules for old extensions. |
| `options` | Array | Array of dictionaries defining the options for this extension, if any. See [Options](https://www.popclip.app/dev/options.md). |
| `entitlements` | Array | Only applies to JavaScript extensions. The possible values are `network` (allows use of XMLHttpRequest), `dynamic` (allows dynamically generated actions) and `script` (allows [calling external scripts](https://www.popclip.app/dev/external-scripts.md)). The `dynamic` entitlement cannot be combined with `network` or `script`. |
| `action` or `actions` | Dictionary or Array | A dictionary or array of dictionaries defining the action(s) for this extension. See [Action properties](https://www.popclip.app/dev/actions.md). |
| `submenu` | Array | Makes the extension a single button that opens a submenu of child actions. See [Submenus](https://www.popclip.app/dev/actions.md#submenus). |
| `showAs` | String | Sets the default presentation of the extension's actions in the PopClip bar: `icon` or `text`. If omitted, the default is `icon`. (The user can override this per action.) |
| `authServiceLabel` | String (Localizable) | For extensions with a sign-in (`auth` function): a label identifying the service to which the user is being asked to sign in. Used in UI prompts like _Sign in to your [label] account_. If omitted, the extension name is used. |
| `authKeychain` | String | For extensions with a sign-in (`auth` function): which keychain the sign-in secret goes in. `sync` (the default) shares one sign-in across the user's devices via iCloud Keychain; `local` keeps it on the Mac where the user signed in, so each device signs in separately. |
| `offersMultipleInstances` | Boolean | Controls whether PopClip enables the Duplicate and New Instance commands for this extension. By default, PopClip allows multiple instances if the action has any options. Setting this property will override the automatic behavior. |
| `shellScriptRationale` | String | A brief explanation of why the extension needs a [Shell Script action](https://www.popclip.app/dev/shell-script-actions.md) instead of JavaScript. Not used by the app; required when [submitting](https://www.popclip.app/extensions/submit.md#shell-script-policy) an extension with a Shell Script action to the directory. |
## The `identifier` field
An identifier may contain only alphanumeric characters (`A-Z`, `a-z`, `0-9`),
period (`.`), and hyphen (`-`).
A good identifier should be globally unique so as not to clash with other
creators. Use your own prefix, which could be a reverse DNS-style prefix based
on a domain name you control, such as `com.example.myextension`. Alternatively,
just pick something likely to be unique to you.
If you don't provide an `identifier`, PopClip will identify the extension by the
package directory name (e.g. `Name.popclipext`) if it's a package extension, or
the `name` if it's a snippet.
**Warning: Reserved identifier**
The identifier prefix `com.pilotmoon.` is reserved for signed extensions
published by me. If you try to use it for your own extensions, you'll get an
error.
---
# Action properties
> Source: https://www.popclip.app/dev/actions.md
Action properties can be placed either in an `action` dictionary, in an
`actions` array, or at the top level. Properties set at the top level will apply
to all actions unless overridden in the individual action.
**Example: Action properties at top level**
Consider this extension, which defines two actions:
```yaml
#popclip
name: HTML Demo
actions:
- title: Action A
icon: iconA.png
captureHtml: true
after: show-result
javaScript: return "Hi from Action A - " + popclip.input.html
- title: Action B
icon: iconB.png
captureHtml: true
after: show-result
javaScript: return "Hi from Action B - " + popclip.input.html
```
Since the `captureHtml` and `after` properties are the same for both actions,
they can be placed at the top level:
```yaml
#popclip
name: HTML Demo
captureHtml: true
after: show-result
actions:
- title: Action A
icon: iconA.png
javaScript: return "Hi from Action A - " + popclip.input.html
- title: Action B
icon: iconB.png
javaScript: return "Hi from Action B - " + popclip.input.html
```
If the extension only needs to define a single action, you can place all the
action properties at the top level.
**Example: Single action**
Consider this extension, which defines a single action:
```yaml
#popclip
name: Stickies
action:
serviceName: Make Sticky
captureHtml: true
```
Since there is only one action, the nesting can be eliminated, and all the
action properties can be placed at the top level:
```yaml
#popclip
name: Stickies
serviceName: Make Sticky
captureHtml: true
```
## Common properties
The following keys define properties common to all action types. All properties
are optional.
| Key | Type | Description |
| --------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `title` | String (Localizable) | The title is displayed on the action button if there is no icon. For extensions with icons, the title is displayed in the tooltip. If omitted, the action will take the extension name as its title. |
| `icon` | String | The icon to show on the action button. See [Icons](https://www.popclip.app/dev/icons.md) for the icon specification format. If omitted, the action will take the extension icon as its icon. To explicitly specify no icon, set this field to `null`. |
| `identifier` | String | A string to identify this action. In shell script and AppleScript actions, the identifier is passed to the script. The script can use this to find out which action was pressed. |
| `requirements` | Array | Array consisting of zero or more of the strings listed in [the `requirements` array](#the-requirements-array). All the requirements in the array must be satisfied for the action to appear. If the field is omitted, `[text]` is used by default. To specify no requirements, supply an empty array: `[]`. The `url`, `isurl`, `email` or `path` requirements have side effects - see [Matching order and side effects of `requirements` and `regex`](#matching-order-and-side-effects-of-requirements-and-regex). |
| `regex` | String | A [Regular Expression](https://www.regular-expressions.info/) applied after `requirements` evaluation. The regex runs against the current text (which may already have been narrowed by a `url`, `isurl`, `email` or `path` requirement). If it matches, the action appears and the substring matched by the regex is passed to the action; otherwise the action is hidden. The regex engine follows the [ICU specification](https://unicode-org.github.io/icu/userguide/strings/regexp.html). |
| `excludedApps` | Array | Array of bundle identifiers of applications. The action will not appear when PopClip is being used in any of the specified apps. |
| `requiredApps` | Array | Array of bundle identifiers of applications. The action will only appear when PopClip is being used in one of the specified apps. _Note: This field does not make PopClip do a check to see if the app is present on the computer. For that, use the `app` field._ |
| `before` | String | String to indicate an action PopClip should take _before_ performing the main action. See [The `before` and `after` strings](#the-before-and-after-strings). |
| `after` | String | String to indicate an action PopClip should take _after_ performing the main action. See [The `before` and `after` strings](#the-before-and-after-strings). |
| `app` | Dictionary | Dictionary describing a "target" app or website that this action sends text to or otherwise interacts with. You can, optionally, specify that the app must be present on the system; if not present, PopClip will prompt the user to install. See [The `app` dictionary](#the-app-dictionary). |
| `stayVisible` | Boolean | If `true`, the PopClip popup will not disappear after the user clicks the action. (An example is the Formatting extension.) Default is `false`. |
| `captureHtml` | Boolean | If `true`, PopClip will attempt to capture HTML and Markdown for the selection. PopClip makes its best attempt to extract HTML, first of all from the selection's HTML source itself, if available. Failing that, it will convert any RTF text to HTML. And failing that, it will generate an HTML version of the plain text. It will then generate Markdown from the final HTML. Default is `false`. |
| `captureRtf` | Boolean | If `true`, PopClip will attempt to capture Rich Text (RTF) content for the selection. If no RTF content is found, it will generate an RTF version of the plain text. Default is `false`. |
| `restorePasteboard` | Boolean | If true, then PopClip will restore the pasteboard to its previous contents after pasting text in the `paste-result` after-step. Default is `false`. |
| `submenu` | Array | An array of actions to show in a submenu of this action. See [Submenus](#submenus). |
| `wantsPrimaryDisplay` | Boolean | If `true`, the action asks to be the popup's primary button β the one centred above the pointer when the popup appears. If more than one visible button asks, the leftmost wins. Default is `false`. (Used by the built-in Copy and Paste actions.) |
| `wantsInitialDisplay` | Boolean | For an action with a `submenu`: if `true`, the submenu asks to be already open when the popup appears, instead of waiting to be clicked. If more than one submenu asks, the first found wins. Default is `false`. (Used by the built-in Spelling action.) |
| Type-specific keys | Varies | See: [Shortcut actions](https://www.popclip.app/dev/shortcut-actions.md), [Service actions](https://www.popclip.app/dev/service-actions.md), [URL actions](https://www.popclip.app/dev/url-actions.md). [Key Press actions](https://www.popclip.app/dev/key-press-actions.md), [Shell Script actions](https://www.popclip.app/dev/shell-script-actions.md), [AppleScript actions](https://www.popclip.app/dev/applescript-actions.md), [JavaScript actions](https://www.popclip.app/dev/js-actions.md). |
### The `requirements` array
These are the values supported by the `requirements` array. Additionally, you
can prefix any requirement with `!` to negate it.
| Value | Description |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `text` | One or more characters of text must be selected. |
| `copy` | Synonym for `text` (for backward compatibility). |
| `cut` | Text must be selected and the app's Cut command must be available. |
| `paste` | The app's Paste command must be available. |
| `url` | The text must _contain_ exactly one web URL (http or https). _(see side effects below)_ |
| `isurl` | The text must _be_ a valid web URL (http or https), with no other text apart from whitespace. _(see side effects below)_ |
| `urls` | The text must contain one or more web URLs (http or https). |
| `email` | The text must contain exactly one email address. _(see side effects below)_ |
| `emails` | The text must contain one or more email addresses. |
| `path` | The text must be a local file path, and it must exist on the local file system. _(see side effects below)_ |
| `formatting` | The selected text control must support formatting. (PopClip makes its best guess about this, erring on the side of a false positive.) |
| `option-foo=bar` | The option with identifier `foo` must be equal to the string `bar`. This mechanism allows actions to be enabled and disabled via options. Boolean option values map to the strings `1` and `0`. |
### Matching order and side effects of `requirements` and `regex`
PopClip evaluates filters in this order:
1. Requirements: First, all `requirements` are checked. If a requirement is one
of `url`, `isurl`, `email` or `path`, PopClip narrows the working text to the
detected value and normalizes it:
- `url` / `isurl`: Only the matching URL is kept, expanded to a full form
with `https://` added if no scheme is present. Example: selecting
`go to apple.com` with `url` requirement yields `https://apple.com`.
- `email`: Only the matching email address is kept.
- `path`: The path is standardized with `~` and `..` expanded (e.g.
`~/Documents` β `/Users/username/Documents`).
2. Regex: Next, if a `regex` is specified, it is applied to the current working
text from step 1. If it matches, the regex match becomes the text passed to
the action; if not, the action is hidden.
Scripts can still read both the final narrowed string and the original full
selection via:
- Shell/AppleScript: `POPCLIP_TEXT` / `{popclip text}` (narrowed) and
`POPCLIP_FULL_TEXT` / `{popclip full text}` (full)
- JavaScript: `popclip.input.matchedText` (narrowed string),
`popclip.input.regexResult` (match result array with capture components) and
`popclip.input.text` (full string)
**Example: requirement + regex narrowing**
```yaml
#popclip
name: Domain WHOIS 1
requirements: [url] # 1) narrow to a single valid URL
regex: (?<=:\/\/)[^\/]+ # 2) match just the host part
url: https://www.whois.com/whois/***
```
- If the user selects: `Check this link: https://example.org/docs` β
- Requirement `url` narrows text to `https://example.org/docs`.
- Regex matches `example.org` which is passed to the action and shown.
JavaScript variant using the capture array:
```yaml
#popclip
name: Domain WHOIS 2
requirements: [url]
regex: https?:\/\/([^\/]+)
javaScript: popclip.openUrl('https://www.whois.com/whois/' + encodeURIComponent(popclip.input.regexResult[1]))
```
Here, the full URL is the regex match, and the domain is taken from capture
group 1 via `regexResult[1]`.
### The `before` and `after` strings
The `cut`, `copy`, `paste` and `paste-plain` values can be used as the `before`
string. All the values can be used as the `after` string.
| Value | Description |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `copy-result` | Copy the text returned from the script to the clipboard. Displays "Copied" notification. |
| `paste-result` | If the app's Paste command is available, paste the text returned from the script, as well as copy it to the clipboard. Otherwise, copy it as in `copy-result`. |
| `preview-result` | Copy the result to the pasteboard and show the result to the user, truncated to 160 characters. If the app's Paste command is available, the preview text can be clicked to paste it. |
| `show-result` | Copy the result to the pasteboard and show it to the user, truncated to 160 characters. |
| `show-status` | Show a tick or an 'X', depending on whether the script succeeded or not. |
| `cut` | Invoke app's Cut command, as if user pressed βX. |
| `copy` | Invoke app's Copy command, as if user pressed βC. |
| `paste` | Invoke app's Paste command, as if user pressed βV. |
| `paste-plain` | Reduce the current clipboard to plain text only, then invoke app's Paste command. |
| `popclip-appear` | Trigger PopClip to appear again with the current selection. (This is used by the Select All extension.) |
| `copy-selection` | Place the original selected text to the clipboard. (This is used by the Swap extension.) |
### The `app` dictionary
The `app` field is a dictionary with the following structure:
| Key | Type | Required? | Description |
| ------------------- | ------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name` | String | Required | Name of the app or website that this extension interacts with. For example `Evernote`. |
| `link` | String | Required | Link to the website or app home page where the user can obtain the app. For example `https://evernote.com/`. |
| `checkInstalled` | Boolean | Optional | If `true`, PopClip will check whether an app with one of the given `bundleIdentifiers` is installed when the user tries to use the extension. If none is found, PopClip will show a message and a link to the website given in `link`. Default is `false`. |
| `bundleIdentifiers` | Array | Required if `checkInstalled` is `true` | Array of bundle identifiers for this app, including all application variants that work with this extension. In the simplest case there may be just one bundle ID. An app may have alternative bundle IDs for free/pro variants, an App Store version, a standalone version, a Setapp version, and so on. Include all the possible bundle IDs that the user might encounter. |
To specify multiple apps, use the `apps` field instead, supplying an array of
dictionaries.
## Submenus
_New in PopClip 2026.7._
An action can define a submenu of child actions, using the `submenu` property.
The value is an array of action dictionaries (or a single action dictionary).
Submenus can be nested.
If the action defines no behaviour of its own, it will appear like a folder
and the submenu will open on mouse hover.
If the action has its own behavior, it will appear like a regular action button
and the user can display the submenu by secondary click (Control-click or right-click).
The `submenu` property can also be placed at the top level of the config, to
make the whole extension appear as a single button that opens a submenu. In
that case, it cannot be combined with the `action` or `actions` fields.
```yaml
#popclip
name: Search Menu
icon: symbol:magnifyingglass
submenu:
- title: Google
url: https://www.google.com/search?q=***
- title: Wikipedia
url: https://en.wikipedia.org/wiki/Special:Search?search=***
- separator: true
- title: Startpage
url: https://www.startpage.com/sp/search?query=***
```
### Example: Shell Script with supplementary actions
This example [package](https://www.popclip.app/dev/packages.md) extension shows a submenu used to offer
variants of an action alongside a main one. It is an adaptation of the
[Comment extension](https://github.com/ttscoff/popclipextensions/tree/master/Comment.popclipext)
by Brett Terpstra. The supplementary actions each
specify an `identifier`, which is how the script can tell which action was
clicked.
`Config.yaml`:
```yaml
name: Comment
icon: symbol:text.bubble
shellScriptFile: comment.rb
after: paste-result
submenu:
- title: Hash Comment
identifier: hash
shellScriptFile: comment.rb
after: paste-result
- title: Slash Comment
identifier: slash
shellScriptFile: comment.rb
after: paste-result
- title: CSS Comment
identifier: css
shellScriptFile: comment.rb
after: paste-result
```
`comment.rb` (with executable flag set):
```ruby
#!/usr/bin/ruby
input = ENV['POPCLIP_TEXT']
case ENV['POPCLIP_ACTION_IDENTIFIER']
when 'hash'
print input.split("\n").map {|line|
"# #{line}"
}.join("\n")
when 'css'
space = input.match(/^((?:\n\s*)*)\S.*?((?:\n\s*)*)$/m)
print "#{space[1]}/* #{input.strip} */#{space[2]}"
when 'slash'
print input.split("\n").map {|line|
"// #{line}"
}.join("\n")
else # HTML
space = input.match(/^([\s\n]*)\S.*?([\s\n]*)$/m)
print "#{space[1]}#{space[2]}"
end
```
**Note: No top-level fallback in submenus**
Note that `shellScriptFile` and `after` are repeated for each action in the
submenu. Unlike the `actions` array, properties set at the top level of the
config do not act as fallback values for the actions in a `submenu`.
### Example: JavaScript module version
Here is the same extension expressed as a
[module](https://www.popclip.app/dev/js-modules.md) snippet, in JavaScript. In a module, there is no
need for action identifiers β each action supplies its own `code` function
inline:
```javascript
// #popclip
// name: Comment
// icon: symbol:text.bubble
defineExtension({
actions: [
{
title: "Comment",
code: (input) => popclip.pasteText(``),
submenu: [
{
title: "Hash Comment",
code: (input) =>
popclip.pasteText(input.text.replaceAll(/^/gm, "# ")),
},
{
title: "Slash Comment",
code: (input) =>
popclip.pasteText(input.text.replaceAll(/^/gm, "// ")),
},
{
title: "CSS Comment",
code: (input) => popclip.pasteText(`/* ${input.text.trim()} */`),
},
],
},
],
});
```
### Submenu functions
JavaScript extensions can alternatively supply a function as the `submenu`
value, to generate the submenu's actions dynamically when it opens. This
requires the `dynamic` entitlement. See
[Submenu functions](https://www.popclip.app/dev/js-modules.md#submenu-functions).
### Separators
Within a `submenu` array, you can insert a separator
gap between actions by adding the special entry `{ separator: true }`, as
shown in the example above.
---
# Options
> Source: https://www.popclip.app/dev/options.md
An extension declares user-settable options with the `options` array at the
top level of its [config](https://www.popclip.app/dev/config.md). Options are presented to the user in a
preferences user interface window and are saved in PopClip's preferences on
behalf of the extension. Options appear in the UI in the order they appear in
the `options` array.
## Option properties
An option dictionary has the following structure.
| Key | Type | Required? | Description |
| -------------- | -------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `identifier` | String | Required | Identifying string for this option. This is passed to your script. The identifier will be downcased or upcased for AppleScript and Shell Script targets, respectively β see [Script variables](https://www.popclip.app/dev/script-variables.md). |
| `type` | String | Required | See [Option types](#option-types). |
| `label` | String (Localizable) | Optional | The label to appear in the UI for this option. If omitted, the identifier is displayed. |
| `description` | String (Localizable) | Optional | A longer description to appear in the UI to explain this option. May contain clickable links, written either as bare URLs or in Markdown syntax: `[label](https://example.com)`. |
| `defaultValue` | String | Optional | This field specifies the default value of the option. If omitted, `string` options default to the empty string, `boolean` options default to `true`, and `multiple` options default to the top item in the list. A `secret` field may not have a default value. |
| `values` | Array | Required for `multiple` type | Array of strings representing the possible values for the multiple choice option. |
| `valueLabels` | Array | Optional | Array of "human friendly" strings corresponding to the multiple choice values. This is used only in the PopClip options UI, and is not passed to the script. If omitted, the option values themselves are shown. |
| `inset` | Boolean | Optional | If true, the option field will be shown inset to the right of the label, instead of under it. Default is false. |
| `icon` | String | Optional | For `boolean` options only. Specify an icon to appear next to the check box. |
| `multiline` | Boolean | Optional | For `string` options only. If true, shows a multi-line text field instead of a single-line one. Useful for longer inputs such as prompts. Default is false. |
| `allowOther` | Boolean | Optional | For `multiple` options only. If true, adds an "Otherβ¦" choice to the list, allowing the user to enter a free-text value. Default is false. |
| `allowNone` | Boolean | Optional | For `multiple` options only. If true, adds a "None" choice to the list, whose value is the empty string. Default is false. |
| `keychain` | String | Optional | For `secret` options only. Which keychain the value goes in: `sync` (the default) shares one value across the user's devices via iCloud Keychain; `local` keeps it only on the Mac where it was entered. |
## Option types
The `type` field of an option dictionary can be one of the following:
| Type | Description |
| ---------- | ------------------------------------------------------------------------- |
| `string` | A text field. |
| `boolean` | A checkbox. |
| `multiple` | A multiple choice list. An array of `values` strings must be provided. |
| `secret` | Concealed text entry. The value is persisted in the keychain. |
| `heading` | Shows as a text heading in the settings user interface. Carries no value. |
---
# Icons
> Source: https://www.popclip.app/dev/icons.md
Icons are specified by using a text string to describe an icon.
_(An interactive icon preview tool is available in the online version of this page, at . Alternatively, a PNG rendering of any icon specifier can be fetched from `https://icons.popclip.app/icon?specifier=` β the preview links in the tables below use this.)_
**Tip: Icon Picker in the app**
PopClip has a built-in icon browser: choose **Icon Picker** from the Tools
menu in PopClip's settings window (or press β₯βI). Search the Iconify icon
libraries, preview any icon specifier with modifiers live, and copy the
resulting string for use in your config.
An icon specifier string describes an icon using a simple text-based format. The
string consists of a series of space-separated keywords, with the final keyword
specifying the **base icon** (see [Base icon formats](#base-icon-formats)), and
the preceding keywords (if any) specifying **modifiers** (see
[Icon modifiers](#icon-modifiers)).
Here are some examples:
| Specifier string | Icon generated | Notes |
| -------------------------------- | ------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------- |
| `T` | [preview](https://icons.popclip.app/icon?specifier=T) | Here, `T` specifies the base icon as a [text icon](#text-icons). |
| `square T` | [preview](https://icons.popclip.app/icon?specifier=square%20T) | Here, `square` is a modifier that encloses the base icon in a square. |
| `square filled T` | [preview](https://icons.popclip.app/icon?specifier=square%20filled%20T) | Combining two modifiers; `filled` specifies that the square is a solid shape. |
| `circle filled T` | [preview](https://icons.popclip.app/icon?specifier=circle%20filled%20T) | The `circle` modifier encloses the base icon in a circle. |
| `search filled T` | [preview](https://icons.popclip.app/icon?specifier=search%20filled%20T) | The `search` modifier encloses the base icon in a magnifying glass shape. |
| `iconify:mdi:home` | [preview](https://icons.popclip.app/icon?specifier=iconify%3Amdi%3Ahome) | Here, the base icon is an [Iconify icon](#iconify-icons). |
| `square filled iconify:mdi:home` | [preview](https://icons.popclip.app/icon?specifier=square%20filled%20iconify%3Amdi%3Ahome) | We put the home icon in a filled square. |
| `strike iconify:mdi:home` | [preview](https://icons.popclip.app/icon?specifier=strike%20iconify%3Amdi%3Ahome) | The `strike` modifier draws a strike-through line over the base icon. |
| `symbol:hand.raised` | [preview](https://icons.popclip.app/icon?specifier=symbol%3Ahand.raised) | Here, the base icon as an [SF Symbols icon](#sf-symbols-icons). |
| `flip-x symbol:hand.raised` | [preview](https://icons.popclip.app/icon?specifier=flip_x%20symbol%3Ahand.raised) | The `flip-x` modifier flips the base icon horizontally. |
## Base icon formats
### File icons
File icons can only be used in [packages](https://www.popclip.app/dev/packages.md). The icon is specified
as a path to a `.png` or `.svg` image file in the package.
```json
{
"icon": "icon.png"
}
```
A good icon will feature a monochrome shape on a transparent background.
Variable opacity can be used for shading. PNG icons should be at least 256
pixels high.
**Note: File icons with modifiers**
File icons can be used with modifiers by adding the prefix `file:`, for example:
```
{
"icon": "strike file:icon.png"
}
```
### Text icons
Text icons can include up to 3 characters and are specified as the text itself.
The prefix `text:` can optionally be used.
```json
{
"icon": "T"
}
```
Text icons are drawn using the system font. Adding the `monospaced` modifier
will draw the icon in a monospaced variant.
If the text icon is a single emoji without modifiers, it rendered in color.
Examples:
| Specifier string | Icon generated |
| --------------------- | ------------------------------------------------------------------------------------- |
| `ABC` (or `text:ABC`) | [preview](https://icons.popclip.app/icon?specifier=ABC) |
| `@` | [preview](https://icons.popclip.app/icon?specifier=%40) |
| `ζ¬` | [preview](https://icons.popclip.app/icon?specifier=%E6%9C%AC) |
| `()` | [preview](https://icons.popclip.app/icon?specifier=%28%29) |
| `monospaced ()` | [preview](https://icons.popclip.app/icon?specifier=monospaced%20%28%29) |
| `π΅βπ«` | [preview](https://icons.popclip.app/icon?specifier=%F0%9F%98%B5%E2%80%8D%F0%9F%92%AB) |
**Note: π‘ Tip: Monospaced font**
Punctuation symbols often look better in icons when drawn with the `monospaced`
modifier.
### Iconify icons
[Iconify](https://iconify.design/) provides access to over 200,000 icons from a
variety of open-source icon sets, using a unified naming system.
The Iconify website provides a [catalog](https://icon-sets.iconify.design/) of
available icons.
The format is `iconify::`.
Some Iconify icons contain color information. These are automatically recognized
by PopClip and will be rendered in color.
Examples:
| Specifier string | Icon generated |
| ---------------------------- | ---------------------------------------------------------------------------------- |
| `iconify:ion:fish` | [preview](https://icons.popclip.app/icon?specifier=iconify%3Aion%3Afish) |
| `iconify:solar:flag-bold` | [preview](https://icons.popclip.app/icon?specifier=iconify%3Asolar%3Aflag-bold) |
| `iconify:logos:spotify-icon` | [preview](https://icons.popclip.app/icon?specifier=iconify%3Alogos%3Aspotify-icon) |
### SF Symbols icons
Apple [SF Symbols](https://developer.apple.com/sf-symbols/) are available on
macOS 11.0 and above. (Symbol availability may vary by macOS version). The icon
catalog can be viewed by installing Apple's SF Symbols app on your Mac.
The format is `symbol:`.
Symbols are always drawn in the monochrome variant.
Examples:
| Specifier string | Icon generated |
| ----------------------- | --------------------------------------------------------------------------- |
| `symbol:flame` | [preview](https://icons.popclip.app/icon?specifier=symbol%3Aflame) |
| `symbol:hand.raised` | [preview](https://icons.popclip.app/icon?specifier=symbol%3Ahand.raised) |
| `symbol:signpost.right` | [preview](https://icons.popclip.app/icon?specifier=symbol%3Asignpost.right) |
### SVG Icons
The icon string can supply SVG source code for an icon. The format is `svg:`.
**Example**
`svg: `
generates:
[preview](https://icons.popclip.app/icon?specifier=svg%3A%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20width%3D'1em'%20height%3D'1em'%20viewBox%3D'0%200%2024%2024'%3E%3Cpath%20fill%3D'currentColor'%20d%3D'm6%2010.95l-1.875%201.025l-2.975-5.2L7.75%203H10v1q0%20.825.588%201.413T12%206q.825%200%201.413-.587T14%204V3h2.25l6.6%203.775l-2.95%205.15l-1.9-.95V21H6z'%2F%3E%3C%2Fsvg%3E)
### Data icons
The icon string can include raw image data as a [data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs).
The format is: `data:[;base64],`, where `` may be
either `image/svg+xml` or `image/png`.
**SVG Example**
Specifier string:
`data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%3E%3Cpath%20fill%3D%22currentColor%22%20d%3D%22M5.5%2015v-4.5H4V9h3v6H5.5ZM9%2015v-2.5q0-.425.288-.713T10%2011.5h2v-1H9V9h3.5q.425%200%20.713.288T13.5%2010v1.5q0%20.425-.288.713t-.712.287h-2v1h3V15H9Zm6%200v-1.5h3v-1h-2v-1h2v-1h-3V9h3.5q.425%200%20.713.288T19.5%2010v4q0%20.425-.288.713T18.5%2015H15Z%22%2F%3E%3C%2Fsvg%3E`
generates:
[preview](https://icons.popclip.app/icon?specifier=data%3Aimage%2Fsvg%2Bxml%2C%253Csvg%2520xmlns%253D%2522http%253A%252F%252Fwww.w3.org%252F2000%252Fsvg%2522%2520width%253D%252224%2522%2520height%253D%252224%2522%2520viewBox%253D%25220%25200%252024%252024%2522%253E%253Cpath%2520fill%253D%2522currentColor%2522%2520d%253D%2522M5.5%252015v-4.5H4V9h3v6H5.5ZM9%252015v-2.5q0-.425.288-.713T10%252011.5h2v-1H9V9h3.5q.425%25200%2520.713.288T13.5%252010v1.5q0%2520.425-.288.713t-.712.287h-2v1h3V15H9Zm6%25200v-1.5h3v-1h-2v-1h2v-1h-3V9h3.5q.425%25200%2520.713.288T19.5%252010v4q0%2520.425-.288.713T18.5%252015H15Z%2522%252F%253E%253C%252Fsvg%253E)
**PNG Example**
Specifier string:
`data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEABAMAAACuXLVVAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAJcEhZcwAAFiUAABYlAUlSJPAAAAAwUExURUdwTAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACVM9DkAAAAPdFJOUwDcIDTLUFYGEqLpZfaDQdxVBh4AAAIbSURBVHja7doxS0JRGMZxuVI6BUG4GBhtDoHQkmPQViSBtAsVjS66CNIuLrWF0CcIaalojsAhXEMaGoJwvOCiCbfR9w5dXI6P4v/3Be4D5/C87xFjMQAAAAAAAAAAAAAAAAAAAADAIknsTmvPTYD4/vqUUmUnASrBtH6vCEAAAhCAAARY9gBv6nF87WYhqR9E2DTfH/y4CeCdRrg1AdJurkD0utaafH/8IVhYd9qTAMOt2X8/+WBO4Lg8+wCVwuT7/rPgBPrdSYB3wRX0eu5LIFIz474EIt9sZ+ISWLsTl0C2Ky6BnrgEquoS+A60JWDnkKYE2uISsHMoJTiBekFcAnlxCXjqEmiYOeRfCq5gcdlLwC6jr4ISCC2jKcEVrMxTCQwUJWCv4KHgBOwyOhKcQGgZ7eQEy2jB1rB4GR08iZfRe0EN12wJ3IiX0cGXeA4pSsDOIUUJhOZQR1DDdXUJ5MUl4KlLwC6jY8EullBfwZWC+Dlgr6C/jFewJr6CsWKg/U0gNIfSijnUFc+hF3EJrKpLoK8ugZa4BOzvcpISKIlLwDsSl0BTXAKJkrgE4uoSyIpLIPkpLoGquAQS9kWq+GHQYxOYoxfpWPIcCMSbgLoEGuoSOBGXQGgODR8v/nXu6HTsHArGEX/t3si5L4FIIzcBKhlxgGYgDpAlAAEIQAACEGBxAviOFpLtqQleDAAAAAAAAAAAAMCi+gOiz1VAs+KXUwAAAABJRU5ErkJggg==`
generates:
[preview](https://icons.popclip.app/icon?specifier=data%3Aimage%2Fpng%3Bbase64%2CiVBORw0KGgoAAAANSUhEUgAAAQAAAAEABAMAAACuXLVVAAAABGdBTUEAALGPC%2FxhBQAAAAFzUkdCAK7OHOkAAAAJcEhZcwAAFiUAABYlAUlSJPAAAAAwUExURUdwTAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACVM9DkAAAAPdFJOUwDcIDTLUFYGEqLpZfaDQdxVBh4AAAIbSURBVHja7doxS0JRGMZxuVI6BUG4GBhtDoHQkmPQViSBtAsVjS66CNIuLrWF0CcIaalojsAhXEMaGoJwvOCiCbfR9w5dXI6P4v%2F3Be4D5%2FC87xFjMQAAAAAAAAAAAAAAAAAAAADAIknsTmvPTYD4%2FvqUUmUnASrBtH6vCEAAAhCAAARY9gBv6nF87WYhqR9E2DTfH%2Fy4CeCdRrg1AdJurkD0utaafH%2F8IVhYd9qTAMOt2X8%2F%2BWBO4Lg8%2BwCVwuT7%2FrPgBPrdSYB3wRX0eu5LIFIz474EIt9sZ%2BISWLsTl0C2Ky6BnrgEquoS%2BA60JWDnkKYE2uISsHMoJTiBekFcAnlxCXjqEmiYOeRfCq5gcdlLwC6jr4ISCC2jKcEVrMxTCQwUJWCv4KHgBOwyOhKcQGgZ7eQEy2jB1rB4GR08iZfRe0EN12wJ3IiX0cGXeA4pSsDOIUUJhOZQR1DDdXUJ5MUl4KlLwC6jY8EullBfwZWC%2BDlgr6C%2FjFewJr6CsWKg%2FU0gNIfSijnUFc%2BhF3EJrKpLoK8ugZa4BOzvcpISKIlLwDsSl0BTXAKJkrgE4uoSyIpLIPkpLoGquAQS9kWq%2BGHQYxOYoxfpWPIcCMSbgLoEGuoSOBGXQGgODR8v%2FnXu6HTsHArGEX%2Ft3si5L4FIIzcBKhlxgGYgDpAlAAEIQAACEGBxAviOFpLtqQleDAAAAAAAAAAAAMCi%2BgOiz1VAs%2BKXUwAAAABJRU5ErkJggg%3D%3D)
## Icon modifiers
The following modifiers can be prefixed to the specifier string to alter how the
icon is drawn.
### Style modifiers
| Keyword | Description |
| ------------ | ----------------------------------------------------------- |
| `square` | Enclose the icon in a square. |
| `circle` | Enclose the icon in a circle. |
| `search` | Enclose the icon in a magnifying glass shape. |
| `strike` | Draw a strike-through line over the icon. |
| `filled` | Draw the enclosing shape as a solid shape. |
| `monospaced` | For text icons only. Draw the text using a monospaced font. |
### Geometric transformations
| Keyword | Description |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `flip-x` | Flip the icon horizontally. |
| `flip-y` | Flip the icon vertically. |
| `move-x=` | Move the icon horizontally by the specified distance, expressed as percentage of the icon's width. For example `move-x=10` to move 10% right or `move-x=-5` to move 5% left. |
| `move-y=` | Move the icon vertically by the specified distance, expressed as percentage of the icon's height. |
| `scale=` | Adjust the scale at which the icon is drawn. For example `scale=120` to enlarge to 120%, or `scale=90` to shrink to 90%. |
| `rotate=` | Rotate the icon by the specified number of degrees. For example `rotate=90` to rotate 90 degrees anticlockwise. |
Examples:
| Specifier string | Icon generated |
| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `symbol:signpost.right` | [preview](https://icons.popclip.app/icon?specifier=symbol%3Asignpost.right) |
| `flip-x symbol:signpost.right` | [preview](https://icons.popclip.app/icon?specifier=flip_x%20symbol%3Asignpost.right) |
| `move-y=-50 symbol:signpost.right` | [preview](https://icons.popclip.app/icon?specifier=move_y%3D-50%20symbol%3Asignpost.right) |
| `scale=50 symbol:signpost.right` | [preview](https://icons.popclip.app/icon?specifier=scale%3D50%20symbol%3Asignpost.right) |
| `rotate=90 symbol:signpost.right` | [preview](https://icons.popclip.app/icon?specifier=rotate%3D90%20symbol%3Asignpost.right) |
| `square filled move-x=4 move-y=-4 scale=115 rotate=45 T` | [preview](https://icons.popclip.app/icon?specifier=square%20filled%20move_x%3D4%20move_y%3D-4%20scale%3D115%20rotate%3D45%20T) |
### Color and aspect
| Keyword | Description |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `preserve-color` | The base icon will be displayed in its original colors instead of used as a monochrome mask. (This is applied implicitly to emoji and color Iconify icons.) |
| `preserve-aspect` | If the base icon is not square, it by default rendered into a square canvas. With this modifier, the icon will be rendered with its original aspect ratio. |
Example:
```json
{
"icon": "preserve-color file:rainbow.png"
}
```
**Tip: Negative modifiers**
In some cases it may be useful to explicitly negate a modifier. This is done by
appending `=0`. For example, to remove the implicit color rendering from an
Iconify icon, use `preserve-color=0`.
## Icon Preview tool

As a handy tool, the following snippet defines an extension that will display
the icon for any text string you select. (To see how to install this, see
[Snippets](https://www.popclip.app/dev/snippets.md).)
```javascript
// #popclip
// name: Icon Preview
// entitlements: [dynamic]
defineExtension({
actions: () => {
return [
{
icon: popclip.input.text,
},
];
},
});
```
---
# Packages
> Source: https://www.popclip.app/dev/packages.md
A PopClip extension package bundles together all the files needed for an
extension in a folder. A package wraps up an extension
so that it can be published as a file download, then installed with a double-click.
Packages are the format used by the [PopClip extensions directory](https://www.popclip.app/extensions/).
## The package folder
A PopClip extension package consists of a config file plus optional additional files,
all contained in a directory whose name ends
with `.popclipext`.
When you double-click a `.popclipext` package, macOS will open it with PopClip,
which will attempt to load and install it.
**Tip: Viewing package contents**
macOS treats `.popclipext` directories as packages. To view the contents of a
package, right-click it in Finder and choose Show Package Contents.
Here is an example package structure, the
[DeepL Translator](https://github.com/pilotmoon/PopClip-Extensions/tree/master/source/DeepLTranslator.popclipext)
extension:
```
DeepLTranslator.popclipext/ -- Package folder
β
βββ Config.ts -- Config and code, in one TypeScript file
βββ Readme.md -- Readme file
βββ deepl.png -- Icon file
```
### A minimal package
At the other end of the scale, a package needs nothing more than a folder
with a snippet inside:
```
Uppercase.popclipext/
βββ Config.js
```
where `Config.js` contains, for example:
```javascript
// #popclip
// name: Uppercase
popclip.pasteText(popclip.input.text.toUpperCase());
```
### Zipped `.popclipextz` files
For distribution, an extension package folder may be zipped and renamed with the
extension `.popclipextz`. Double-clicking these files opens them directly with PopClip.
You can examine an existing PopClip extension by
renaming it with a `.zip` extension and unzipping it, to reveal a `.popclipext`
package.
**Note: Tidying up**
After PopClip installs an extension from a `.popclipextz` file, it deletes the file.
## The Config file
Every package must include a [config file](https://www.popclip.app/dev/config.md). PopClip will try
looking in the root of the package directory for a file with base name `Config`
(case sensitive). The file is interpreted according to its extension:
| File Name | Format | Interpretation |
| ----------------------------------------------------------------------------------------------------- | ------- | ------------------------------------------------------------------------------- |
| `Config.plist` | Plist | An Apple [XML Property List](https://en.wikipedia.org/wiki/Property_list) file. |
| `Config.json` | JSON | A [JSON](https://www.json.org/json-en.html) file. |
| `Config.yaml` | YAML | A [YAML 1.2](https://yaml.org) file. |
| `Config.js` `Config.ts` `Config.applescript` `Config.` ...or just `Config` | Snippet | Interpreted as [snippet](https://www.popclip.app/dev/snippets.md). |
**Note: Historical note**
Plist was the original format for PopClip extensions. It is not recommended
for new extensions β see [Plist](https://www.popclip.app/dev/config.md#plist).
## Other files
Apart from the config file, an extension package may contain any number of other
files. You are free to name these however you like, except for the reserved
names `Config[.*]` and `_Signature.plist`. You can also use subfolders to
organise your files.
You can prefix file or folder names with an underscore `_` or dot `.` to [exclude](https://www.popclip.app/extensions/submit.md#excluded-files)
them from the final package delivered by the PopClip extensions directory.
Handy for test files or documentation.
## Examples
For a whole bunch of example extension packages,
see [pilotmoon/PopClip-Extensions/.../source](https://github.com/pilotmoon/PopClip-Extensions/tree/master/source).
---
# Authenticating to external services
> Source: https://www.popclip.app/dev/auth.md
Extensions that talk to an external service on the user's behalf usually need
a credential of some kind. This page describes the tools PopClip provides for
signing in to services and storing secrets.
## API key authentication
If the service just needs an API key that the user can obtain and paste in,
you don't need any special machinery. Define an [option](https://www.popclip.app/dev/options.md)
of type `secret`: it appears as a concealed text field, and PopClip stores
the value in the user's keychain.
For anything more involved β validating a username and password, or an OAuth
sign-in β use the `auth` function.
## The `auth` function
A [module extension](https://www.popclip.app/dev/js-modules.md) can define an
[`auth` function](https://www.popclip.app/dev/api/interfaces/Extension.html#auth). When it does,
PopClip shows a **Sign in** button in the action's settings UI, and calls the
function when the user clicks it:
```ts
type AuthFunction = (
info: AuthInfo,
flow: AuthFlowFunction,
) => Promise;
```
The [`info` object](https://www.popclip.app/dev/api/interfaces/AuthInfo.html) carries the values of
the extension's `username` and `password` options (if defined), the
extension's name and identifier, and a `redirect` URL for use in OAuth flows.
Whatever the function returns is saved in the user's keychain as the
extension's `authsecret`, and the settings UI switches to a signed-in state
with a **Sign out** button (which clears the stored secret). Return a plain
string, or an [AuthResult](https://www.popclip.app/dev/api/interfaces/AuthResult.html) object
`{ secret, label, expiresIn }` β the `label` is shown as the signed-in
account identifier, and `expiresIn` (a token lifetime in seconds) makes
PopClip treat the sign-in as expired after that time.
## Username and password sign-in
For services that authenticate with a username and password, define options
with the identifiers `username` and `password`. PopClip passes their values
to the `auth` function in `info`. An option of type `password` is never
stored β it exists only to be passed to the `auth` function.
The [Pinboard extension](https://github.com/pilotmoon/PopClip-Extensions/tree/master/source/Pinboard.popclipext)
uses this pattern to retrieve the user's API token. Here is a compact but
complete version of it, as an installable snippet β the `auth` function
signs in, and the action then uses the stored `authsecret` to bookmark the
selected URL:
```javascript
// #popclip
// name: Pinboard
// icon: iconify:simple-icons:pinboard
// requirements: [url]
// entitlements: [network]
// after: show-status
import axios from "axios";
const api = axios.create({
baseURL: "https://api.pinboard.in/v1/",
params: { format: "json" },
});
defineExtension({
options: [
{ identifier: "username", type: "string", label: "Username" },
{ identifier: "password", type: "password", label: "Password" },
],
auth: async (info) => {
// validate the credentials by fetching the user's API token,
// using HTTP basic authentication
const response = await api.get("user/api_token", { auth: info });
return response.data.result;
},
action: async (input, options, context) => {
// bookmark the selected URL
const url = input.data.urls[0];
const description = context.browserUrl === url ? context.browserTitle : url;
const auth_token = `${options.username}:${options.authsecret}`;
await api.get("posts/add", { params: { url, description, auth_token } });
},
});
```
**Note: `secret` vs `password` options**
Both option types conceal their input; the difference is what happens to the
value. A `secret` option is stored in the user's keychain, for a credential
the extension keeps and uses β typically a pasted API key. A `password`
option is never stored: the `auth` function uses it once to obtain a token
from the service, and only the token is kept. PopClip never retains the
user's actual password.
## OAuth sign-in
For OAuth authorization-code flows, use the `flow` callback passed as the
`auth` function's second parameter. Calling it opens the service's
authorization page in the user's browser, with your parameters appended.
After the user approves, the service redirects the browser to the
`info.redirect` URL β a local address that PopClip itself serves β and
`flow` resolves with the query parameters you named in `expect`:
```ts
defineExtension({
auth: async (info, flow) => {
// step 1: the user authorizes the extension in their browser
const { code } = await flow(
"https://example.com/oauth/authorize",
{ client_id, redirect_uri: info.redirect },
["code"],
);
// step 2: exchange the authorization code for an access token
const { data } = await axios.post("https://example.com/oauth/token", {
grant_type: "authorization_code",
code,
client_id,
client_secret,
redirect_uri: info.redirect,
});
return { secret: data.access_token, expiresIn: data.expires_in };
},
});
```
The [Raindrop.io extension](https://github.com/pilotmoon/PopClip-Extensions/tree/master/source/RaindropIO.popclipext)
is a complete working example of this pattern.
For services still using OAuth 1.0a request signing, the
[`oauth-1.0a`](https://www.popclip.app/dev/js-environment.md#bundled-libraries) library is bundled in
PopClip's JavaScript environment.
## Using the stored secret
Action code reads the stored secret as `options.authsecret`. It has one
special behaviour: accessing it while the extension is not signed in throws
an error, so an action that requires sign-in fails with a "Not signed in"
message rather than proceeding with an empty credential.
```ts
defineExtension({
action: {
requirements: ["url"],
async code(input, options) {
await axios.post(
"https://example.com/api/save",
{ url: input.data.urls[0] },
{ headers: { Authorization: `Bearer ${options.authsecret}` } },
);
popclip.showSuccess();
},
},
});
```
If the service rejects the stored secret β an expired or revoked token, say β
throw the error returned by
[`popclip.signInRequiredError()`](https://www.popclip.app/dev/api/interfaces/PopClip.html#signinrequirederror).
PopClip clears the saved secret, so the extension shows as signed out, and
opens the settings UI for the user to sign in again. (The related
[`popclip.settingsRequiredError()`](https://www.popclip.app/dev/api/interfaces/PopClip.html#settingsrequirederror)
sends the user to settings _without_ signing them out β for example when a
required option is missing.)
## Registering as a client app
Before you can use OAuth, you have to register an application with the service
to obtain a client identifier. A few notes on doing that as an extension
author.
**Register in your own name.** The registration is yours: you hold the
credentials, and the service will contact you about quotas, policy changes and
anything it considers abuse. Please don't register as just "PopClip", or use the
PopClip icon.
**Choose a name that tells the user what they are approving.** The name you
register appears on the authorization page shown when the user signs in, so it
is what they will use to decide whether to trust the request. Identify that it
is an extension for PopClip and who made it. Example: _"Raindrop Extension for PopClip, by @author"_
**Link to your own repository.** Where the service asks for a homepage or
support URL, give the extension's own GitHub repository or web page, not this website.
**Register as a native or desktop app.** The redirect URL you need is the one
supplied as `info.redirect`, which looks like
`http://localhost:58906/callback/com.example.popclip.extension.myextension/auth`.
Some services accept a `localhost` redirect only for apps registered as native
or desktop clients, so choose that type if you are asked.
If your extension is later published in the
[PopClip Extensions Directory](https://www.popclip.app/extensions/), get in touch and we can revisit
the registration then.
## Storing the client secret
If your registration gives you a client secret, you have a small problem: there is nowhere to hide it.
Client secrets have to ship inside the extension, and an extension is source code that anyone can read.
The [`util.clarify`](https://www.popclip.app/dev/api/interfaces/Util.html#clarify) function is used here. It deciphers a JSON object that has been lightly obscured β
stringify, then Base64, then ROT13 β so the credentials at least don't sit in the source as
plaintext, where they could be scraped or indexed:
```ts
import { credentials } from "./client.json"; // { "credentials": "" }
const { client_id, client_secret } = util.clarify(credentials);
```
To be clear: this is obfuscation and not security. Anyone determined can recover the values by reversing the process. That is an accepted limitation. Client credentials are embedded in ordinary apps too, and can be extracted from them just the same. Treat an extension's client credentials as protected from casual exposure rather than secret.
To prepare an obscured blob, apply the reverse of `clarify` to your JSON:
encode it as Base64, then apply ROT13 to the result.
## Related config keys
- [`authServiceLabel`](https://www.popclip.app/dev/top-level-properties.md) β a label for the
service, used in prompts such as "Sign in to your [label] account".
Defaults to the extension's name.
- [`authKeychain`](https://www.popclip.app/dev/top-level-properties.md) β which keychain the
sign-in secret goes in: `sync` (the default) shares one sign-in across the
user's devices via iCloud Keychain; `local` keeps it on the Mac where the
user signed in, so each device signs in separately. Declare `local` where
the service issues per-device credentials, such as OAuth flows with
rotating refresh tokens or dynamic client registration.
---
# Developer Changelog
> Source: https://www.popclip.app/dev/changelog.md
Detailed notes on changes to PopClip's extensions programming interface will be
kept in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## 2026.8.1 (6221)
### Added
- Snippets: the `language` key is now optional. A code body under a `//` comment header is treated as TypeScript by default:
```js
// #popclip
// name: Minimal JS/TS Snippet
popclip.showText("hi friends!");
```
Likewise, code with a `--` comment header is AppleScript by default. For real files (`.js`/`.ts` files in packages or opened as snippets) the suffix still determines the language.
- Snippets: `module: true` is no longer required. PopClip now
[detects](https://www.popclip.app/dev/js-modules.md#module-detection) that the code is a module
if it uses `export` syntax, a `defineExtension()`
call, or a reference to `module` or `exports`. A complete module snippet is
now just:
```js
// #popclip
// name: Minimal Module Snippet
defineExtension({ action: () => popclip.showText("hi friends!") });
```
- JavaScript: `import` and `export` statements now work in plain JavaScript
code, as they already did in TypeScript. This applies everywhere JavaScript
runs: `.js` files, snippets and inline `javascript` keys.
- JavaScript: new
[popclip.runShellScript()](https://www.popclip.app/dev/api/interfaces/PopClip.html#runshellscript)
and
[popclip.runShellScriptFile()](https://www.popclip.app/dev/api/interfaces/PopClip.html#runshellscriptfile)
methods, for running a script with the `script` entitlement.
Set the interpreter, environment variables, a prefix line, stdin and positional arguments.
```js
const { stdout } = await popclip.runShellScript("print(2 ** 100)", {
interpreter: "python3",
});
```
- JavaScript: a new global template function `$` β the
[shell tag](https://www.popclip.app/dev/api/interfaces/ShellTag.html), a convenience shorthand for running shell commands from
JavaScript. It runs the template text with `/bin/zsh` in strict mode (`set -euo pipefail`), and
interpolated values are shell-escaped.
```js
// #popclip speak definition example
// name: Speak Definition
// entitlements: [script]
const word = popclip.input.text.trim();
const definition = util.getDictionaryDefinition(word) ?? "no definition";
await $`say ${definition}`;
```
- JavaScript: new
[popclip.performService()](https://www.popclip.app/dev/api/interfaces/PopClip.html#performservice)
method performs a macOS Service by name, with string or content-dictionary
input.
- JavaScript: new
[util.hash()](https://www.popclip.app/dev/api/interfaces/Util.html#hash)
method computes a plain message digest. It is the counterpart to
[util.hmac()](https://www.popclip.app/dev/api/interfaces/Util.html#hmac) and supports the same
algorithms β `sha1`, `md5`, `sha256`, `sha384`, `sha512` and `sha224` β
taking a `Uint8Array` and returning a `Uint8Array`.
```js
const digest = util.hash(Buffer.from(popclip.input.text), "sha256");
const hex = Buffer.from(digest).toString("hex");
```
- JavaScript:
[util.base64Encode()](https://www.popclip.app/dev/api/interfaces/Util.html#base64encode)
now accepts a `Uint8Array` as well as a string, so the output of
`util.hash()` or `util.hmac()` can be encoded directly. A string is encoded
as before, so existing calls are unaffected.
- JavaScript:
[util.base64Decode()](https://www.popclip.app/dev/api/interfaces/Util.html#base64decode)
can now return the decoded bytes as a `Uint8Array` rather than as a string,
for data that is not text.
```js
const bytes = util.base64Decode(encoded, { bytes: true });
```
- JavaScript: [Buffer](https://www.popclip.app/dev/api/classes/Buffer.html) now supports the
`"base64url"` encoding, both as a global and via `require("buffer")`. It
uses the standard URL-safe alphabet (`+/` β `-_`) and no padding when
encoding, and is interchangeable with `"base64"` when decoding.
```js
Buffer.from("hello?~").toString("base64url"); // aGVsbG8_fg
```
- Shell scripts: when running shell scripts, a new
[`shellMode`](https://www.popclip.app/dev/shell-script-actions.md#shell-mode) setting controls how the
script run is executed: `login` (via the user's shell as a
login shell), `nonlogin`, or `none` (no shell at all β direct execution).
For legacy compatibility, classic Shell Script actions default to `login`, but the new JavaScript methods default to `none`.
- Options: new `migrateFrom` key for `string` and `multiple` options: names a removed
option whose stored value carries over to this one if its
value is a non-empty string. Useful with `allowOther` where a multiple
option with separate free text override was used by a previous extension version.
- You can now install snippets by dragging the text onto the PopClip menu bar icon.
### Changed
- Installing an unsigned extension whose config declares the
`script` entitlement now shows the install confirmation.
- A module that mixes a default export with named exports is now a load
error. Previously, the named exports were silently ignored.
### Fixed
- JavaScript: [util.hmac()](https://www.popclip.app/dev/api/interfaces/Util.html#hmac) read from the
start of the backing buffer when passed a `Uint8Array` view with a non-zero
offset, such as one made with `subarray()`, producing the wrong result.
### Documentation
- The developer documentation has been generally reorganized to present JavaScript as the
primary language for authoring extensions, with the other action types
covered as supplementary material. Page order, navigation and examples
have been updated throughout.
- The docs are now LLM-friendly: every page has a plain
Markdown twin (add `.md` to its URL), and the whole reference is available
as [/llms.txt](https://www.popclip.app/llms.txt) and [/dev/all.md](https://www.popclip.app/dev/all.md).
- The docs name every config key in camelCase (`serviceName`,
`keyCombo`, `popclipVersion`), where previously they used lowercase with
spaces (`service name`). This is a documentation convention only: PopClip
treats all [key naming styles](https://www.popclip.app/dev/config.md#key-naming) as equivalent, so
existing configs are unaffected.
- The AppleScript and JavaScript compound keys are documented with a capital
S β `appleScript`, `appleScriptFile`, `appleScriptCall`, `javaScript`,
`javaScriptFile` β matching API function names such as
`popclip.runAppleScript()`. These spellings have always been accepted via
[key name mapping](https://www.popclip.app/dev/config.md#key-name-mapping).
- Some renamed terminology: "module-based extensions" are now simply
[module extensions](https://www.popclip.app/dev/js-modules.md), and what was called "inverted syntax"
is now a [code snippet](https://www.popclip.app/dev/snippets.md#inverted-syntax) β with a config-only
snippet now called a [config snippet](https://www.popclip.app/dev/snippets.md#config-snippets) to
distinguish the two.
- New page: [Calling external scripts](https://www.popclip.app/dev/external-scripts.md), covering the `$` shell
tag and the shell script and AppleScript functions.
- The [JavaScript API Reference](https://www.popclip.app/dev/api/) is now hosted directly on this site instead of on GitHub Pages.
## Version 2026.8 (6159)
### Added
- Files with `.js`, `.ts`, and `.yaml` extensions can now be opened
directly as extension [snippets](https://www.popclip.app/dev/snippets.md), the same as `.popcliptxt` files.
PopClip appears in the Open With menu for them, without becoming their default
application. You can also drag the files onto the PopClip menu bar icon.
- New `script` entitlement, required to use the new AppleScript-running JavaScript methods
below. Like `network`, it cannot be combined with `dynamic`.
- JavaScript: new
[popclip.runAppleScript()](https://www.popclip.app/dev/api/interfaces/PopClip.html#runapplescript)
and
[popclip.runAppleScriptFile()](https://www.popclip.app/dev/api/interfaces/PopClip.html#runapplescriptfile)
methods run an AppleScript.
```js
const result = await popclip.runAppleScript(
'on greet(a)\nreturn "hello " & a\nend greet',
{ handler: "greet", parameters: [popclip.input.text] },
);
```
- JavaScript: new
[popclip.runShortcut()](https://www.popclip.app/dev/api/interfaces/PopClip.html#runshortcut)
method runs a macOS Shortcut by name.
```js
const summary = await popclip.runShortcut("Summarize Text", {
input: popclip.input.text,
});
```
- JavaScript: new
[popclip.revealFile()](https://www.popclip.app/dev/api/interfaces/PopClip.html#revealfile)
method shows a file or folder in the Finder. Takes an absolute path β the kind
found in `popclip.input.data.paths` β with a leading `~` expanded.
```js
popclip.revealFile(popclip.input.data.paths[0]);
popclip.revealFile("~/Downloads");
```
- JavaScript: new dictionary functions on the `util` global:
[util.hasDictionaryDefinition()](https://www.popclip.app/dev/api/interfaces/Util.html#hasdictionarydefinition)
and
[util.getDictionaryDefinition()](https://www.popclip.app/dev/api/interfaces/Util.html#getdictionarydefinition),
looking words up in the same dictionaries as the macOS Dictionary app.
- JavaScript: new spelling functions on the `util` global:
[util.checkSpelling()](https://www.popclip.app/dev/api/interfaces/Util.html#checkspelling),
[util.getSpellingGuesses()](https://www.popclip.app/dev/api/interfaces/Util.html#getspellingguesses),
[util.getSpellingLanguages()](https://www.popclip.app/dev/api/interfaces/Util.html#getspellinglanguages)
and
[util.getPreferredSpellingLanguages()](https://www.popclip.app/dev/api/interfaces/Util.html#getpreferredspellinglanguages),
via the system spell checker.
```js
const guesses = util.getSpellingGuesses(popclip.input.text, {
language: "en",
limit: 5,
});
```
- Key Press actions: new `key combo target` property, choosing where PopClip
posts the key events: to the session event tap (`session`, the default, and
what PopClip has always done), to the process of the application the action is
acting on (`app`), or to the HID event tap (`hid`). See [Key Press actions](https://www.popclip.app/dev/key-press-actions.md#target).
- JavaScript:
[popclip.pressKey()](https://www.popclip.app/dev/api/interfaces/PopClip.html#presskey)
takes an options object as its third argument, with the same `target` choice:
`popclip.pressKey('command b', 0, { target: 'app' })`.
- JavaScript: `popclip.pressKey()` now returns a promise that resolves once the
press has been made. Await it when a later step depends on the press having
completed.
- JavaScript: new
[popclip.pressKeys()](https://www.popclip.app/dev/api/interfaces/PopClip.html#presskeys)
method presses a sequence of key combos, with optional waits, as one unit:
`await popclip.pressKeys(['command space', 'wait 100', 'command v'], { target: 'session' })`. Entries
take the same forms as `key combos` config entries; the same `target` option
as `pressKey` applies to the whole sequence.
- New [action properties](https://www.popclip.app/dev/actions.md#common-properties):
- `wants primary display`: the action asks to be the one centred above the pointer when the popup appears.
- `wants initial display`: for an action with a submenu, the submenu asks to be already open
when the popup appears.
- New `shell script rationale` config field: a brief explanation of why an
extension needs a Shell Script action instead of JavaScript. Ignored by the
app; the [Extensions Directory](https://www.popclip.app/extensions/submit.md#shell-script-policy) requires it
for submissions with a Shell Script action.
### Changed
- JavaScript: `popclip.pasteText()`, `popclip.pasteContent()`,
`popclip.copyText()`, `popclip.copyContent()`, `popclip.performCommand()` and
`popclip.share()` now return promises. Previously documented as returning
nothing, so this is purely additive β existing calls are unaffected. Await one
when a later step depends on it having finished:
```js
await popclip.performCommand("copy");
```
- Documentation: removed the documented claim that population functions may not
read `popclip.context.browserUrl` and `popclip.context.browserTitle`, which was incorrect.
- Documentation: added the previously undocumented `keywords` config field to
the top level properties table. It supplies
extra search words for the extension's directory listing.
## PopClip 2026.7 (5992)
### Added
- Actions can now have submenus, using the new `submenu` property. A submenu is
defined by an array of child actions, or (in JavaScript extensions) a
population function that generates the actions dynamically when the submenu
opens. The function form requires the `dynamic` entitlement. See [Submenus](https://www.popclip.app/dev/actions.md#submenus) and [Submenu functions](https://www.popclip.app/dev/js-modules.md#submenu-functions).
- New top-level config properties:
- `show as`: set the action's default presentation to `icon` or `text`.
- `auth service label`: a label identifying the service to be signed in to, shown in
the action's settings UI.
- `offers multiple instances`: controls whether the user can duplicate the action
to create multiple instances.
- New keys for option dictionaries:
- `multiline`: for `string` options, show a multi-line text field.
- `allow other`: for `multiple` options, adds an "Otherβ¦" choice allowing
the user to enter a free-text value.
- `allow none`: for `multiple` options, adds a "None" choice whose value is
the empty string.
- Option `description` fields can now contain clickable links, written either
as bare URLs or in Markdown syntax: `[label](https://example.com)`.
- URL actions: added `spaces as plus` property. If `true`, spaces in the query
are encoded as `+` instead of `%20` (some search engines, e.g. Amazon,
expect this).
- JavaScript: The [auth function](https://www.popclip.app/dev/api/interfaces/Extension.html#auth)
can now return an [AuthResult](https://www.popclip.app/dev/api/interfaces/AuthResult.html)
object `{ secret, label, expiresIn }` instead of a bare secret string. The
`label` is displayed as the signed-in account identifier (e.g. username/email), and `expiresIn` (token
lifetime in seconds) lets PopClip treat the sign-in as expired after that
time.
- JavaScript: New methods
[popclip.signInRequiredError()](https://www.popclip.app/dev/api/interfaces/PopClip.html#signinrequirederror)
and
[popclip.settingsRequiredError()](https://www.popclip.app/dev/api/interfaces/PopClip.html#settingsrequirederror)
return errors that an action can throw to send the user to the extension's
settings UI. The former also clears the stored `authsecret`, signing the
extension out.
- JavaScript: Added
[popclip.openTemplateUrl()](https://www.popclip.app/dev/api/interfaces/PopClip.html#opentemplateurl) method.
### Changed
- JavaScript: The extension's `name` and `icon` are now
[static-only properties](https://www.popclip.app/dev/js-modules.md#static-only-properties) β they can no
longer be defined dynamically by a module.
- Holding Option (β₯) when clicking a URL or search action now performs a
"verbatim" search: the query is wrapped in double quotes so the search
engine treats it as an exact phrase. (This replaces the old `alternate url`
mechanism β see below.)
- JavaScript: Updated bundled npm libraries to latest versions.
### Removed
- The `alternate url` property of URL actions has been removed. If present in
a config, it is now ignored. The Option (β₯) key now triggers the verbatim
search behaviour instead.
- JavaScript: Removed the `util.buildQueryUrl()` function. Use
`popclip.openTemplateUrl()` or construct URLs with the standard `URL` class
instead.
## PopClip 2025.9.2 (5155)
- Disable JavaScript inspectability in Safari by default. (Enable with
`defaults write com.pilotmoon.popclip EnableJSInspection -bool true`.)
- JavaScript: Updated bundled npm libraries to latest versions.
- JavaScript: Added `valibot` and `fast-plist` to bundled npm libraries.
## PopClip 2025.9 (5118)
There were no changes to the extension programming interface in this release.
## PopClip 2024.12 (4688)
### Changed
- JavaScript extensions can access `localhost` and local network addresses
(unqualified domains and `.local` domains) using `http:` connections. An
`https:` connection is still required to connect to fully qualified domains.
- TypeScript sources are now transpiled with
[sucrase](https://www.npmjs.com/package/sucrase), instead of the full
[typescript](https://www.npmjs.com/package/typescript) library. This reduces
the size of the PopClip application by nearly 1MB.
- When calling PopClip from the command line (to use the JavaScript test
harness), it now prints usage information if missing or incorrect arguments
are provided, instead of ignoring them.
### Added
- Added `oauth-1.0a` to built-in NPM modules.
- Added
[util.hmac()](https://www.popclip.app/dev/api/interfaces/Util.html#hmac)
function for HMAC calculation (useful for extensions that need to use OAuth
1.0a).
- Added
[util.getRandomValues()](https://www.popclip.app/dev/api/interfaces/Util.html#getrandomvalues)
and
[util.randomUuid()](https://www.popclip.app/dev/api/interfaces/Util.html#randomuuid).
- The
[popclip.openUrl()](https://www.popclip.app/dev/api/interfaces/PopClip.html#openurl)
method:
- now has an `activate` option to control whether the target application is
brought to the front. Default is `true`.
- can now accept a [URL](https://developer.mozilla.org/en-US/docs/Web/API/URL)
object instead of a string. When a URL object is passed, the URL is
serialized internally with `%20` instead of `+` for spaces. This solves a
[mildly annoying pain-point](https://github.com/pilotmoon/PopClip-Extensions/blob/39e72253906bb5c09f523d1239b24e297fa323e7/source/Craft.popclipext/Config.ts#L43)
for extensions that use URL objects to construct URLs.
- The
[popclip.copyText()](https://www.popclip.app/dev/api/interfaces/PopClip.html#copytext)
method now has a `notify` option to control whether the "Copied" indicator is
shown when the text is copied. Default is `true`.
- JavaScript API now has a global `TextEncoder` class which acts as a shim
approximating the standard Web API class. This improves compatibility with
some NPM modules.
- Added `isurl` [requirements](https://www.popclip.app/dev/actions.md#the-requirements-array) key. This
requires that the selected text is a single URL (as opposed to text
_containing_ a url, which the existing `url` key specifies). This makes the
`popclip.input.isUrl` property added in version 2024.5 available to
non-JavaScript extensions.
## PopClip 2024.5.2 (4615)
### Changed
- The [JavaScript test harness](https://www.popclip.app/dev/js-environment.md#test-harness) has been
improved.
- Now loads a module from a file, and optionally invokes a named function in
that module.
- Now supports TypeScript files.
- Improved the tool output format.
- Changed the command name from `runjs` to `run`.
### Added
- The
[`popclip.showText`](https://www.popclip.app/dev/api/interfaces/PopClip.html#showtext)
method now takes an optional `style` option which can be either `compact` or
`large`. The default style is `compact`, which is the same as the previous
behavior. The `large` style is a new style that shows the text full-screen in
a "large type" display.
## PopClip 2024.5 (4578)
### Added
- JavaScript extensions can now be debugged using the Safari Web Inspector. This
lets you inspect and debug your code while it is running inside PopClip. The
inspector can be accessed from Safari's Develop menu, which must be enabled
with the "Show features for web developers" in Safari's Advanced settings.
- Added new `popclip.input.isUrl` property that indicates whether the input text
is a single a web URL (as opposed to text _containing_ one or more URLs). This
is useful for extensions that have different actions depending on whether the
input is a URL or not.
- Added `popclip.share()` function to send text to other apps using macOS
Sharing Extensions. I will soon update the official Notes, Messages and
Reading List extension to use this feature.
### Fixed
- Extension requirements keys `path`, `url`, `email` now properly propagate
their matched text as the input to Service actions.
## PopClip 2024.3 (4508)
### Added
- The text string format can now be used for all kinds of icons, e.g.
`square filled symbol:flame`.
- Added `strike` modifier for icon strings, which overlays a strike-out effect.
- Added `flip_x`, `flip_y`, `move_x`, `move_y`, `scale` and `rotate` modifiers
for icon strings.
- Emoji text icons now render in color.
- In the JavaScript environment, `popclip.input.regexResult` is an array
containing the full result of the regex match, including any capture groups.
Unlike previous PopClip versions, this array is now always available
regardless of whether the regex was specified in the static config (as an ICU
regex string) or in a module (as a JavaScript RegExp object).
- Added option type `secret` for storing a string in the macOS Keychain instead
of in the PopClip preferences file. This is useful for extensions that save
sensitive data such as a password or API key.
### Changed
- Config.json files are now parsed with a JSON parser instead of a YAML parser.
- All extensions must now have a static config defining at least `name`. An
override name may also be specified in the dynamic (JS module) config.
- If an extension defines an `identifier`, it must be in the static config.
Defining an identifier in dynamic config is now a load error.
- Extension identifiers now must begin and end with a letter or number.
Separators (period, hypen or underscore) are allowed in the middle but not
multiple in a row.
- Renamed `flip horizontal` and `flip vertical` to `flip x` and `flip y`
respectively.
- Option labels are now optional. If omitted, the option `identifier` is used as
the label.
- Identifier prefix `app.popclip.` is now reserved for signed extensions only.
- When an action specifies both a `regex` and a `url`, `path` or `email`
requirement, the requirement is now applied first, and then the regex is
applied to the output of the requirement.
- Unsigned extensions no longer purge all existing options upon installation.
Instead, only options of type `secret` are purged, and only if an unsigned
extension replaces a signed extension with the same identifier.
- _For Setapp edition only:_ the storage location for Extensions has changed
from `~/Library/Application Support/com.pilotmoon.popclip-setapp/Extensions`
to `~/Library/Application Support/PopClip/Extensions`.
- Updated all embedded NPM modules to latest versions.
### Documentation
- Updated all documentation to reflect the changes in this release. In
particular, the [Icons](https://www.popclip.app/dev/icons.md) page has been fully rewritten to reflect
the new icon string features.
## PopClip 2023.9 (4225)
### Added
- TypeScript can now be used as the source language for JavaScript actions and
module extensions. This is done by specifying a file with the `.ts`
extension in the `javascript file` or `module` field. For snippets, specify
`typescript` in the `language` field.
- ~~PopClip ships with a TypeScript type definitions file, `popclip.d.ts`,
inside the app bundle.~~ _This has now been removed as of v2024.5. Instead,
use the
[@popclip/types NPM package](https://www.popclip.app/dev/js-environment.md#popclip-types-package)._
You can configure your dev envionment to reference this to aid in developing
your own extensions.
- URL actions can now specify an optional `alternate url`, invoked by holding
Option (β₯).
- URL actions now have an optional `clean query` flag to clean up newlines and
whitespace in the text before inserting into the URL.
### Changed
- Updated the versions of several of the built in NPM modules.
- Key Press actions no longer automatically wait 100ms between keypresses.
Instead, use `wait ` to add a delay if needed. See
[Wait between keypresses](https://www.popclip.app/dev/key-press-actions.md#wait-between-key-presses).
- The Unsigned Extension warning is now only shown for extensions with Shell
Script actions, AppleScript actions, or JavaScript actions with entitlements.
- In URL actions, leading and trailing whitespace and newlines are now always
trimmed before URL-encoding.
### Documentation
- Amended documentation in various places to reflect the new TypeScript support.
- Added [Using `require()`](https://www.popclip.app/dev/js-environment.md#using-require) to the JavaScript
environment documentation.
- Added [Abbreviated forms](https://www.popclip.app/dev/js-modules.md#abbreviated-forms) to the
module-based extensions documentation.
## Documentation Update, 2023-08-30
- The developer documentation moved from GitHub to
.
- The previous single README was split into multiple pages.
- All parts revised and updated; more examples added.
- Added brand new documentation for
[Module extensions](https://www.popclip.app/dev/js-modules.md).
## PopClip 2023.7 (4151)
There were no changes to the extension programming interface in this release.
## PopClip 2022.12 (4069)
### Added
- Extensions and snippets can now use icons from
[Iconify](https://icon-sets.iconify.design/), which provides over 100,000 open
source icons. Specify them like this: `iconify:ph:rainbow-bold`.
- Snippets can now be added as a comment header to any text file, with the
result that the entire file becomes installable as a JavaScript, Apple Script
or shell script extension. (See
[Header Snippets](https://github.com/pilotmoon/PopClip-Extensions#header-snippets)
in the main Readme.)
- PopClip will install an extension from a `.popcliptxt` file. This is basically
a snippet in a text file.
- Added `shell script` field for specifying a shell script as a literal string.
This allows shell scripts to be put directly in snippets.
- Added optional `stdin` field for shell scripts, to allow passing a value to
the script via stdin.
- Allows the AppleScript source to be specified as `applescript` string or
`applescript file` when calling a named handler.
- A `key combo` string can now specify `numpad` as a modifier, to denote
pressing a key on the numeric keypad.
- Added options for icon drawing including `flip horizontal`, `flip vertical`
and `preserve aspect`.
- Added built-in [core-js](https://github.com/zloirock/core-js) shim inside
PopClip to allow modern JavaScript features on all target platforms.
### Changed
- Increased maximum selectable snippet length from 1000 to 5000 characters.
- The `script interpreter` field has been renamed to `interpreter`.
- Shell script files are no longer executed with `/bin/sh` by default. An
interpreter must be explicitly specified.
- The `preserve image color` field has been renamed to `preserve color`.
- The `parameters` field in the `applescript call` dictionary has been renamed
to `params`.
- Icons are now drawn in a square canvas with uniform height and width, unless
the new `preserve aspect` flag is set.
- PopClip now enforces that the extension identifier may contain only A-Z, a-z,
0-9, period (.), and hyphen (-).
- Updated the versions of several of the built in NPM modules.
- When an action combines both `requirements` and `regex`, the requirements are
now applied first, and then the regex is applied to the result.
### Fixed
- The snippet detector now correctly recognizes snippets written in YAML flow
syntax (i.e. with braces `{}`), as well as JSON syntax (since valid JSON is
valid YAML).
- Fixed never-ending spinner with some Shortcuts actions.
## PopClip 2022.5 (3895)
### Added
- Added the ability to execute pre-compiled AppleScript `.scpt` files, and to
invoke handlers within them with parameters.
- Key Press extensions can now take an array of key combos, to press a sequence
of keys.
- Extended the key code string format to simplify specifying non-character keys,
and raw key codes. For example: `command tab`.
- Brought back the `restore pasteboard` field for actions.
- Added a 'test harness' mode to PopClip, for testing your JavaScript code in
the PopClip environment. Run as:
`/Application/PopClip.app/Contents/MacOS/PopClip runjs `
- Additions to the JavaScript programming environment:
- Added RTF processing features (via RichText class object).
- Added locale information to the `util` object.
- Added a promise-based global function `sleep` (e.g. `await sleep(1000)`).
- Supports the new key combo string format in the `popclip.pressKey()` method.
- Improvements to the XMLHttpRequest implementation, including adding `Blob`
and `ArrayBuffer` support.
### Changed
- For Key Press extensions, there is now a 100ms delay after each key press.
- Updated the versions of some of the bundled npm libraries for JavaScript
extensions. (There should be no breaking changes.)
## PopClip 2021.11 (3785)
### Added
- PopClip will now load either JSON (`Config.json`) or YAML (`Config.yaml`) as
an alternative to an XML Property List (`Config.plist`) for the extension
config file. The same field names are used in each of the three formats, and
they each define the same logical structure. The choice of format is just a
matter of which you prefer. (I'm currently leaning towards YAML for the best
readability.)
- Field names for use in the Config files are now defined in a spaced lowercase
form such as `applescript file`. However, PopClip will accept field names in
all common forms including the original "spaced capitalized" form (e.g.
`AppleScript File`) and camel case (e.g. `appleScriptFile`).
- The `URL` field for Search extensions will now accept `***` in addition to
`{popclip text}` as the placeholder.
- The text-based icon format has a new "magnifying glass" style, intended for
search extensions.
- The text-base icon specification format has changed since 2021.10 (see
README).
- The `Script Interpreter` can now be specified as a bare executable name (e.g.
`perl`), and PopClip will locate the tool in the `PATH` of the user's default
shell.
- Added a new field called `AppleScript`, allowing AppleScripts to be specified
as a verbatim text string in the config file (rather than as a separate file
via `AppleScript File`).
- Allow key combos to be specified as a text string, for example
`command option T`.
- Added an `emails` requirement to specify one or more email addresses.
- Added `POPCLIP_EMAILS` and `POPCLIP_PATHS` fields.
- Added Shortcut action type, to run a named Shortcut on macOS 12.0.
- Added JavaScript action type.
### Changed
- Removed the `Extension ...` and `Option ...` prefixes from field names (e.g.
`Extension Name` is now just `Name`). The old names will continue to work.
- The extension's `Identifier` and/or `Name` are now optional. If either is
omitted, popclip will generate one from the .popclipext package name.
- An action's `Title` is now optional. If omitted, the action takes the
extension's name as its title.
- An action's `Icon` is now optional. If omitted, the action takes the
extension's icon (if any) as its icon.
- The `Actions` array is now optional. An extension with a single action may now
be specified at the top level of the config file, without a separate action
dictionary.
- Renamed `Blocked Apps` to `Excluded Apps`, `Regular Expression` to `Regex`,
`Pass HTML` to `Capture HTML`, `Required Software Version` to
`PopClip Version`, and `Required OS Version` to `MacOS Version`. The old names
will continue to work.
- Renamed the requirements `httpurl` and `httpurls` to `url` and `urls`.
- When URLs without a scheme prefix are detected in text, PopClip now defaults
to https instead of http.
- Changed the text icon specification format. (Docs todo.)
### Note
My goal with these recent changes is to drastically lower the barrier of entry
for users creating their own extensions. The changes mean that extensions can
now be defined with fewer fields and less structure.
As the cherry on top of that, PopClip now has a new built in action for
installing extensions from selected text. It activates when you select text
starting with `# popclip` followed by a YAML extension definition. The extension
must be a URL, Service or Key Press extension. Here is an example:
```yaml
# popclip extension to search Emojipedia
name: Emojipedia
icon: search filled E
url: https://emojipedia.org/search/?q=***
```
This means simple extensions can be shared simply by plain text in emails, on
websites etc. Extensions shared this way don't also show an unsigned extension
warning.
There is limit of 1000 characters for this. (If you are doing anything requiring
more than that, you should probably be creating a packaged extension.)
## PopClip 2021.10 (3543)
### Added
- Executable shell scripts now have the user's `PATH` set in the script
variables.
- Brought back `Preserve Image Color`.
### Changed
- The `App` specifier can now be set on individual actions as well as at the
root level.
### Deprecated
- ~~The `Script Interpreter` field is deprecated.~~ _Reverted - see later
changes to this field._
## PopClip 2021.9 (3510)
### Added
- PopClip now supports SVG image files as well as allowing you to specify an
image as a SF Symbols identifier or to generate an icon from up to 3 letters
of text.
- PopClip now provides an HTML and a Markdown version for _all_ text selections,
when `Pass HTML` is set. When the content is not HTML backed, the HTML and
Markdown is generated from the selected RTF or plain text content.
- Added `POPCLIP_MARKDOWN` field to contain the markdownified HTML.
- Added `POPCLIP_ACTION_IDENTIFIER` field. This is passed to the action script
allowing you to use the same script for multiple actions.
- Added `POPCLIP_FULL_TEXT` field. This is always contains the full selected
text in cases where `POPCLIP_TEXT` only contains the part of text matched by
regex or requirement.
- Added `Option Value Labels` array so that the options list can show a display
name different to option string value itself.
- Added `Option Description` field to add more information in the UI about an
option.
- Shell scripts with the executable bit set can optionally specify their
interpreter with a hashbang, instead of the `Script Interpreter` field.
### Removed
- Removed the `html` requirement since all selections now come with HTML (as
above).
- ~~Removed the `Preserve Image Color` option. PopClip now always converts the
icon to monochrome.~~ _Restored in 2021.10._
- ~~Removed the `Restore Pasteboard` option. PopClip now always restores the
pasteboard, unless using the `*-result` keys.~~
- Removed the `Long Running` option. All extensions are now assumed to be
potentially long running.
### Changed
- The `POPCLIP_HTML` field is now sanitized to remove CSS, potentially unsafe
tags, and to fix invalid markup. The unsanitized HTML is still available in a
new field `POPCLIP_RAW_HTML`.
- Renamed the `Image File` and `Extension Image File` fields to `Icon` and
`Extension Icon`, respectively. (The old names will also still work but are no
longer documented.)
- Added `App` dictionary field to specify a single app (since it turns out we
hardly ever need to specify more than one app). (`Apps` array will still work
but is no longer documented.)
- The error checking when loading an extension is more robust, so errors such as
incorrect field types will now be caught. And you'll get an more specific
message about what the problem is.
One more thing... there is also a brand new extension format based on
JavaScript. Documentation still "to-do", watch this space!