Script Kit online on Stackblitz โšก๏ธ

I spent last week getting Script Kit running "in browser" to emulate the terminal experience over on Stackblitz. Here's a quick demo:

https://stackblitz.com/edit/node-rnrhra?file=scripts%2Frepos-to-markdown.js

The plan is to use this to host interactive demos for the guide/docs. I'd appreciate if you could play around with it a bit and see if I missed anything.

Discuss Post

TypeScript support! ๐Ÿš€

beta.62 brings with it a long-awaited, much-requested feature: TypeScript support!

CleanShot 2021-09-27 at 10 42 38

TypeScript Support ๐Ÿš€

1. But, how?

Each time your run a TS script, Script Kit will compile the TS script using esbuild to a JS script in a .scripts dir (notice the "dot"). The compiled JS script is then imported from there. Using .scripts as a sibling dir will help avoid any import/path issues. You can also write TS "library" files in your ~/.kenv/lib dir and import them into your script just fine.

If you're experienced with esbuild and curious about the settings, they look like this:

let { build } = await import("esbuild")
await build({
entryPoints: [scriptPath],
outfile,
bundle: true,
platform: "node",
format: "esm",
external: ["@johnlindquist/kit"],
})

This also opens the door to exporting/building/bundling scripts and libs as individual shippable tools which I'll investigate more in the future.

2. Can I still run my JS scripts if I switch to TS?

Yes! Both your TS and JS scripts will show up in the UI.

3. Why the import "@johnlindquist/kit"?

When you create a new TS script, the generated script will start with the line: import "@johnlindquist/kit"

This is mostly to make your editor stop complaining by forcing it to load the type definition files and forcing it to treat the file as an "es module" so support "top-level await". It's not technically required since it's not technically importing anything, but your editor will certainly complain very loudly if you leave it out.

4. Where is the setting stored?

Look in your ~/.kenv/.env for KIT_MODE=ts.

fs-extra's added to global

The fs-extra methods are now added on the global space. I found myself using outputFile, write/readJson, etc too often and found them to be a great addition. The only one missing is copy since we're already using that to "copy to clipboard". You can bring it in with the normal import/alias process if needed, e.g., let {copy:fsCopy} = await import("fs-extra")

Sync Path

CleanShot 2021-09-27 at 11 10 26

You may notice running scripts from the Script Kit app that some commands you can run in your terminal might be missing, like "yarn", etc.

Run the following command in your terminal to copy the $PATH var from your terminal to your ~/.kenv/.env. This will help "sync" up which commands are available between your terminal and running scripts from the app.

~/.kit/bin/kit sync-path
Discuss Post

Scripts in GitHub actions (preview)

tl;dr Here's an example repo

The example script creates a release, downloads an image, and uploads it to the release.

https://github.com/johnlindquist/kit-action-example

Template Repo

This page has a "one-click" clone so you can add/play with your own script.

https://github.com/johnlindquist/kit-action-template

What is it?

Use any of your scripts in a GitHub action. use the kit-action and point it to a scripts in your scripts dir:

name: "example"
on:
workflow_dispatch:
pull_request:
push:
branches:
- main
jobs:
example:
runs-on: ubuntu-latest
steps:
- name: Script Kit
uses: johnlindquist/kit-action@main
with:
script: "example-script" # The name of a script in your ./scripts dir

Add env vars:

You most likely add "secrets" to GitHub actions, so you'll want to pass them to your scripts as environment variables:

jobs:
example:
runs-on: ubuntu-latest
steps:
- name: Script Kit
uses: johnlindquist/kit-action@main
with:
script: "example-script"
env:
REPO_TOKEN: "${{ secrets.REPO_TOKEN }}" # load in your script with await env("REPO_TOKEN")

Works with your existing repos

Feel free to add this action and a scripts dir to your existing repos. It automatically loads in your repo so you can parse package.json, compress assets, or whatever it is you're looking to add to your CI.

What does "preview" mean?

Everything is working, but it's pointing to the "main" branch rather than a tagged version. Once I get some feedback, I'll tag a "1.0" version so you can uses: @johlindquist/kit-action@v1

Please ask for help! ๐Ÿ˜‡

I'd โค๏ธ to help you script something for a github action! Please let me know whatever I can do to help.

Discuss Post

beta.55 Improved Search, Drag, and Happiness ๐Ÿ˜Š

Search Improvements

beta.55 has a vastly improved search:

Search descriptions ๐ŸŽ‰

CleanShot 2021-08-20 at 13 37 44

Search shortcuts

CleanShot 2021-08-20 at 13 51 49

Search by kenv

CleanShot 2021-08-20 at 13 51 18

Sear by "command-name" (if you can't think of // Menu: name)

CleanShot 2021-08-20 at 13 54 45

Sorts by "score" (rather than alphabetically)

Drag

Choices can now take a drag property. This will make list items "draggable" and allow you to drag/drop to copy files from your machine (or even from URLs!) into any app. When using remote URLs, their will be a bit of "delay" while the file downloads (depending on the file size) between "drag start" and "drop enabled", so just be aware. I'll add some sort of download progress indicator sometime in the future, just not high priority ๐Ÿ˜…

// Menu: Drag demo
await arg(
{
placeholder: "Drag something from below",
ignoreBlur: true,
},
[
{
name: "Heart Eyes (local)",
drag: "/Users/johnlindquist/Downloads/john-hearts@2x.png",
img: "/Users/johnlindquist/Downloads/john-hearts@2x.png",
},
{
name: "React logo svg (wikipedia)",
drag: "https://upload.wikimedia.org/wikipedia/commons/a/a7/React-icon.svg",
img: "https://upload.wikimedia.org/wikipedia/commons/a/a7/React-icon.svg",
},
]
)

CleanShot 2021-08-20 at 15 26 07

You can use the drag object syntax to define a format and data

text/html: Renders the HTML payload in contentEditable elements and rich text (WYSIWYG) editors like Google Docs, Microsoft Word, and others. text/plain: Sets the value of input elements, content of code editors, and the fallback from text/html. text/uri-list: Navigates to the URL when dropping on the URL bar or browser page. A URL shortcut will be created when dropping on a directory or the desktop.

// Menu: Drag demo
await arg(
{
placeholder: "Drag something from below",
ignoreBlur: true,
},
[
{
name: "Padding 4",
drag: {
format: "text/plain",
data: `className="p-4"`,
},
},
{
name: "I love code",
drag: {
format: "text/html",
data: `<span style="background-color:yellow;font-family:Roboto Mono">I โค๏ธ code</span>`,
},
},
]
)

CleanShot 2021-08-20 at 15 48 00

Happiness

I'm very happy with the state of Script Kit. When I started almost a year ago, I had no idea I could push the concept of creating/sharing/managing custom scripts so far. I think it looks great, feels speedy, and is flexible enough to handle so, so many scenarios.

With everything in place, next week I'm starting on creating lessons, demos, and docs. It's time to show you what Script Kit can really do ๐Ÿ˜‰

P.S. - Thanks for all the beta-testing and feedback. It's been tremendously helpful!

Discuss Post

beta.46 Design, โš Flags, div, fixed notify

Design/theme

Put a lot of work into tightening up pixels and made progress towards custom themes:

CleanShot 2021-08-13 at 09 35 40

Here's a silly demo of me playing with theme generation:

Flags โš

An astute observer would notice that the Edit and Share tabs are now gone. They've been consolidated into a "flag menu".

When you press the right key from the main menu of script, the flag menu now opens up. This shows the selected script and gives you some options. It also exposes the keyboard shortcuts associated with those options that you can use to :

CleanShot 2021-08-13 at 09 42 52

I've found I use cmd+o and cmd+n all the time to tweak scripts of quickly create a new one to play around with.

Custom Flags

You can pass your own custom flags like so:

Install flags-demo

//Menu: Flags demo
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)
}

Notice that flag is a global while flags is an object you pass to arg. This is to help keep it consistent with terminal usage:

From the terminal

flags-demo --open

Will set the global flag.open to true

CleanShot 2021-08-13 at 10 08 30

You could also run this and pass in all the args:

flags-demo https://egghead.io --copy

In the app, you could create a second script to pass flags to the first with. This is required if you need to pass multiple flags since the arg helper can only "submit" one per arg.

await run(`flags-demo https://egghead.io --copy`)

I'll put together some more demos soon. There are plenty of existing CLI tools out there using flags heavily, so lots of inspiration to pull from.

await div()

There's a new div "component". You can pass in arbitrary HTML. This works well with the md() helper which generates html from markdown.

Install div-demo

// Menu: Div Demo
// Hit "enter" to continue, escape to exit
await div(`<img src="https://placekitten.com/320"/>`)
await div(
md(
`
# Some header
## You guessed it, an h2
* I
* love
* lists
`
)
)

Fixed notify

notify is now fixed so that it doesn't open a prompt

The most basic usage is:

notify("Hello world")

notify leverages https://www.npmjs.com/package/node-notifier

So the entire API should be available. Here's an example of using the "type inside a notification":

Install notify-demo

// Menu: Notify Demo
let notifier = notify({
title: "Notifications",
message: "Write a reply?",
reply: true,
})
notifier.on("replied", async (obj, options, metadata) => {
await arg(metadata.activationValue)
})
Discuss Post

beta.33 `console.log` component, cmd+o to Open, `className`

console.log Component

The follow code will create the below prompt (๐Ÿ‘€ notice the black background logging component):

let { stdout } = await $`ls ~/projects | grep kit`
await arg(`Select a kit dir`, stdout.split("\n"))
CleanShot 2021-07-22 at 16 13 10@2x
console.log(chalk`{green.bold The current date is:}`)
console.log(new Date().toLocaleDateString())
await arg()
CleanShot 2021-07-22 at 16 12 24@2x

The log even persists between prompts:

let first = await arg("First name")
console.log(first)
let last = await arg("Last name")
console.log(`${first} ${last}`)
let age = await arg("Age")
console.log(`${first} ${last} ${age}`)
let emotion = await arg("Emotion")
console.log(`${first} ${last} ${age} ${emotion}`)
await arg()
CleanShot 2021-07-22 at 16 19 36@2x

Click the "edit" icon to open the full log in your editor: CleanShot 2021-07-22 at 16 20 57@2x

cmd+o to Open

From the main menu, hitting cmd+o will open:

  1. The currently selected script from the main menu
  2. The currently running script
  3. Any "choice" that provides a "filePath" prop:
await arg(`cmd+o to open file`, [
{
name: "Karabiner config",
filePath: "~/.dotfiles/karabiner/karabiner.edn",
},
{
name: "zshrc",
filePath: "~/.zshrc",
},
])

I've found this really useful when I want to tweak the running script, but I don't want to go back through the process of finding it.

Experimental className

You can pass className into the arg options to affect the container for the list items or panel. Most classes from Tailwind should be available. Feel free to play around with it and let me know how it goes ๐Ÿ˜‡:

await arg(
{
className: "p-4 bg-black font-mono text-xl text-white",
},
`
<p>Working on Script Kit today</p>
<img src="https://i.imgflip.com/5hc0v4.jpg" title="made at imgflip.com"/>`
)
CleanShot 2021-07-22 at 16 38 40@2x
await arg(
{
className: "p-4 bg-black font-mono text-xl text-white",
},
["Eat", "more", "tacos ๐ŸŒฎ"]
)
CleanShot 2021-07-22 at 16 41 19@2x
Discuss Post

beta.29 M1 build, install remote kenvs, polish, upcoming lessons

I'm starting on lessons/docs on Monday. If you have anything specific you want me to cover, please reply below!

M1 Build

If you're on an M1 mac, you can download the new M1 build from https://www.scriptkit.com/

  1. Download https://www.scriptkit.com/
  2. Quit Kit. *note - typing kit quit or k q in the app is the fastest way to quit.
  3. Drag/drop to overwrite your previous build
  4. Kit should now auto-update from the M1 channel
  5. Open Kit

Kenv Management

There are a lot of tools to help manage other kenvs. They're in the Kit menu and once you've installed a remote kenv (which is really just a git repo with a scripts dir), then more options show up in the Edit menu to move scripts between kenvs, etc. I'll cover this in detail in the docs/lessons

Polish

Lots of UI work:

  • Remembering position - Each script with a //Shortcut will remember its last individual prompt position. For example, if you have a script that uses textarea, then drag it to the upper right, the next time you launch that script, it will launch in that position.
  • //Image metadata - Scripts can now have images:
//Image: https://placekitten.com/64

or

//Image: logo.png

will load from ~/.kenv/assets/logo.png

  • Spinner - added a spinner for when you submit a prompt and the process needs to do some work before opening the next prompt

CleanShot 2021-07-16 at 12 22 58

  • Resizing - Lots of work on getting window resizing behavior consistent between different UIs. This was a huge pain, but you'll probably never appreciate it ๐Ÿ˜…
  • Lots more - many more small things

Lessons!

I'm starting to work on lessons next week and getting back into streaming schedule. I would โ™ฅ๏ธ to hear any specific questions or lessons you would like to see to help you remove some friction from your day. I'll be posting the lessons over on egghead.io for your viewing pleasure. Please ask questions in the replies!

Discuss Post

Beta.20 MOAR SPEED! โšก๏ธ

Process Pools and Virtualized Lists

Experimental textarea

Feel free to play around with the textarea for multiline input.

let value = await textarea()

The API of textarea will change (it currently just sets the placeholder), but it will always return the string value of the textarea, so there won't be any breaking changes if you just keep the default behavior. cmd+s submits. cmd+w cancels.

Experimental editor (this will become a paid ๐Ÿ’ต feature later this year)

As an upgrade to textarea, await editor() will give you a full editor experience. Same as the textarea, the API will also change, but will always return a string of the content.

// Defaults to markdown
let value = await editor()

โš ๏ธ API is subject to change!

let value = await editor("markdown", `
## Preloaded content
* nice
`)
let value = await editor("javascript", `
console.log("Support other languages")
`)

A note on paid features

Everything you've used so far in the Script Kit app will stay free. The core kit is open-source MIT.

The paid features will be add-ons to the core experience: Themes, Editor, Widgets, Screenshots, Record Audio, and many more fun ideas. These will roll out experimentally in the free version first then move exclusively to the paid version. Expect the paid versions later this year.

Discuss Post

Beta.19 New Features - Gotta go fast! ๐ŸŽ๐Ÿ’จ

Beta.19 is all about speed! I've finally landed on an approach I love to get the prompt moving waaaay faster.

Couple videos below:

Instant Prompts

// Shortcut: option 5
let { items } = await db(async () => {
let response = await get(
`https://api.github.com/users/johnlindquist/repos`
)
return response.data
})
await arg("Select repo", items)

Instant Tabs

Instant Main Menu

The main menu now also leverages the concepts behind Instant Prompts listed above.

Faster in the future

These conventions laid the groundwork for caching prompt data, but I still have plenty ideas to speed things, especially around how the app launches the process. I'm looking forward to making this even faster for you!

I'm also starting the work on an "Instant Textarea" because I know popping open a little textarea to take/save notes/ideas is something many people would use. ๐Ÿ“

Discuss Post

How to Get Your Scripts Featured on ScriptKit.com ๐Ÿ˜Ž

TL;DR

  • Help -> Create kenv
  • Git init new kenv, push to github
  • Reply, dm, contact me somehow with the repo ๐Ÿ˜‡

Here's a video walking you through it:

Discuss Post

Beta.18 Changes/Features (`db` has a breaking change)

โš ๏ธBreaking: New db helper

lowdb updated to 2.0, so I updated the db helper to support it.

  • access/mutate the objects in the db directly. Then .write() to save your changes to the file.
  • await db() and await myDb.write()

Example with a simple object:

let shoppingListDb = await db("shopping-list", {
list: ["apples", "bananas"],
})
let item = await arg("Add to list")
shoppingListDb.list.push(item)
await shoppingListDb.write()
await arg("Shopping list", shoppingListDb.list)

You can also use an async function to store the initial data:

let reposDb = await db("repos", async () => {
let response = await get(
"https://api.github.com/users/johnlindquist/repos"
)
return {
repos: response.data,
}
})
await arg("Select repo", reposDb.repos)

Text Area prompt

let text = await textarea()
inspect(text)

CleanShot 2021-06-04 at 14 25 12

Optional value

arg choice objects used to require a value. Now if you don't provide a value, it will simply return the entire object:

let person = await arg("Select", [
{ name: "John", location: "Chair" },
{ name: "Mindy", location: "Couch" },
])
await arg(person.location)

โš—๏ธ Experimental "Multiple kenvs"

There was a ton ๐Ÿ‹๏ธโ€โ™€๏ธ of internal work over the past couple weeks to get this working. The "big idea" is supporting multiple kit environments. For example:

  • private/personal kenv
  • shared kenv
  • company kenv
  • product kenv

Future plans

In an upcoming release:

  • you'll be able to "click to install kenv from repo" (just like we do with individual scripts)
  • update a git-controlled kenv (like a company kenv)
  • the main prompt will be able to search for all scripts across kenvs.
  • If multiple kenvs exist, creating a new script will ask you which kenv to create it in.

For now, you can try adding/creating/switching the help menu. It should all work fine, but will be waaaay cooler in the future ๐Ÿ˜Ž

CleanShot 2021-06-04 at 11 50 32

Improved Error Prompt

Now when an error occurs, it takes the error data, shuts down the script, then prompts you on what to do. For example, trying to use the old db would result in this:

CleanShot 2021-06-04 at 12 03 04

Improved Tab Switching

Switching tabs will now cancel the previous tabs' script. Previously, if you quickly switched tabs on the main menu, the "Hot" tab results might show up in a different tab because the loaded after the tab switched. The internals around message passing between the script and the app now have a cancellation mechanism so you only get the latest result that matches the prompt/tab. (This was also a ton of internals refactoring work ๐Ÿ˜…)

Discuss Post

โœจNEW FEATURESโœจ beta.17

New features are separated into the comments below:

Discuss Post

โœจ NEW โœจ // Background: true

beta.12 brings in the ability to start/stop background tasks.

Using // Background :true at the top of your script will change the behavior in the main menu:

// Background: true
setInterval(() => {}, 1000) //Some long-running process
Screen Shot 2021-05-06 at 1 30 53 PM Screen Shot 2021-05-06 at 1 31 13 PM Screen Shot 2021-05-06 at 1 33 02 PM

Auto (like nodemon)

// Background: auto
setInterval(() => {}, 1000) //Some long-running process

Using auto, after you start the script, editing will stop/restart the script.

Discuss Post

// Watch: metadata ๐Ÿ‘€

Script Kit now supports // Watch: metadata

// Watch: ~/projects/thoughts/**/*.md
let { say } = await kit("speech")
say("journal updated")
  • // Watch: supports any file name, glob, or array (Kit will JSON.parse the array).
  • Scripts will run on the "change" event
  • Read more about supported globbing

Read about the other metadata

I would LOVE to hear about scenarios you would use this for or if you run into any issues ๐Ÿ™

Discuss Post

beta.96 - Design, Drop, and Hotkeys! Oh my!

Can't wait to see what you build! Happy Scripting this weekend! ๐Ÿ˜‡

Discuss Post

*New* Choice Preview

Install google-image-search

// Menu: Google Image Search
// Description: Searches Google Images
// Author: John Lindquist
// Twitter: @johnlindquist
let gis = await npm("g-i-s")
let selectedImageUrl = await arg(
"Image search:",
async input => {
if (input.length < 3) return []
let searchResults = await new Promise(res => {
gis(input, (_, results) => {
res(results)
})
})
return searchResults.map(({ url }) => {
return {
name: url.split("/").pop().replace(/\?.*/g, ""),
value: url,
preview: `<img src="${url}" />`,
}
})
}
)
copy(selectedImageUrl)

Install giphy-search

// Menu: Giphy
// Description: Search giphy. Paste markdown link.
// Author: John Lindquist
// Twitter: @johnlindquist
let download = await npm("image-downloader")
let queryString = await npm("query-string")
let { setSelectedText } = await kit("text")
if (!env.GIPHY_API_KEY) {
show(
`<div class="p-4">
<div>
Grab an API Key from the Giphy dev dashboard:
</div>
<a href="https://developers.giphy.com/dashboard/">Here</a>
</div>`
)
}
let GIPHY_API_KEY = await env("GIPHY_API_KEY")
let search = q =>
`https://api.giphy.com/v1/gifs/search?api_key=${GIPHY_API_KEY}&q=${q}&limit=10&offset=0&rating=g&lang=en`
let { input, url } = await arg(
"Search giphy:",
async input => {
if (!input) return []
let query = search(input)
let { data } = await get(query)
return data.data.map(gif => {
return {
name: gif.title.trim() || gif.slug,
value: {
input,
url: gif.images.downsized_medium.url,
},
preview: `<img src="${gif.images.downsized_medium.url}" alt="">`,
}
})
}
)
let formattedLink = await arg("Format to paste", [
{
name: "URL Only",
value: url,
},
{
name: "Markdown Image Link",
value: `![${input}](${url})`,
},
{
name: "HTML <img>",
value: `<img src="${url}" alt="${input}">`,
},
])
setSelectedText(formattedLink)
Discuss Post

Types are here!

Update (1.1.0-beta.86) adds a ~/.kit/kit.d.ts to allow better code hinting and completion.

โ—๏ธAfter updating, you will need to manually "link" your ~/.kenv to your ~/.kit for the benefits (This will happen automatically for new users during install)

Method 1 - Install and run this script

Click to install link-kit

await cli("install", "~/.kit")

Method 2 - In your terminal

PATH=~/.kit/node/bin ~/.kit/node/bin/npm --prefix ~/.kenv i ~/.kit

Now your scripts in your ~/.kenv/scripts should have completion/hinting for globals included in the "preloaded" scripts.

I still need to add types for the helpers that load scripts from dirs kit(), cli(), etc.

Please let me know how it goes and if you have any questions. Thanks!

Discuss Post