Press cmd+;
(or ctrl+;
on Windows) to open the Script Kit prompt. Search for the script you want to run and press enter
to run it.
You can also open the prompt from the menu bar and select "Open Prompt."
With the prompt open, run a script with cmd+enter
(ctrl+enter
on Windows) to launch the script in debug mode. An inspector will appear alongside the script, allowing you to inspect current values and step through it line by line. Use the debugger
statement anywhere in your script to create a breakpoint where your script will pause. (When running the script normally, the debugger
statement is simply ignored.)
let response = await get("https://api.github.com/repos/johnlindquist/kit")
// The inspector will pause your script so you can examine the value of "response""
debugger
Keep your scripts in ~/.kenv/scripts
("kenv" stands for "Kit Environment").
With the Kit.app prompt open, start typing the name of the script you want to create, then hit `enter`` when prompted to create a script. Your editor will launch with the newly created script focused.
Kit.app continuously watches the ~/.kenv/scripts
directory for changes. Creating, deleting, or modifying scripts will be automatically reflected in the Kit.app prompt.
The file name of the script is lowercased and dashed like hello-world.js
by convention. You can add an addionational //Name: Hello World
to the top of your script for a more friendly name to appear when searching in the prompt.
//Name: Hello World
When creating a script with the prompt, you can type the Friendly Name
of the script and Kit.app will automatically create the dashed file name for you.
Use the // Shortcut
metadata to add a global keyboard shortcut to any script
// Shortcut: cmd shift j
import "@johnlindquist/kit"
say(`You pressed command shift j`)
// Shortcut: opt i
import "@johnlindquist/kit"
say(`You pressed option i`)
The simplest form of input you can accept from a user is an arg()
// Name: Input Text
import "@johnlindquist/kit"
let name = await arg("Enter your name")
await div(md(`Hello, ${name}`))
// Name: Select From a List
import "@johnlindquist/kit"
let fruit = await arg("Pick a fruit", [
"Apple",
"Banana",
"Cherry",
])
await div(md(`You selected ${fruit}`))
// Name: Select From a List of Objects
import "@johnlindquist/kit"
let { size, weight } = await arg("Select a Fruit", [
{
name: "Apple",
description: "A shiny red fruit",
// add any properties to "value"
value: {
size: "small",
weight: 1,
},
},
{
name: "Banana",
description: "A long yellow fruit",
value: {
size: "medium",
weight: 2,
},
},
])
await div(
md(
`You selected a fruit with size: ${size} and weight: ${weight}`
)
)
// Name: Select From a Dynamic List
import "@johnlindquist/kit"
let { size, weight } = await arg("Select a Fruit", [
{
name: "Apple",
description: "A shiny red fruit",
// add any properties to "value"
value: {
size: "small",
weight: 1,
},
},
{
name: "Banana",
description: "A long yellow fruit",
value: {
size: "medium",
weight: 2,
},
},
])
await div(
md(
`You selected a fruit with size: ${size} and weight: ${weight}`
)
)
// Name: Display a Preview When Focusing a Choice
import "@johnlindquist/kit"
let heights = [320, 480, 640]
let choices = heights.map(h => {
return {
name: `Kitten height: ${h}`,
preview: () =>
`<img class="w-full" src="http://placekitten.com/640/${h}">`,
value: h,
}
})
let height = await arg("Select a Kitten", choices)
await div(md(`You selected ${height}`))
If the second argument to arg()
is a string, it will be displayed beneath the input as HTML.
// Just a string
await arg(
"Select a fruit",
md(`I recommend typing "Apple"`) // "md" converts strings to HTML
)
A function that returns a string will also be displayed beneath the input as HTML. You can use the input
text in the function to create dynamic HTML.
// A function, takes typed "input", returns string
await arg("Select a fruit", input =>
md(`You typed "${input}"`)
)
// An async function, takes typed "input", returns string
// `hightlight` requires "async" takes markdown, applies code highlighting
await arg(
"Select a fruit",
async input =>
await highlight(`
~~~js
await arg("${input}")
~~~
`)
)
// Dynamic choices, static preview
await arg(
{
preview: async () =>
await highlight(`
## This is just information
<!-- value: https://github.com/johnlindquist/kit/blob/main/GUIDE.md -->
Usually to help you make a choice
Just type some text to see the choices update
`),
},
async input => {
return Array.from({ length: 10 }).map(
(_, i) => `${input} ${i}`
)
}
)
Use await div('')
to display HTML.
// Name: Display HTML
import "@johnlindquist/kit"
await div(`<h1>Hello World</h1>`)
The second argument of div
allows you to add tailwind classes to the container of your html. For example, p-5
will add a padding: 1.25rem;
to the container.
await div(`<h1>Hi</h1>`, `p-5`)
Script Kit bundles Tailwind CSS.
// Name: Display HTML with CSS
import "@johnlindquist/kit"
await div(
`<h1 class="p-10 text-4xl text-center">Hello World</h1>`
)
The md()
function will convert Markdown to HTML into HTML that you can pass into div. It will also add the default Tailwind styles so you won't have to think about formatting.
// Name: Display Markdown
import "@johnlindquist/kit"
let html = md(`# Hello World`)
await div(html)
Use the widget
method to spawn a new, persisting window that is disconnected from the script.
await widget(`
<div class="bg-black text-white h-screen p-5">
Hello there!
<div>
`)
To add an options menu to your choices, you must provide a flags
object. If one of the keyboard shortcuts are hit, or the user selects the option, then the flag
global will have the matching key from your flags set to true
:
let urls = [
"https://scriptkit.com",
"https://egghead.io",
"https://johnlindquist.com",
]
let flags = {
open: {
name: "Open",
shortcut: "cmd+o",
},
copy: {
name: "Copy",
shortcut: "cmd+c",
},
}
let url = await arg(
{ placeholder: `Press 'right' to see menu`, flags },
urls
)
if (flag?.open) {
$`open ${url}`
} else if (flag?.copy) {
copy(url)
} else {
console.log(url)
}
Using the same script above, In the terminal, you would pass an open flag like so:
my-sites --open
The db
helpers reads/writes to json files in the ~/.kenv/db
directory. It's meant as a simple wrapper around common json operations.
// Menu: Database Read/Write Example
// Description: Add/remove items from a list of fruit
let fruitDb = await db(["apple", "banana", "orange"])
// This will keep prompting until you hit Escape
while (true) {
let fruitToAdd = await arg(
{
placeholder: "Add a fruit",
//allows to submit input not in the list
strict: false,
},
fruitDb.items
)
fruitDb.items.push(fruitToAdd)
await fruitDb.write()
let fruitToDelete = await arg(
"Delete a fruit",
fruitDb.items
)
fruitDb.items = fruitDb.items.filter(
fruit => fruit !== fruitToDelete
)
await fruitDb.write()
}
This db helper can also be used as a simple Key/value Store like this:
// Menu: Database Read/Write Example 2
// Description: Use 'db' helper as Key/Value Store
// Open the json file with the same name as the script file, the data in the param is the default,
// which will be used when the db file is opened the first time
const scriptDB = await db({hello: 'World'});
// Note: This db read here should only make sure the db object has the latest content from disk.
// It may be unnecessary directly after opening the db object.
await scriptDB.read();
if (scriptDB.data.hello === 'World') {
// change value in your db
scriptDB.data.hello = 'Bob';
} else {
// change value back in your db
scriptDB.data.hello = 'World';
}
await scriptDB.write();
The // Watch
metadata enables you to watch for changes to a file on your system.
// Name: Speak File
// Watch: ~/speak.txt
import "@johnlindquist/kit"
let speakPath = home("speak.txt")
try {
let content = await readFile(speakPath, "utf-8")
if (content.length < 60) {
// We don't want `say` to run too long 😅
say(content)
}
} catch (error) {
log(error)
}
The // Watch
metadata uses Chokidar under the hood, so it supports the same glob patterns. Please use cautiously, as this can cause a lot of scripts to run at once.
// Name: Download Log
// Watch: ~/Downloads/*
import "@johnlindquist/kit"
// These are optional and automatically set by the watcher
let filePath = await arg()
let event = await arg()
if (event === "add") {
await appendFile(home("download.log"), filePath + "\n")
}
Script Kit bundles zx as the global $
Here's an example from their docs (make sure to cd
to the proper dir)
await $`cat package.json | grep name`
let branch = await $`git branch --show-current`
await $`dep deploy --branch=${branch}`
await Promise.all([
$`sleep 1; echo 1`,
$`sleep 2; echo 2`,
$`sleep 3; echo 3`,
])
let name = "foo bar"
await $`mkdir /tmp/${name}`
The get
, post
, put
, and del
methods use the axios API
// Name: Get Example
import "@johnlindquist/kit"
let response = await get(
"https://scriptkit.com/api/get-example"
)
await div(md(response.data.message))
// Name: Post Example
import "@johnlindquist/kit"
let response = await post(
"https://scriptkit.com/api/post-example",
{
name: await arg("Enter your name"),
}
)
await div(md(response.data.message))
Use download
to download a file from a url:
// Name: Download a File
import "@johnlindquist/kit"
let url = "https://www.scriptkit.com/assets/logo.png"
let buffer = await download(url)
let fileName = path.basename(url)
let filePath = home(fileName)
await writeFile(filePath, buffer)
You can use readFile
to read a text file from your system:
// Name: Read a Text File
import "@johnlindquist/kit"
// Download a readme for the sake of the example
let fileUrl = `https://raw.githubusercontent.com/johnlindquist/kit/main/README.md`
let filePath = home("README.md")
let buffer = await download(fileUrl)
await writeFile(filePath, buffer)
// Read the file
let contents = await readFile(filePath, "utf-8")
await editor(contents)
// Name: Create a Text File
import "@johnlindquist/kit"
let filePath = await path() // Select a path that doesn't exist
let exists = await pathExists(filePath)
if (!exists) {
await writeFile(filePath, "Hello world")
} else {
await div(md(`${filePath} already exists...`))
}
// Name: Update a Text File
import "@johnlindquist/kit"
let filePath = home(`my-notes.md`)
// `ensureReadFile` will create the file with the content
// if it doesn't exist
let content = await ensureReadFile(filePath, "Hello world")
await editor({
value: content,
onInput: _.debounce(async input => {
await writeFile(filePath, input)
}, 200),
})
Use cron syntax to run scripts on a schedule. The following example will show a notification to stand up and stretch every 15 minutes.
// Name: Stand Up and Stretch
// Schedule: */15 * * * *
import "@johnlindquist/kit"
notify(`Stand up and stretch`)
Crontab.guru is a great utility to help generate and understand cron syntax.
The env
helper will read environment variables from ~/.kenv/.env. If the variable doesn't exist, it will prompt you to create it.
// Name: Env Example
import "@johnlindquist/kit"
let KEY = await env("MY_KEY")
await div(md(`You loaded ${KEY} from ~/.kenv/.env`))
If you pass a function as the second argument to env
, it will only be called if the variable doesn't exist.
This allows you to set Enviroment Variables from a list, an API, or any other data source.
// Name: Choose an Environment Variable
import "@johnlindquist/kit"
let MY_API_USER = await env("MY_API_USER", async () => {
return await arg("Select a user for your API", [
"John",
"Mindy",
"Joy",
])
})
await div(
md(
`You selected ${MY_API_USER}. Running this script again will remember your choice`
)
)
The Script Kit main window also includes many other share options:
- Share as Gist cmd+g: Creates as Gist of the selected script, then copies the URL to the clipboard
- Share as Link opt+s: Creates a private installable kit://link to the selected script, then copies the URL to the clipboard. These links are very long as they encode the entire script into the URL.
- Share as URL opt+u: Creates a Gist of the selected script, then copies an installable public URL to the clipboard
- Share as Markdown cmd+m: Copies the selected script as a Markdown snippet to the clipboard
Featured scripts are displayed in:
- The
Community
tab of the Script Kit main window - On the Community Scripts page
To get featured, post your script to the Script Kit Github discussions Share page. With a script focused in the Script Kit main window, you can press right or cmd+k to bring up a share menu which will automatically walk you through creating a shareable post for the script.
As a shortcut, hit cmd+s with a script selected to automatically run the "Share as Discussion" process.
// Name: Play with Data in Chrome DevTools
import "@johnlindquist/kit"
// Will open a standalone Chrome DevTools window
// The object passed in will be displayed
// You can access the object using `x`, e.g., `x.message` will be `Hello world`
dev({
message: "Hello world",
})
A shortcode allows you quickly run a script without needing to search for it.
To trigger a // Shortcode
, type the string of characters from the main menu, then hit spacebar
. In this example, you would type oi
then spacebar
to run this script:
// Shortcode: oi
import "@johnlindquist/kit"
say(`You pressed option i`)
A common pattern from Terminal is to quickly submit a script from a hint. Using a bracket around a single character will submit that character when pressed.
import "@johnlindquist/kit"
let value = await arg({
placeholder: "Continue?",
hint: `Another [y]/[n]`,
})
if (value === "y") {
say(`You pressed y`)
} else {
say(`You pressed n`)
}
If you need to provide a little more information to the user, use a choice instead of a hint. This allows you to provide a full value that will be submitted instead of just the single letter.
import "@johnlindquist/kit"
let value = await arg("Select a food", [
{
name: "[a]pple",
value: "apple",
},
{
name: "[b]anana",
value: "banana",
},
{
name: "[c]heese",
value: "cheese",
},
])
await div(md(value))
Are you a fan of one of these amazing tools?
We love all these tools! So we made sure the scripts you create in Script Kit can be invoked by them too:
If you have a script named center-app
, then you can paste the following snippet into the "scripts" section of any of these tools.
~/.kit/kar center-app
kar
is an executable that takes the script name and sends it to Kit.app to run.
It's named
kar
because we're HUGE fans of karabiner and using "kit kar" as a transport for scripts into the app makes us giggle 😇
Any arguments you pass to the script will also be sent along. So if you want to run center-app
with a padding of 50
:
~/.kit/kar center-app 50
// Name: Select a Path
import "@johnlindquist/kit"
let filePath = await path()
await div(md(`You selected ${filePath}`))
// Name: Select a Path with Options
import "@johnlindquist/kit"
await path({
hint: `Select a path containing JS files`,
onlyDirs: true,
onChoiceFocus: async (input, { focused }) => {
let focusedPath = focused.value
try {
let files = await readdir(focusedPath)
let hasJS = files.find(f => f.endsWith(".js"))
setPreview(
md(
`${
hasJS ? "✅ Found" : "🔴 Didn't find"
} JS files`
)
)
} catch (error) {
log(error)
}
},
})
// Name: Select from Finder Prompt
import "@johnlindquist/kit"
let filePath = await selectFile()
let folderPath = await selectFolder()
await div(md(`You selected ${filePath} and ${folderPath}`))
// Name: Run Commands in the Terminal
import "@johnlindquist/kit"
await term({
//defaults to home dir
cwd: `~/.kenv/scripts`,
command: `ls`,
})
The shell defaults to
zsh
. You can change your shell by setting theKIT_SHELL
environment variable in the ~/kenv/.env, but most of the testing has been done withzsh
.
Script Kit ships with a built-in version of the Monaco editor. Use await editor()
to switch to the editor prompt.
// Name: Editor Example
import "@johnlindquist/kit"
let result = await editor()
await div(md(result))
// Name: Load Text Into the Editor
import "@johnlindquist/kit"
let { data } = await get(
`https://raw.githubusercontent.com/johnlindquist/kit/main/README.md`
)
let result = await editor({
value: data,
// Supports "css", "js", "ts", "md", "properties". "md" is default. More language support coming in future releases.
language: "md",
footer: `Hit cmd+s to continue...`,
})
await div(md(result))
This is similar to VS Code's "Add
code
to path"
You can run the kit
CLI from your terminal with
~/.kit/bin/kit
but this option will allow you run the CLI with:
kit
If you're familiar with adding to your
.zshrc
, just add~/.kit/bin
to your PATH.
The kit
CLI will allow you to run, edit, etc scripts from your terminal.
Kit.app requires accessibility permission for the following reasons:
- Watch user input to trigger Snippets and Clipboard History
- Send keystrokes to trigger for
setSelectedText
,getSelectedText
,keyboard.type
and others - In the future, recording Macros, mouse actions, and more
❗️ You must quit Kit.app and re-open it for changes to take effect.
Some scenarios require setInterval
or other "live data" utils. This means you can't use await
on the arg/div/textarea/etc because await
prevents the script from continuing on to start the setInterval
.
Use the Promise then
on arg/div/textarea/etc to allow the script to continue to run to the setInterval
. Inside of the then
callback, you will have to clear the interval for your script to continue/complete:
let intervalId = 0
div(md(`Click a value`)).then(async value => {
clearInterval(intervalId)
await div(md(value))
})
intervalId = setInterval(() => {
let value = Math.random()
setPanel(
md(`
[${value}](submit:${value})
`)
)
}, 1000)
strict
is enabled by default and it forces the user to pick an item from the list, preventing them from entering their own text.
When you disabled strict
, if you type something that eliminates the entire list, then hit Enter, the string from the input will be passed back.
Note: If the list values are Objects and the user inputs a String, you will need to handle either type being returned
// If the list is completely filtered, hitting enter does nothing.
let fruit = await arg(`You can only pick one`, [
`Apple`,
`Banana`,
`Orange`,
])
// If the list is completely filtered, hitting enter sends whatever
// is currently in the input.
let fruitOrInput = await arg(
{
placeholder: `Pick a fruit or type anything`,
strict: false,
},
[`Apple`, `Banana`, `Orange`]
)
await textarea(`${fruit} and ${fruitOrInput}`)
A quick key allows you to bind a single key to submit a prompt.
You can add quick keys inside the "hint" if you don't want to bother with choices:
//Type "y" or "n"
let confirm = await arg({
placeholder: "Eat a taco?",
hint: `[y]es/[n]o`,
})
console.log(confirm) //"y" or "n"
Otherwise, add the quick keys in the name
of the choices and it will return the quick key:
// Type "a", "b", or "g"
let fruit = await arg(`Pick one`, [
`An [a]pple`,
`A [b]anana`,
`a [g]rape`,
])
console.log(fruit) //"a", "b", or "g"
You can add a value, then typing the quick key will return the value:
// Type "c" or "a"
let vegetable = await arg("Pick a veggie", [
{ name: "[C]elery", value: "Celery" },
{ name: "C[a]rrot", value: "Carrot" },
])
console.log(vegetable) //"Celery" or "Carrot"
You can control the size/position of each show
window you create, but you'll need some info from the current screen (especially with a multi-monitor setup!) to be able to position the window where you want it:
let width = 480
let height = 320
let { workArea } = await getActiveScreen()
let { x, y, width: workAreaWidth } = workArea
await widget(
md(`
# I'm in the top right of the current screen!
<div class="flex justify-center text-9xl">
😘
</div>
`),
{
width,
height,
x: x + workAreaWidth - width,
y: y,
}
)
When you pass a function as the second argument of arg
, you can take the current input
and return a string. Kit.app will then render the results as HTML. The simplest example looks like this:
await arg("Start typing", input => input)
If you want to make it look a bit nicer, you can wrap the output with some HTML:
await arg(
"Type something",
input =>
`<div class="text-3xl flex justify-center items-center p-5">
${input || `Waiting for input`}
</div>`
)
Growing on the example above, here's a Celsius to Fahrenheit converter:
let cToF = celsius => {
return (celsius * 9) / 5 + 32
}
await arg(
"Enter degress in celsius",
input =>
`<div class="text-3xl flex justify-center items-center p-5">
${input ? cToF(input) + "f" : `Waiting for input`}
</div>`
)
We're developers. We clone project templates from github. degit is available on the global scope for exactly this scenario.
let projectName = await arg("Name your project")
let targetDir = home("projects", projectName)
await degit(`https://github.com/sveltejs/template`).clone(
targetDir
)
edit(targetDir)
When you use console.log()
in a script, it writes the log out to a relative directory.
For example:
~/.kenv/scripts/my-script.js
will write logs to:
~/.kenv/logs/my-script.log
You can view the live output of a log in your terminal with:
tail -f ~/.kenv/logs/my-script.log
If you want to watch the main log, you can use:
tail -f ~/.kit/logs/kit.log
You can save any webpage as a PDF.
// Name: Save news as PDF
import "@johnlindquist/kit"
const pdfResults = await getWebpageAsPdf('https://legiblenews.com');
await writeFile(home('news.pdf'), pdfResults);
You can take a screenshot of any webpage.
// Name: Take screenshot of news webpage
import "@johnlindquist/kit"
const screenshotResults = await getScreenshotFromWebpage('https://legiblenews.com', {
screenshotOptions: { fullPage: true },
});
await writeFile(home('news.png'), screenshotResults);
## Scrape content from a webpage
<!-- value: https://github.com/johnlindquist/kit/blob/main/GUIDE.md -->
You can scrape content from a webpage. The first time you run this, you will be prompted to install Playwright.
```js
// Name: Scrape John's pinned Github repositories
import "@johnlindquist/kit"
const items = await scrapeSelector(
'https://github.com/johnlindquist',
// CSS Selector to target elements
'.pinned-item-list-item-content > div > a',
// [Optional] function to transform the elements, if omitted then `element.innerText` is returned
(element) => ({
title: element.innerText,
link: element.href,
}),
// [Optional] options
{
headless: false,
timeout: 60000,
}
);
let filePath = home(`pinned-repos.md`)
// `ensureReadFile` will create the file with the content
// if it doesn't exist
let content = await ensureReadFile(filePath, items.map(({title, link}) => `- [${title}](${link})`).join('\n'))
This Guide constantly evolving. If you're missing something, suggest an edit to the docs or open an issue on GitHub.
Hit Enter to download the latest docs.