// Name: Get GitHub Commits Messages Since Tag// Description: Get all commit messages since a tag// Author: John Lindquist// Twitter: @johnlindquistimport "@johnlindquist/kit"let { Octokit } = await import("@octokit/rest")let ownerRepo = await arg("Enter username/repo. Example: johnlindquist/kit")let [owner, repo] = ownerRepo.split("/")let tag = await arg("Tag. Example: v1.54.53")let client = new Octokit({auth: await env("GITHUB_PERSONAL_ACCESS_TOKEN"),})let page = 1let hasMorePages = truelet messages = []let ref = nulllet tagPage = 1while (!ref) {let listTags = await client.repos.listTags({owner,repo,per_page: 100,name: tag,page: tagPage,})tagPage++ref = listTags.data.find(t => t.name === tag).commit.sha}let commit = await client.repos.getCommit({owner,repo,ref,})let since = commit.data.commit.author.datewhile (hasMorePages) {let data = await client.repos.listCommits({owner,repo,since,per_page: 100,page: page,})hasMorePages = data.data.length === 100messages = messages.concat(data.data.map(c => c.commit.message))page++}let text = messages.join("\n\n")if (env?.["GITHUB_SCRIPTKIT_TOKEN"]) {let response = await createGist(text, {description: `Commit messages since ${tag}`,isPublic: false,fileName: "commit-messages.txt",})open(response.html_url)debugger} else {await editor(text)}
// Name: Open Recent VS Code Projectimport "@johnlindquist/kit"import { URL, fileURLToPath } from "url"// /Users/johnlindquist/Library/Application Support/Code/User/globalStorage/state.vscdblet filename = home("Library", "Application Support", "Code", "User", "globalStorage", "state.vscdb")// windows path not tested, just guessingif (isWin) filename = home("AppData", "Roaming", "Code", "User", "globalStorage", "state.vscdb")let { default: sqlite3 } = await import("sqlite3")let { open } = await import("sqlite")const db = await open({filename,driver: sqlite3.Database,})let key = `history.recentlyOpenedPathsList`let table = `ItemTable`let result = await db.get(`SELECT * FROM ${table} WHERE key = '${key}'`)let recentPaths = JSON.parse(result.value)recentPaths = recentPaths.entries.map(e => e?.folderUri).filter(Boolean).map(uri => fileURLToPath(new URL(uri)))let recentPath = await arg("Open a recent path", recentPaths)hide()await exec(`code ${recentPath}`)
// Name: Paste Clipboard Image as Cloudinary Markdown URL// Shortcut: opt shift vimport "@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 wantlet markdown = ``await setSelectedText(markdown)} else {await div(md(`# No Image in Clipboard`))}
// Name: Silent Mention// Shortcut: opt ximport "@johnlindquist/kit"let makeSilent = (str: string) =>str.replace(/[.#@]/g, m => m + "\u2060")let text =(await getSelectedText()) ||(await arg("Enter text to silent"))let silentText = makeSilent(text)await setSelectedText(silentText)
// Name: Screenshot Current Tweetimport "@johnlindquist/kit"const { chromium }: typeof import("playwright") = await npm("playwright")let url = await getActiveTab()let timeout = 5000let headless = falseconst 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()
sl y
// Name: Sleep on Shortcode// Shortcode: slimport "@johnlindquist/kit"let confirm = await arg({placeholder: `Sleep system?`,// Script Kit parses hints and assigns single key shortcuts to single letters inside of []hint: `[y]/[n]`,})if (confirm === "y") {sleep()}
// Name: Theme Creator// This will create a file at ~/.kenv/theme.txt// Edit the file, then hit save to update the themeimport "@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 Colorimport "@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: Edit in VIMimport "@johnlindquist/kit"let text = await getSelectedText()let vimTextPath = tmpPath("vim.txt")await writeFile(vimTextPath, text)await term(`vim ${vimTextPath}`)let contents = await readFile(vimTextPath, "utf-8")await setSelectedText(contents)
// Name: Widget Dynamic Listsimport "@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"><buttonclass="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)
// Menu: Search Anime
// Description: Use the jikan.moe API to search anime
// Author: John Lindquist
// Twitter: @johnlindquist
let anime = await arg("Anime:")
let response = await get(
  `https://api.jikan.moe/v3/search/anime?q=${anime}`
)
let { image_url, title } = response.data.results[0]
showImage(image_url, { title })
// 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: Center App
// Description: Center the frontmost app
// Author: John Lindquist
// Twitter: @johnlindquist
let { workArea, bounds } = await getActiveScreen()
let { width, height } = workArea
let { x, y } = bounds
let padding = 100
let top = y + padding
let left = x + padding
let right = x + width - padding
let bottom = y + height - padding
setActiveAppBounds({
  top,
  left,
  right,
  bottom,
})
// 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: Chrome Tab Switcher
// Description: List all Chrome tabs. Then switch to that tab
// Author: John Lindquist
// Twitter: @johnlindquist
let tabs = await getTabs()
let url = await arg(
  "Select Chrome tab:",
  tabs.map(({ url, title }) => ({
    name: url,
    value: url,
    description: title,
  }))
)
focusTab(url)
// Description: Launch a url in Chrome. If url is already open, switch to that tab.
// Author: John Lindquist
// Twitter: @johnlindquist
let url = await arg("Enter url:")
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: John's personal startup script for scriptkit.com
// Description: This probably won't run on your machine 😜
// Author: John Lindquist
// Twitter: @johnlindquist
edit(`~/projects/scriptkit.com`)
iterm(`cd ~/projects/scriptkit.com; vercel dev`)
await focusTab("http://localhost:3000")
// 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}`)
// Description: Launch Twitter in Chrome. If Twitter is already open, switch to that tab.
// Author: John Lindquist
// Twitter: @johnlindquist
// Shortcut: opt t
//runs the "chrome-tab" script with twitter.com passed into the first `arg`
await run("chrome-tab", "twitter.com")
// 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: ``,
  },
  {
    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: Hello World
// Description: Enter an name, speak it back
// Author: John Lindquist
// Twitter: @johnlindquist
let name = await arg(`What's your name?`)
say(`Hello, ${name}!`)
// 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: Dad Joke
// Description: Logs out a Dad Joke from icanhazdadjoke.com
// Author: John Lindquist
// Twitter: @johnlindquist
let response = await get(`https://icanhazdadjoke.com/`, {
  headers: {
    Accept: "text/plain",
  },
})
let joke = response.data
setPanel(joke)
say(joke)
// 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: Paste URL
// Description: Copy the current URL from your browser. Paste it at cursor.
// Author: John Lindquist
// Twitter: @johnlindquist
let url = await getActiveTab()
await setSelectedText(url)
// Menu: Project Name
// Description: Generate an alliteraive, dashed project name, copies it to the clipboard, and shows a notification
// Author: John Lindquist
// Twitter: @johnlindquist
let { generate } = await npm("project-name-generator")
const name = generate({
  word: 2,
  alliterative: true,
}).dashed
await setSelectedText(name)
// 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: Read News
// Description: Scrape headlines from news.google.com then pick headline to read
// Author: John Lindquist
// Twitter: @johnlindquist
let headlines = await scrapeSelector(
  "https://news.google.com",
  "h3",
  el => ({
    name: el.innerText,
    value: el.firstChild.href,
  })
)
let url = await arg("What do you want to read?", headlines)
exec(`open "${url}"`)
// 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}          
                `),
            }),
          }
        })
      }
    )
    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: Open Sound Prefs
// Description: Open the Sound prefs panel
// Author: John Lindquist
// Twitter: @johnlindquist
exec(`open /System/Library/PreferencePanes/Sound.prefPane`)
// 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: Title Case
// Description: Converts the selected text to title case
// Author: John Lindquist
// Twitter: @johnlindquist
let { titleCase } = await npm("title-case")
let text = await getSelectedText()
let titleText = titleCase(text)
await setSelectedText(titleText)
// 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)