John Lindquist

John Lindquist

// Name: Paste Clipboard Image as Cloudinary Markdown URL
// Shortcut: opt shift v
import "@johnlindquist/kit"
let buffer = await clipboard.readImage()
if (buffer && buffer.length) {
let { default: cloudinary } = await npm("cloudinary")
cloudinary.config({
cloud_name: await env("CLOUDINARY_CLOUD_NAME"),
api_key: await env("CLOUDINARY_API_KEY"),
api_secret: await env("CLOUDINARY_API_SECRET"),
})
let response = await new Promise((response, reject) => {
let cloudStream = cloudinary.v2.uploader.upload_stream(
{
folder: "clipboard",
},
(error, result) => {
if (error) {
reject(error)
} else {
response(result)
}
}
)
new Readable({
read() {
this.push(buffer)
this.push(null)
},
}).pipe(cloudStream)
})
log(response)
// format however you want
let markdown = `![${response.url}](${response.url})`
await setSelectedText(markdown)
} else {
await div(md(`# No Image in Clipboard`))
}

// Name: Screenshot Current Tweet
import "@johnlindquist/kit"
const { chromium }: typeof import("playwright") = await npm(
"playwright"
)
let url = await getActiveTab()
let timeout = 5000
let headless = false
const browser = await chromium.launch({
timeout,
headless,
})
const context = await browser.newContext({
colorScheme: "dark",
})
const page = await context.newPage()
page.setDefaultTimeout(timeout)
await page.goto(url)
let screenshotPath = home(
"Downloads",
path.parse(url).name + ".png"
)
try {
await page
.locator("article[tabindex='-1']")
.screenshot({ path: screenshotPath })
await revealFile(screenshotPath)
log(`Done`)
} catch (error) {
log(error)
}
await browser.close()

// Name: Theme Creator
// This will create a file at ~/.kenv/theme.txt
// Edit the file, then hit save to update the theme
import "@johnlindquist/kit"
let themePath = kenvPath("theme.txt")
if (!(await isFile(themePath))) {
let defaultTheme = `
--color-primary: 255, 155, 255
--color-secondary: 255, 113, 39
--color-background: 255, 255, 255
`.trim()
await writeFile(themePath, defaultTheme)
}
await edit(themePath)
let { watch } = await npm("chokidar")
setIgnoreBlur(true)
let mS = mainScript()
watch(themePath).on("change", async () => {
let contents = await readFile(themePath, "utf-8")
let theme = contents.split("\n").reduce((acc, line) => {
let [k, v] = line.trim().split(":")
acc[k.trim()] = v.trim()
return acc
}, {})
setTheme(theme)
})
await mS

// Name: Get Tailwind Color
import "@johnlindquist/kit"
let nearestColor = await npm("nearest-color")
const colors = {
black: "#000",
white: "#fff",
slate: {
50: "#f8fafc",
100: "#f1f5f9",
200: "#e2e8f0",
300: "#cbd5e1",
400: "#94a3b8",
500: "#64748b",
600: "#475569",
700: "#334155",
800: "#1e293b",
900: "#0f172a",
},
gray: {
50: "#f9fafb",
100: "#f3f4f6",
200: "#e5e7eb",
300: "#d1d5db",
400: "#9ca3af",
500: "#6b7280",
600: "#4b5563",
700: "#374151",
800: "#1f2937",
900: "#111827",
},
zinc: {
50: "#fafafa",
100: "#f4f4f5",
200: "#e4e4e7",
300: "#d4d4d8",
400: "#a1a1aa",
500: "#71717a",
600: "#52525b",
700: "#3f3f46",
800: "#27272a",
900: "#18181b",
},
neutral: {
50: "#fafafa",
100: "#f5f5f5",
200: "#e5e5e5",
300: "#d4d4d4",
400: "#a3a3a3",
500: "#737373",
600: "#525252",
700: "#404040",
800: "#262626",
900: "#171717",
},
stone: {
50: "#fafaf9",
100: "#f5f5f4",
200: "#e7e5e4",
300: "#d6d3d1",
400: "#a8a29e",
500: "#78716c",
600: "#57534e",
700: "#44403c",
800: "#292524",
900: "#1c1917",
},
red: {
50: "#fef2f2",
100: "#fee2e2",
200: "#fecaca",
300: "#fca5a5",
400: "#f87171",
500: "#ef4444",
600: "#dc2626",
700: "#b91c1c",
800: "#991b1b",
900: "#7f1d1d",
},
orange: {
50: "#fff7ed",
100: "#ffedd5",
200: "#fed7aa",
300: "#fdba74",
400: "#fb923c",
500: "#f97316",
600: "#ea580c",
700: "#c2410c",
800: "#9a3412",
900: "#7c2d12",
},
amber: {
50: "#fffbeb",
100: "#fef3c7",
200: "#fde68a",
300: "#fcd34d",
400: "#fbbf24",
500: "#f59e0b",
600: "#d97706",
700: "#b45309",
800: "#92400e",
900: "#78350f",
},
yellow: {
50: "#fefce8",
100: "#fef9c3",
200: "#fef08a",
300: "#fde047",
400: "#facc15",
500: "#eab308",
600: "#ca8a04",
700: "#a16207",
800: "#854d0e",
900: "#713f12",
},
lime: {
50: "#f7fee7",
100: "#ecfccb",
200: "#d9f99d",
300: "#bef264",
400: "#a3e635",
500: "#84cc16",
600: "#65a30d",
700: "#4d7c0f",
800: "#3f6212",
900: "#365314",
},
green: {
50: "#f0fdf4",
100: "#dcfce7",
200: "#bbf7d0",
300: "#86efac",
400: "#4ade80",
500: "#22c55e",
600: "#16a34a",
700: "#15803d",
800: "#166534",
900: "#14532d",
},
emerald: {
50: "#ecfdf5",
100: "#d1fae5",
200: "#a7f3d0",
300: "#6ee7b7",
400: "#34d399",
500: "#10b981",
600: "#059669",
700: "#047857",
800: "#065f46",
900: "#064e3b",
},
teal: {
50: "#f0fdfa",
100: "#ccfbf1",
200: "#99f6e4",
300: "#5eead4",
400: "#2dd4bf",
500: "#14b8a6",
600: "#0d9488",
700: "#0f766e",
800: "#115e59",
900: "#134e4a",
},
cyan: {
50: "#ecfeff",
100: "#cffafe",
200: "#a5f3fc",
300: "#67e8f9",
400: "#22d3ee",
500: "#06b6d4",
600: "#0891b2",
700: "#0e7490",
800: "#155e75",
900: "#164e63",
},
sky: {
50: "#f0f9ff",
100: "#e0f2fe",
200: "#bae6fd",
300: "#7dd3fc",
400: "#38bdf8",
500: "#0ea5e9",
600: "#0284c7",
700: "#0369a1",
800: "#075985",
900: "#0c4a6e",
},
blue: {
50: "#eff6ff",
100: "#dbeafe",
200: "#bfdbfe",
300: "#93c5fd",
400: "#60a5fa",
500: "#3b82f6",
600: "#2563eb",
700: "#1d4ed8",
800: "#1e40af",
900: "#1e3a8a",
},
indigo: {
50: "#eef2ff",
100: "#e0e7ff",
200: "#c7d2fe",
300: "#a5b4fc",
400: "#818cf8",
500: "#6366f1",
600: "#4f46e5",
700: "#4338ca",
800: "#3730a3",
900: "#312e81",
},
violet: {
50: "#f5f3ff",
100: "#ede9fe",
200: "#ddd6fe",
300: "#c4b5fd",
400: "#a78bfa",
500: "#8b5cf6",
600: "#7c3aed",
700: "#6d28d9",
800: "#5b21b6",
900: "#4c1d95",
},
purple: {
50: "#faf5ff",
100: "#f3e8ff",
200: "#e9d5ff",
300: "#d8b4fe",
400: "#c084fc",
500: "#a855f7",
600: "#9333ea",
700: "#7e22ce",
800: "#6b21a8",
900: "#581c87",
},
fuchsia: {
50: "#fdf4ff",
100: "#fae8ff",
200: "#f5d0fe",
300: "#f0abfc",
400: "#e879f9",
500: "#d946ef",
600: "#c026d3",
700: "#a21caf",
800: "#86198f",
900: "#701a75",
},
pink: {
50: "#fdf2f8",
100: "#fce7f3",
200: "#fbcfe8",
300: "#f9a8d4",
400: "#f472b6",
500: "#ec4899",
600: "#db2777",
700: "#be185d",
800: "#9d174d",
900: "#831843",
},
rose: {
50: "#fff1f2",
100: "#ffe4e6",
200: "#fecdd3",
300: "#fda4af",
400: "#fb7185",
500: "#f43f5e",
600: "#e11d48",
700: "#be123c",
800: "#9f1239",
900: "#881337",
},
}
const tailwindColors = {}
for (const colorsKey in colors) {
if (typeof colors[colorsKey] === "string") {
tailwindColors[colorsKey] = colors[colorsKey]
} else {
for (const nestedKey in colors[colorsKey]) {
tailwindColors[`${colorsKey}-${nestedKey}`] =
colors[colorsKey][nestedKey]
}
}
}
let color = await arg({
type: "color",
})
const getNearestTailwindColor =
nearestColor.from(tailwindColors)
const nearestTailwindColor = getNearestTailwindColor(color)
await editor({
language: "json",
value: JSON.stringify(
{
color,
nearestTailwindColor,
},
null,
"\t"
),
})

// Name: Widget Dynamic Lists
import "@johnlindquist/kit"
let names = ["John", "Mindy", "Ben", "Scooter"]
let items = _.shuffle(names).map(name => ({ name }))
let html = `
<div class="flex flex-col w-screen h-screen justify-around items-center">
<button
class="rounded px-4 py-2 bg-black bg-opacity-50 hover:bg-opacity-25 w-1/2"
v-for="(item, index) in items" :key="item.name" :data-name="item.name" :data-index="index">{{index}}. {{item.name}}</button>
<div>{{selected}}</div>
</div>
`
let w = await widget(html, {
state: {
items,
selected: items[0].name
}
}
)
w.onClick(event => {
if (event.dataset.name) {
w.setState({selected: event.dataset.name})
}
})
setInterval(() => {
items = _.shuffle(items)
w.setState({
items,
})
}, 1000)
import "@johnlindquist/kit"
// Menu: Chrome History
// Description: Open a url from your history
// Author: John Lindquist
// Twitter: @johnlindquist
let historyFilePath = home(
"Library/Application Support/Google/Chrome/Default/History"
)
// ~/.kenv/tmp/chrome-history/History
let tmpHistoryPath = tmpPath("History")
// We make copy of the db each time or you run into a SQL_BUSY if Chrome is open
await copyFile(historyFilePath, tmpHistoryPath)
let sqlite3 = await npm("sqlite3")
let db = new sqlite3.Database(tmpHistoryPath)
let limit = 1000
let sql = `SELECT url, title, last_visit_time FROM urls ORDER BY last_visit_time DESC LIMIT ${limit}`
let callback = async (err, rows) => {
let url = await arg(
"Chrome History",
rows.map(({ url, title }) => {
return {
name: title,
description: url,
value: url,
}
})
)
open(url)
}
db.all(sql, callback)
db.close()

// Name: Get Data from a Widget
import "@johnlindquist/kit"
let state = {
firstName: "",
lastName: "",
fullName: "",
}
let w = await widget(
`
<div class="flex flex-col justify-center items-center text-white">
<form class="flex flex-col">
<input id="first" type="text" class="text-black" placeholder="First Name" autofocus>
<input id="last" type="text" class="text-black" placeholder="Last Name">
<button id="submit" type="submit">Submit</button>
</form>
<div class="flex flex-col">
<div>{{firstName}} {{lastName}}</div>
<div>{{fullName}}</div>
</div>
</div>
`,
{
height: 360,
width: 360,
state,
}
)
w.onInput(event => {
if (event.targetId === "first") {
state.firstName = event.value
}
if (event.targetId === "last") {
state.lastName = event.value
}
w.setState(state)
})
w.onClick(event => {
if (event.targetId === "submit") {
state.fullName = `Hello, ${state.firstName} ${state.lastName}`
}
w.setState(state)
})

// Name: Post URL with Metadata to Tana
import "@johnlindquist/kit"
let og = await npm("opengraph-io")
// You'll need to sign-up for https://www.opengraph.io/
// Create a free account and grab an API key from: https://dashboard.opengraph.io/apis/Opengraphio
// Paste the token when prompted for OPENGRAPH_API_ID_TOKEN
let openGraph = og({
appId: await env("OPENGRAPH_API_ID_TOKEN", {
hint: `Grab a token from [here](https://dashboard.opengraph.io/apis/Opengraphio)`,
}),
cacheOk: true, // If a cached result is available, use it for quickness
useProxy: false, // Proxies help avoid being blocked and can bypass capchas
maxCacheAge: 432000000, // The maximum cache age to accept
acceptLang: "en-US,en;q=0.9", // Language to present to the site.
fullRender: false, // This will cause JS to execute when rendering to deal with JS dependant sites
})
let url = await getActiveTab()
// on windows, remove above and use:
// let url = await arg("Paste URL here")
let result = await openGraph.getSiteInfo(url)
debugger
if (result.openGraph.error) {
await editor(result.openGraph.error)
} else {
let { title, site_name, description } = result.openGraph
// In the future, Tana will support more than just raw text
// For now, only text is supported.
// This gives you a chance to tweak the Title/Description before posting:
let note = await template(
`
${title} - ${site_name}
- ${url}
- ${description}
`.trim()
)
let TANA_URL =
"https://europe-west1-tagr-prod.cloudfunctions.net"
// Select a node in Tana, hit cmd+K from mac, ctrl+K for windows, search for "API" to generate a token
// Paste the token when prompted
let TANA_TOKEN_TODOS = await env("TANA_TOKEN_TODOS_TWO")
await hide()
// await copy(note.trim()) if you prefer to copy/paste you can use this instead
await get(`${TANA_URL}/addToNode`, {
headers: {
authorization: `Bearer ${TANA_TOKEN_TODOS}`,
},
params: {
note: note.trim(),
},
})
}
debugger

// Name: Grab Readwise Highlights
import "@johnlindquist/kit"
let API_TOKEN = await env("TANA_API_TOKEN")
let HIGHLIGHTS_URL = "https://readwise.io/api/v2/export/"
// Credit to: https://github.com/tanainc/tana-paste-examples
// Copy/pasted and refactored to Script Kit: scriptkit.com
let getItemsFromReadwise = async (daysToFetch = 1) => {
let dateOffset = 24 * 60 * 60 * 1000 * daysToFetch
let updatedAfterDate = new Date()
updatedAfterDate.setTime(
updatedAfterDate.getTime() - dateOffset
)
let response = await fetch(
`${HIGHLIGHTS_URL}?updatedAfter=${updatedAfterDate.toISOString()}`,
{
headers: {
Authorization: `Token ${API_TOKEN}`,
},
}
)
return await response.json()
}
let daysToFetch = await arg("Days to fetch")
let { results } = await getItemsFromReadwise(
parseInt(daysToFetch, 10)
)
let result = results.map(book => {
let isValidSourceURL =
book.source_url?.startsWith("https://")
let hasHighlights = book.highlights?.length > 0
let title = `${book.title}`
let url = ``
if (isValidSourceURL) {
url = `URL:: ${book.source_url}`
}
let type = `type:: ${book.category?.replace(/s$/, "")}`
let author = `author:: ${book.author}`
let highlights = book.highlights.map(highlight => {
let lines = highlight.text.split("\n")
let cleanLines = lines.map(line => {
let cleanedLine = line.replace(/•\s+/, "").trim()
return `${cleanedLine}`
})
let note = ``
if (highlight.note) {
note = `${highlight.note}`
}
return `
${cleanLines}
${note}
`.trim()
})
let maybeUrl = url
? `
- ${url}
`
: ``
let maybeHighlights = highlights?.length
? `
- Highlights
- ${highlights.join("\n")}`
: ``
return `
- ${title} #from-the-web
${maybeUrl}
- ${type}
- ${author}
${maybeHighlights}
`.trim()
})
// because I'm on a trial account (I assume)
let [skip, ...rest] = result
// copies the readwise content to the clipboard
// I'm using slice to only take 2 results for demo purposes
await copy(`%%tana%%
${rest.slice(0, 2).join("\n")}
`)
// Name: Midjourney Prompt
// Description: Generate a Random Midjourney Prompt
// Author: John Lindquist
// Twitter: @johnlindquist
import "@johnlindquist/kit"
let count = parseInt(
await arg({
placeholder: "How many prompts to generate and paste?",
onInput: input => {
submit(input) //auto submit the number on press
},
}),
10
)
let frontmost = await npm("frontmost-app")
let exitIfNotDiscord = async () => {
let { localizedName: name } = await frontmost()
if (name !== "Discord") {
exit()
}
}
await exitIfNotDiscord()
let n = (x = 0) =>
`::${Number(_.random(0.2, 2.2) + x).toPrecision(1)}`
let iw = (x = 0) =>
`${Number(_.random(0.2, 2.2) + x).toPrecision(1)}`
let parentDir = await env(
"MIDJOURNEY_PARENT_DIR",
async () => {
return await path({
hint: `Select a parent dir to clone the repo: https://github.com/johnlindquist/midjourney`,
})
}
)
let mjDir = path.resolve(parentDir, "midjourney")
let mjExists = await isDir(mjDir)
if (!mjExists) {
cd(parentDir)
await exec(
`git clone https://github.com/johnlindquist/midjourney`
)
cd(mjDir)
} else {
cd(mjDir)
await exec(`git pull`)
}
// await wiki() will pull a random wikipedia title
let wiki = await npm("random-word-wikipedia")
// Generate functions based on .md files in the repo
let files = await readdir(mjDir)
files = files.filter(file => file.endsWith(".md"))
for await (let file of files) {
let filePath = path.resolve(mjDir, file)
let list = await readFile(filePath, "utf-8")
let { name } = path.parse(filePath)
global[name] = () => _.sample(list.trim().split("\n"))
}
hide()
let go = async () => {
await exitIfNotDiscord()
// Customize however you want. More old "maybe working" examples here: https://github.com/johnlindquist/midjourney/blob/main/prompts.md
let fnsPrompt = `/imagine prompt: ${image()} ${myimage()} Studio photoshoot of ${celeb()} ${animal()} ${hero()}::4 ${await wiki()}${n()} in a ${color()} ${climate()}${n()} in the style of ${game()}${n()} taken by ${photographer()}${n()} by ${artist()}${n()} stylized like ${style()} and ${rank()}${n()} --stylize ${_.random(
1000,
3000
)} --uplight --aspect 16:9 --iw ${iw()}`
let prompt = _.sample([fnsPrompt])
await exitIfNotDiscord()
await setSelectedText(prompt)
log(prompt)
await wait(1000)
await exitIfNotDiscord() //I try to be extra careful when scripting the `Enter` key
await keyboard.pressKey(Key.Enter)
await keyboard.releaseKey(Key.Enter)
await wait(250) // Discord needs a sec after paste+enter to recorgnize it as a command
await exitIfNotDiscord()
await keyboard.pressKey(Key.Enter)
await keyboard.releaseKey(Key.Enter)
}
let i = 0
while (i < count) {
i++
await go()
await wait(1000)
}
// Menu: App Launcher
// Description: Search for an app then launch it
// Author: John Lindquist
// Twitter: @johnlindquist
let createChoices = async () => {
let apps = await fileSearch("", {
onlyin: "/",
kind: "application",
})
let prefs = await fileSearch("", {
onlyin: "/",
kind: "preferences",
})
let group = path => apps =>
apps
.filter(app => app.match(path))
.sort((a, b) => {
let aName = a.replace(/.*\//, "")
let bName = b.replace(/.*\//, "")
return aName > bName ? 1 : aName < bName ? -1 : 0
})
return [
...group(/^\/Applications\/(?!Utilities)/)(apps),
...group(/\.prefPane$/)(prefs),
...group(/^\/Applications\/Utilities/)(apps),
...group(/System/)(apps),
...group(/Users/)(apps),
].map(value => {
return {
name: value.split("/").pop().replace(".app", ""),
value,
description: value,
}
})
}
let appsDb = await db("apps", async () => ({
choices: await createChoices(),
}))
let app = await arg("Select app:", appsDb.choices)
let command = `open -a "${app}"`
if (app.endsWith(".prefPane")) {
command = `open ${app}`
}
exec(command)
// Menu: Book Search
// Description: Use Open Library API to search for books
// Author: John Lindquist
// Twitter: @johnlindquist
let query = await arg('Search for a book title:')
//This API can be a little slow. Wait a couple seconds
let response = await get(`http://openlibrary.org/search.json?q=${query}`)
let transform = ({title, author_name}) =>
`* "${title}" - ${author_name?.length && author_name[0]}`
let markdown = response.data.docs.map(transform).join('\n')
inspect(markdown, 'md')
// Menu: Chrome Bookmarks
// Description: Select and open a bookmark from Chrome
// Author: John Lindquist
// Twitter: @johnlindquist
let bookmarks = await readFile(
home(
"Library/Application Support/Google/Chrome/Default/Bookmarks"
)
)
bookmarks = JSON.parse(bookmarks)
bookmarks = bookmarks.roots.bookmark_bar.children
let url = await arg(
"Select bookmark",
bookmarks.map(({ name, url }) => {
return {
name,
description: url,
value: url,
}
})
)
exec(`open "${url}"`)
// Menu: Open Chrome Tab
// Description: List all Chrome tabs. Then switch to that tab
// Author: John Lindquist
// Twitter: @johnlindquist
let currentTabs = await getTabs()
let bookmarks = await readFile(
home(
"Library/Application Support/Google/Chrome/Default/Bookmarks"
)
)
bookmarks = JSON.parse(bookmarks)
bookmarks = bookmarks.roots.bookmark_bar.children
let bookmarkChoices = bookmarks.map(({ name, url }) => {
return {
name: url,
description: name,
value: url,
}
})
let currentOpenChoices = currentTabs.map(
({ url, title }) => ({
name: url,
value: url,
description: title,
})
)
let bookmarksAndOpen = [
...bookmarkChoices,
...currentOpenChoices,
]
let choices = _.uniqBy(bookmarksAndOpen, "name")
let url = await arg("Focus Chrome tab:", choices)
focusTab(url)
// Menu: Convert Colors
// Description: Converts colors between rgb, hex, etc
// Author: John Lindquist
// Twitter: @johnlindquist
let convert = await npm("color-convert")
let createChoice = (type, value, input) => {
return {
name: type + ": " + value,
value,
html: `<div class="h-full w-full p-1 text-xs flex justify-center items-center font-bold" style="background-color:${input}">
<span>${value}</span>
</div>`,
}
}
//using a function with "input" allows you to generate values
let conversion = await arg("Enter color:", input => {
if (input.startsWith("#")) {
return ["rgb", "cmyk", "hsl"].map(type => {
let value = convert.hex[type](input).toString()
return createChoice(type, value, input)
})
}
//two or more lowercase
if (input.match(/^[a-z]{2,}/)) {
return ["rgb", "hex", "cmyk", "hsl"]
.map(type => {
try {
let value =
convert.keyword[type](input).toString()
return createChoice(type, value, input)
} catch (error) {
return ""
}
})
.filter(Boolean)
}
return []
})
setSelectedText(conversion)
// Menu: Search for a File
// Description: File Search
// Author: John Lindquist
// Twitter: @johnlindquist
/** Note: This is a very basic search implementation based on "mdfind".
* File search will be a _big_ focus in future versions of Script Kit
*/
let selectedFile = await arg(
"Search a file:",
async input => {
if (input?.length < 4) return []
let files = await fileSearch(input)
return files.map(path => {
return {
name: path.split("/").pop(),
description: path,
value: path,
}
})
}
)
exec(`open ${selectedFile}`)
// Menu: Giphy
// Description: Search giphy. Paste link.
// Author: John Lindquist
// Twitter: @johnlindquist
let download = await npm("image-downloader")
let queryString = await npm("query-string")
let GIPHY_API_KEY = await env("GIPHY_API_KEY", {
hint: md(
`Get a [Giphy API Key](https://developers.giphy.com/dashboard/)`
),
ignoreBlur: true,
secret: true,
})
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.original.url,
},
preview: `<img src="${gif.images.downsized.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)
// Menu: Gist from Finder
// Description: Select a file in Finder, then create a Gist
// Author: John Lindquist
// Twitter: @johnlindquist
let filePath = await getSelectedFile()
let file = filePath.split("/").pop()
let isPublic = await arg("Should the gist be public?", [
{ name: "No", value: false },
{ name: "Yes", value: true },
])
const body = {
files: {
[file]: {
content: await readFile(filePath, "utf8"),
},
},
}
if (isPublic) body.public = true
let config = {
headers: {
Authorization:
"Bearer " +
(await env("GITHUB_GIST_TOKEN", {
info: `Create a gist token: <a class="bg-white" href="https://github.com/settings/tokens/new">https://github.com/settings/tokens/new</a>`,
message: `Set .env GITHUB_GIST_TOKEN:`,
})),
},
}
const response = await post(
`https://api.github.com/gists`,
body,
config
)
exec(`open ` + response.data.html_url)
// Menu: Google Image Grid
// Description: Create a Grid of Images
// Author: John Lindquist
// Twitter: @johnlindquist
let gis = await npm("g-i-s")
await arg("Search for images:", async input => {
if (input.length < 3) return ``
let searchResults = await new Promise(res => {
gis(input, (_, results) => {
res(results)
})
})
return `<div class="flex flex-wrap">${searchResults
.map(({ url }) => `<img class="h-32" src="${url}" />`)
.join("")}</div>`
})
// Menu: Detect Image Width and Height
// Description: Show the metadata of an image
// Author: John Lindquist
// Twitter: @johnlindquist
let sharp = await npm("sharp")
let image = await arg("Search an image:", async input => {
if (input.length < 3) return []
let files = await fileSearch(input, { kind: "image" })
return files.map(path => {
return {
name: path.split("/").pop(),
value: path,
description: path,
}
})
})
let { width, height } = await sharp(image).metadata()
console.log({ width, height })
await arg(`Width: ${width} Height: ${height}`)
// Menu: Resize an Image
// Description: Select an image in Finder. Type option + i to resize it.
// Author: John Lindquist
// Twitter: @johnlindquist
// Shortcut: opt i
let sharp = await npm("sharp")
let imagePath = await getSelectedFile()
let width = Number(await arg("Enter width:"))
let metadata = await sharp(imagePath).metadata()
let newHeight = Math.floor(
metadata.height * (width / metadata.width)
)
let lastDot = /.(?!.*\.)/
let resizedImageName = imagePath.replace(
lastDot,
`-${width}.`
)
await sharp(imagePath)
.resize(width, newHeight)
.toFile(resizedImageName)
// Menu: New Journal Entry
// Description: Generate a file using the current date in a specified folder
// Author: John Lindquist
// Twitter: @johnlindquist
let { format } = await npm("date-fns")
let date = format(new Date(), "yyyy-MM-dd")
let journalPath = await env("JOURNAL_PATH")
if (!(await isDir(journalPath))) {
mkdir("-p", journalPath)
}
let journalFile = path.join(journalPath, date + ".md")
if (!(await isFile(journalFile))) {
let journalPrompt = `How are you feeling today?`
await writeFile(journalFile, journalPrompt)
}
edit(journalFile, env?.JOURNAL_PATH)
// Menu: Open Project
// Description: List dev projects
// Author: John Lindquist
// Twitter: @johnlindquist
let { projects, write } = await db("projects", {
projects: [
"~/.kit",
"~/projects/kitapp",
"~/projects/scriptkit.com",
],
})
onTab("Open", async () => {
let project = await arg("Open project:", projects)
edit(project)
})
onTab("Add", async () => {
while (true) {
let project = await arg(
"Add path to project:",
md(projects.map(project => `* ${project}`).join("\n"))
)
projects.push(project)
await write()
}
})
onTab("Remove", async () => {
while (true) {
let project = await arg("Open project:", projects)
let indexOfProject = projects.indexOf(project)
projects.splice(indexOfProject, 1)
await write()
}
})
// Menu: Quick Thoughts
// Description: Add lines to today's journal page
// Author: John Lindquist
// Twitter: @johnlindquist
let { format } = await npm("date-fns")
let date = format(new Date(), "yyyy-MM-dd")
let thoughtsPath = await env("THOUGHTS_PATH")
let thoughtFile = path.join(thoughtsPath, date + ".md")
let firstEntry = true
let addThought = async thought => {
if (firstEntry) {
thought = `
- ${format(new Date(), "hh:mmaa")}
${thought}\n`
firstEntry = false
} else {
thought = ` ${thought}\n`
}
await appendFile(thoughtFile, thought)
}
let openThoughtFile = async () => {
let { stdout } = exec(`wc ${thoughtFile}`, {
silent: true,
})
let lineCount = stdout.trim().split(" ").shift()
edit(thoughtFile, thoughtsPath, lineCount + 1) //open with cursor at end
await wait(500)
exit()
}
if (!(await isFile(thoughtFile)))
await writeFile(thoughtFile, `# ${date}\n`)
while (true) {
let thought = await arg({
placeholder: "Thought:",
hint: `Type "open" to open journal`,
})
if (thought === "open") {
await openThoughtFile()
} else {
await addThought(thought)
}
}
// Menu: Reddit
// Description: Browse Reddit from Script Kit
// Author: John Lindquist
// Twitter: @johnlindquist
let Reddit = await npm("reddit")
let envOptions = {
ignoreBlur: true,
hint: md(
`[Create a reddit app](https://www.reddit.com/prefs/apps)`
),
secret: true,
}
let reddit = new Reddit({
username: await env("REDDIT_USERNAME"),
password: await env("REDDIT_PASSWORD"),
appId: await env("REDDIT_APP_ID", envOptions),
appSecret: await env("REDDIT_APP_SECRET", envOptions),
userAgent: `ScriptKit/1.0.0 (https://scriptkit.com)`,
})
let subreddits = [
"funny",
"aww",
"dataisbeautiful",
"mildlyinteresting",
"RocketLeague",
]
subreddits.forEach(sub => {
onTab(sub, async () => {
let url = await arg(
"Select post to open:",
async () => {
let best = await reddit.get(`/r/${sub}/hot`)
return best.data.children.map(({ data }) => {
let {
title,
thumbnail,
url,
subreddit_name_prefixed,
preview,
} = data
let resolutions =
preview?.images?.[0]?.resolutions
let previewImage =
resolutions?.[resolutions?.length - 1]?.url
return {
name: title,
description: subreddit_name_prefixed,
value: url,
img: thumbnail,
...(previewImage && {
preview: md(`
![${title}](${previewImage})
### ${title}
`),
}),
}
})
}
)
exec(`open "${url}"`)
})
})
// Menu: Share Selected File
// Description: Select a file in Finder. Creates tunnel and copies link to clipboard.
// Author: John Lindquist
// Twitter: @johnlindquistt
// Background: true
let ngrok = await npm("ngrok")
let handler = await npm("serve-handler")
let exitHook = await npm("exit-hook")
let http = await import("http")
let filePath = await getSelectedFile()
let symLinkName = _.last(
filePath.split(path.sep)
).replaceAll(" ", "-")
let symLinkPath = tmp(symLinkName)
console.log(`Creating temporary symlink: ${symLinkPath}`)
ln(filePath, symLinkPath)
let port = 3033
const server = http.createServer(handler)
cd(tmp())
server.listen(port, async () => {
let tunnel = await ngrok.connect(port)
let shareLink = tunnel + "/" + symLinkName
console.log(
chalk`{yellow ${shareLink}} copied to clipboard`
)
copy(shareLink)
})
exitHook(() => {
server.close()
if (test("-f", symLinkPath)) {
console.log(
`Removing temporary symlink: ${symLinkPath}`
)
exec(`rm ${symLinkPath}`)
}
})
// Menu: Speak Script
// Description: Run a Script based on Speech Input
// Author: John Lindquist
// Twitter: @johnlindquist
let { scripts } = await db("scripts")
let escapedScripts = scripts.map(script => ({
name: `"${script.name.replace(/"/g, '\\"')}"`, //escape quotes
value: script.filePath,
}))
let speakableScripts = escapedScripts
.map(({ name }) => name)
.join(",")
let speech = await applescript(String.raw`
tell application "SpeechRecognitionServer"
listen for {${speakableScripts}}
end tell
`)
let script = escapedScripts.find(
script => script.name == `"${speech}"`
)
await run(script.value)
// Menu: Speed Reader
// Description: Display clipboard content at a defined rate
// Author: John Lindquist
// Twitter: @johnlindquist
let wpm = 1000 * (60 / (await arg('Enter words per minute:')))
let text = await paste()
text = text
.trim()
.split(' ')
.filter(Boolean)
.flatMap((sentence) => sentence.trim().split(' '))
let i = 0
let id = setInterval(() => {
setPlaceholder(` ${text[i++]}`)
if (i >= text.length) clearInterval(id)
}, wpm)
// Menu: Synonym
// Description: List synonyms
// Author: John Lindquist
// Twitter: @johnlindquist
let synonym = await arg("Type a word", async input => {
if (!input || input?.length < 3) return []
let url = `https://api.datamuse.com/words?ml=${input}&md=d`
let response = await get(url)
return response.data.map(({ word, defs }) => {
return {
name: `${word}${defs?.[0] && ` - ${defs[0]}`}`,
value: word,
selected: `Paste ${word}`,
}
})
})
setSelectedText(synonym)
// Menu: Update Twitter Name
// Description: Change your name on twitter
// Author: John Lindquist
// Twitter: @johnlindquist
let Twitter = await npm('twitter-lite')
let envOptions = {
hint: md(
`You need to [create an app](https://developer.twitter.com/en/apps) to get these keys/tokens`,
),
ignoreBlur: true,
secret: true,
}
let client = new Twitter({
consumer_key: await env('TWITTER_CONSUMER_KEY', envOptions),
consumer_secret: await env('TWITTER_CONSUMER_SECRET', envOptions),
access_token_key: await env('TWITTER_ACCESS_TOKEN_KEY', envOptions),
access_token_secret: await env('TWITTER_ACCESS_TOKEN_SECRET', envOptions),
})
let name = await arg('Enter new twitter name:')
let response = await client
.post('account/update_profile', {
name,
})
.catch((error) => console.log(error))
// Menu: Vocab Quiz
// Description: Quiz on random vocab words
// Author: John Lindquist
// Twitter: @johnlindquist
await npm("wordnet-db")
let randomWord = await npm("random-word")
let { WordNet } = await npm("natural")
let wordNet = new WordNet()
let words = []
while (true) {
setPlaceholder(`Finding random word and definitions...`)
while (words.length < 4) {
let quizWord = randomWord()
let results = await new Promise(resolve => {
wordNet.lookup(quizWord, resolve)
})
if (results.length) {
let [{ lemma, def }] = results
words.push({ name: def, value: lemma })
}
}
let word = words[0]
let result = await arg(
`What does "${word.value}" mean?`,
_.shuffle(words)
)
let correct = word.value === result
setPlaceholder(
`${correct ? "✅" : "🚫"} ${word.value}: ${word.name}`
)
words = []
await wait(2000)
}
// Menu: Word API
// Description: Queries a word api. Pastes selection.
// Author: John Lindquist
// Twitter: @johnlindquist
let typeMap = {
describe: "rel_jjb",
trigger: "rel_trg",
noun: "rel_jja",
follow: "lc",
rhyme: "rel_rhy",
spell: "sp",
synonym: "ml",
sounds: "rel_nry",
suggest: "suggest",
}
let word = await arg("Type a word and hit Enter:")
let typeArg = await arg(
"What would you like to find?",
Object.keys(typeMap)
)
let type = typeMap[typeArg]
word = word.replace(/ /g, "+")
let url = `https://api.datamuse.com/words?${type}=${word}&md=d`
if (typeArg == "suggest")
url = `https://api.datamuse.com/sug?s=${word}&md=d`
let response = await get(url)
let formattedWords = response.data.map(({ word, defs }) => {
let info = ""
if (defs) {
let [type, meaning] = defs[0].split("\t")
info = `- (${type}): ${meaning}`
}
return {
name: `${word}${info}`,
value: word,
}
})
let pickWord = await arg("Select to paste:", formattedWords)
setSelectedText(pickWord)