// Name: Merge / Split Alfred clipboard// Description: Merge or split clipboard content using Alfred app's clipboardimport "@johnlindquist/kit"const Database = await npm("better-sqlite3");const databasePath = home('Library/Application Support/Alfred/Databases/clipboard.alfdb')if (!await pathExists(databasePath)) {notify("Alfred clipboard database not found" )exit()}const db = new Database(databasePath);const queryClipboard = async (sql, params) => {const stmt = db.prepare(sql);return sql.trim().toUpperCase().startsWith("SELECT") ? stmt.all(params) : stmt.run(params);};const getMergedClipboards = async (count, separator) => {const sql = `SELECT item FROM clipboard WHERE dataType = 0 order by ROWID desc LIMIT ?`;const clipboards = await queryClipboard(sql, [count]);return clipboards.map(row => row.item.trim()).join(separator);};const writeMergedClipboards = async (mergedText) => {await clipboard.writeText(mergedText);};const getSplitClipboard = async (separator, trim) => {const currentClipboard = await clipboard.readText();return currentClipboard.split(separator).map(item => trim ? item.trim() : item);};const writeSplitClipboard = async (splitText) => {const lastTsSql = `SELECT ts FROM clipboard WHERE dataType = 0 ORDER BY ts DESC LIMIT 1`;const lastTsResult = await queryClipboard(lastTsSql, []);let lastTs = lastTsResult.length > 0 ? Number(lastTsResult[0].ts) : 0;const insertSql = `INSERT INTO clipboard (item, ts, dataType, app, appPath) VALUES (?, ?, 0, 'Kit', '/Applications/Kit.app')`;for (let i = 0; i < splitText.length - 1; i++) {lastTs += 1;await queryClipboard(insertSql, [splitText[i], lastTs]);}await clipboard.writeText(splitText[splitText.length - 1]);};const action = await arg("Choose action", ["Merge", "Split"]);if (action === "Merge") {const count = await arg({placeholder: "Enter the number of clipboard items to merge",}, async (input) => {if (isNaN(Number(input)) || input.length === 0)return ''return md(`<pre>${await getMergedClipboards(input, '\n')}</pre>`)})const separator = await arg({placeholder: "Enter the separator for merging",}, async (input) => {if (input === '\\n') input = '\n'return md(`<pre>${await getMergedClipboards(count, input)}</pre>`)})const mergedText = await getMergedClipboards(count, separator);await writeMergedClipboards(mergedText);await notify("Merged clipboard items and copied to clipboard");} else {// const separator = await arg("Enter the separator for splitting");const separator = await arg({placeholder: "Enter the separator for splitting",}, async (input) => {if (input === '\\n') input = '\n'let strings = await getSplitClipboard(input, true);return md(`<pre>${strings.join('\n')}</pre>`)})const trim = await arg("Trim clipboard content?", ["Yes", "No"]);const splitText = await getSplitClipboard(separator, trim === "Yes");await writeSplitClipboard(splitText);await notify("Split clipboard content and stored in Alfred clipboard");}db.close();
// Name: Type Clipboard// Description: Get the content of the clipboard and "keystroke" it without pasting// Shortcut: ctrl+cmd+alt+shift+vimport "@johnlindquist/kit"const clipboardText = await clipboard.readText()if (clipboardText.length > 1000) {await notify("Clipboard content is too long")exit()}await applescript(String.raw`set chars to count (get the clipboard)tell application "System Events"delay 0.1keystroke (get the clipboard)end tell`)
// Name: Open in WhatsAppimport "@johnlindquist/kit"//get the text from the clipboardlet text = await clipboard.readText();//normalize the texttext = text.replace(/[-() ]/g, "");//validate if valid phone numberif (!text.match(/^(\+\d{12,13})|(\d{10,11})$/)) {notify("Invalid phone number");exit()}//assume Argentina if no country code since that's where I'm fromif (!text.startsWith("+")) {text = "+54" + text;}//open in WhatsAppopen(`https://wa.me/${text}`);
// Name: convert selected imagesimport "@johnlindquist/kit";// Grab selected filesconst files = (await getSelectedFile()).split("\n");// Set up whitelist of formatsconst supportedFormats = [".heic", ".png", ".gif", ".webp", ".jpg", ".jpeg"];// Filter files based on supported formatsconst selectedFiles = files.filter(file =>supportedFormats.some(format => file.toLowerCase().endsWith(format)));// Notify if no files are selectedif (!selectedFiles.length) {await notify("No supported files selected");exit();}const convertHeic = await npm("heic-convert");const sharp = await npm("sharp");// Select the output formatconst outputFormat = await arg("Choose an output format", ["jpg","png","webp",]);const getUniquePath = async (outputPath, suffix = "") => {if (await isFile(outputPath)) {const name = path.basename(outputPath, path.extname(outputPath));const newName = `${name}${suffix}-copy${path.extname(outputPath)}`;const newPath = path.join(path.dirname(outputPath), newName);return await getUniquePath(newPath, `${suffix}-copy`);} else {return outputPath;}};// Convert selected files to the chosen output format using appropriate librariesfor (const file of selectedFiles) {const content = await readFile(file);const name = path.basename(file).split(".")[0];const outputPath = path.join(path.dirname(file), name + `.${outputFormat}`);const uniqueOutputPath = await getUniquePath(outputPath);if (file.toLowerCase().endsWith(".heic")) {const formatMap = {jpg: "JPEG",png: "PNG",}const outputBuffer = await convertHeic({buffer: content,format: formatMap[outputFormat],quality: 0.5,});await writeFile(uniqueOutputPath, outputBuffer);} else {const sharpImage = sharp(content);switch (outputFormat) {case "jpg":await sharpImage.jpeg({ quality: 40 }).toFile(uniqueOutputPath);break;case "png":await sharpImage.png().toFile(uniqueOutputPath);break;case "webp":await sharpImage.webp({ quality: 40 }).toFile(uniqueOutputPath);break;}}}await notify(`Converted selected files to ${outputFormat.toUpperCase()}`);
// Name: Open URL in clipboardimport "@johnlindquist/kit"//get the clipboardlet text = await clipboard.readText();//get the first URL in the clipboard, if anylet url = text.match(/(https?:\/\/[^\s]+)/);//if there's a URL, open itif (url) {open(url[0]);} else {notify("No URL found in clipboard");}
// Name: Emoji Search// Description: Search and copy emoji to clipboard using SQLite databaseimport "@johnlindquist/kit"const Database = await npm("better-sqlite3")const databaseFile = projectPath("db", "emoji-search-emojilib.db")const emojilibURL = "https://raw.githubusercontent.com/muan/emojilib/main/dist/emoji-en-US.json"const createDatabase = async () => {const response = await get(emojilibURL)const emojiData = response.data as Record<string, string[]>//create db and tableconst db = new Database(databaseFile)db.exec(`CREATE TABLE IF NOT EXISTS emojis(emoji TEXT PRIMARY KEY, name TEXT, keywords TEXT, used INTEGER DEFAULT 0)`)//populate with data from emojilibfor (const [emojiChar, emojiInfo] of Object.entries(emojiData)) {const description = emojiInfo[0]const tags = emojiInfo.slice(1).join(', ')db.prepare("INSERT OR REPLACE INTO emojis VALUES (?, ?, ?, 0)").run(emojiChar, description, tags)}db.close()};if (!await pathExists(databaseFile)) {await createDatabase()}const db = new Database(databaseFile)const queryEmojis = async () => {const sql = "SELECT emoji, name, keywords FROM emojis ORDER BY used DESC"const stmt = db.prepare(sql)return stmt.all()}const snakeToHuman = (text) => {return text.split('_').map((word, index) => index === 0 ? word.charAt(0).toUpperCase() + word.slice(1) : word).join(' ')}const emojis = await queryEmojis()const selectedEmoji = await arg("Search Emoji", emojis.map(({ emoji, name, keywords }) => ({name: `${snakeToHuman(name)} ${keywords}`,html: md(`<div class="flex items-center"><span class="text-5xl">${emoji}</span><div class="flex flex-col ml-2"><span class="text-2xl" style="color: lightgrey">${snakeToHuman(name)}</span><small style="color: darkgrey">${keywords}</small></div></div>`),value: emoji,})))await clipboard.writeText(selectedEmoji)// Update the 'used' countconst updateSql = "UPDATE emojis SET used = used + 1 WHERE emoji = ?"const updateStmt = db.prepare(updateSql)updateStmt.run(selectedEmoji)db.close()
// Name: Text Manipulation// Description: Transform clipboard text based on user-selected optionsimport "@johnlindquist/kit"let transformations = {upperCase: text => text.toUpperCase(),lowerCase: text => text.toLowerCase(),capitalize: text => text.split('\n').map(line => line.split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ')).join('\n'),decodeUrl: text => text.split('\n').map(line => decodeURIComponent(line)).join('\n'),snakeCase: text => text.split('\n').map(line => line.replace(/[\s-_]+(\w)/g, (_, p) => `_${p.toLowerCase()}`).replace(/^[A-Z]/, match => match.toLowerCase())).join('\n'),camelCase: text => text.split('\n').map(line => line.replace(/[\s-_]+(\w)/g, (_, p) => p.toUpperCase()).replace(/^[A-Z]/, match => match.toLowerCase())).join('\n'),kebabCase: text => text.split('\n').map(line => line.replace(/[\s-_]+(\w)/g, (_, p) => `-${p.toLowerCase()}`).replace(/^[A-Z]/, match => match.toLowerCase())).join('\n'),reverseCharacters: text => text.split('\n').map(line => line.split('').reverse().join('')).join('\n'),removeDuplicateLines: text => {let lines = text.split('\n');return [...new Set(lines)].join('\n');},keepOnlyDuplicateLines: text => {let lines = text.split('\n');let duplicates = lines.filter((item, index) => lines.indexOf(item) !== index);return [...new Set(duplicates)].join('\n');},removeEmptyLines: text => text.split('\n').filter(line => line.trim() !== '').join('\n'),removeAllNewLines: text => text.split('\n').map(line => line.trim()).join(''),trimEachLine: text => text.split('\n').map(line => line.trim()).join('\n'),sortLinesAlphabetically: text => text.split('\n').sort().join('\n'),sortLinesNumerically: text => text.split('\n').sort((a, b) => a - b).join('\n'),reverseLines: text => text.split('\n').reverse().join('\n'),shuffleLines: text => {let lines = text.split('\n')for (let i = lines.length - 1; i > 0; i--) {let j = Math.floor(Math.random() * (i + 1))let temp = lines[i]lines[i] = lines[j]lines[j] = temp}return lines.join('\n')},joinBy: (text, separator) => text.split('\n').join(separator),splitBy: (text, separator) => text.split(separator).join('\n'),removeWrapping: text => {const lines = text.split('\n');const matchingPairs = [['(', ')'], ['[', ']'], ['{', '}'], ['<', '>'], ['"', '"'], ["'", "'"]];return lines.map(line => {const firstChar = line.charAt(0);const lastChar = line.charAt(line.length - 1);for (const [open, close] of matchingPairs) {if (firstChar === open && lastChar === close) {return line.slice(1, -1);}}if (firstChar === lastChar) {return line.slice(1, -1);}return line;}).join('\n');},wrapEachLine: (text, wrapper) => {const lines = text.split('\n');return lines.map(line => `${wrapper}${line}${wrapper}`).join('\n');},captureEachLine: (text, regex) => {const lines = text.split('\n');const pattern = new RegExp(regex);return lines.map(line => {const match = line.match(pattern);return match ? match[0] : '';}).join('\n');},removeLinesMatching: (text, regex) => {if (regex.length === 0) return text;const lines = text.split('\n');const pattern = new RegExp(regex, 'i');return lines.filter(line => !pattern.test(line)).join('\n');},keepLinesMatching: (text, regex) => {if (regex.length === 0) return text;const lines = text.split('\n');const pattern = new RegExp(regex, 'i')return lines.filter(line => pattern.test(line)).join('\n');},prependTextToAllLines: (text, prefix) => {const lines = text.split('\n');return lines.map(line => prefix + line).join('\n');},appendTextToAllLines: (text, suffix) => {const lines = text.split('\n');return lines.map(line => line + suffix).join('\n');},replaceRegexInAllLines: (text, regexWithReplacement) => {const [regex, replacement] = regexWithReplacement.split('|');const pattern = new RegExp(regex, 'g');const lines = text.split('\n');return lines.map(line => line.replace(pattern, replacement)).join('\n');},removeRegexInAllLines: (text, regex) => {const pattern = new RegExp(regex, 'g');const lines = text.split('\n');return lines.map(line => line.replace(pattern, '')).join('\n');},generateNumberedList: (text) => {const lines = text.split('\n');return lines.map((line, index) => `${index + 1}. ${line}`).join('\n');},noop: text => text,}let options = [// Existing options here{name: "Decode URL", description: "Decode a URL-encoded text", value: {key: "decodeUrl"}},{name: "Upper Case",description: "Transform the entire text to upper case",value: {key: "upperCase",},},{name: "Lower Case",description: "Transform the entire text to lower case",value: {key: "lowerCase",},},{name: "snake_case", description: "Convert text to snake_case", value: {key: "snakeCase"}},{name: "Capitalize", description: "Convert text to Capital Case", value: {key: "capitalize"}},{name: "camelCase", description: "Convert text to camelCase", value: {key: "camelCase"}},{name: "kebab-case", description: "Convert text to kebab-case", value: {key: "kebabCase"}},{name: "Reverse Characters", description: "Reverse the characters in the text", value: {key: "reverseCharacters"}},{name: "Remove Duplicate Lines",description: "Remove duplicate lines from the text",value: {key: "removeDuplicateLines"}},{name: "Keep Only Duplicate Lines",description: "Keep only duplicate lines in the text",value: {key: "keepOnlyDuplicateLines"}},{name: "Remove Empty Lines", description: "Remove empty lines from the text", value: {key: "removeEmptyLines"}},{name: "Remove All New Lines", description: "Remove all new lines from the text", value: {key: "removeAllNewLines"}},{name: "Trim Each Line",description: "Trim whitespace from the beginning and end of each line",value: {key: "trimEachLine"}},{name: "Sort Lines Alphabetically", description: "Sort lines alphabetically", value: {key: "sortLinesAlphabetically"}},{name: "Sort Lines Numerically", description: "Sort lines numerically", value: {key: "sortLinesNumerically"}},{name: "Reverse Lines", description: "Reverse the order of lines", value: {key: "reverseLines"}},{name: "Shuffle Lines", description: "Randomly shuffle the order of lines", value: {key: "shuffleLines"}},{name: "Join By",description: "Join lines by a custom separator",value: {key: "joinBy",parameter: {name: "Separator",description: "Enter a separator to join lines",defaultValue: ",",},},},{name: "Split By",description: "Split lines by a custom separator",value: {key: "splitBy",parameter: {name: "Separator",description: "Enter a separator to split lines",},},},{name: "Remove Wrapping",description: "Remove wrapping characters from each line",value: {key: "removeWrapping",},},{name: "Wrap Each Line With",description: "Wrap each line with a custom character or string",value: {key: "wrapEachLine",parameter: {name: "Wrapper",description: "Enter a wrapper for each line",defaultValue: '"',},},},{name: "Capture Each Line",description: "Capture and return the first match of a regex pattern in each line",value: {key: "captureEachLine",parameter: {name: "Pattern",description: "Enter a regex pattern to capture",defaultValue: "\\d+",},},},{name: "Remove Lines Matching",description: "Remove lines that match the given regex",value: {key: "removeLinesMatching",parameter: {name: "Regex",description: "Enter a regex to match lines to remove",defaultValue: '',},},},{name: "Keep Lines Matching",description: "Keep lines that match the given regex",value: {key: "keepLinesMatching",parameter: {name: "Regex",description: "Enter a regex to match lines to keep",defaultValue: '',},},},{name: "Prepend Text to All Lines",description: "Add text to the beginning of all lines",value: {key: "prependTextToAllLines",parameter: {name: "Text",description: "Enter text to prepend to all lines",defaultValue: '',},},},{name: "Append Text to All Lines",description: "Add text to the end of all lines",value: {key: "appendTextToAllLines",parameter: {name: "Text",description: "Enter text to append to all lines",defaultValue: '',},},},{name: "Replace Regex in All Lines",description: "Replace regex matches in all lines with specified text",value: {key: "replaceRegexInAllLines",parameter: {name: "Regex and Replacement",description: "Enter regex and replacement text separated by a '|'",defaultValue: '',},},},{name: "Generate Numbered List",description: "Prepend numbers to each line",value: {key: "generateNumberedList",},},{name: "Remove Regex In All Lines",description: "Remove matches of the provided regex in all lines",value: {key: "removeRegexInAllLines",parameter: {name: "Regex",description: "Enter a regex to remove from all lines",},},},{name: "No Operation",description: "Do nothing to the text, if you accidentally hit Cmd + enter and need no more transformations",}]const handleTransformation = async (text, transformation) => {let {key, parameter} = transformation;let paramValue = parameter ? await arg({input: parameter.defaultValue,}, (input) => md(`<pre>${transformations[key](text, input)}</pre>`)) : null;return transformations[key](text, paramValue);};let flags = {rerun: {name: "Rerun",shortcut: "cmd+enter",},}let clipboardText = await clipboard.readText()let operations: string[] = []let rerun = true;while (rerun) {let transformation = await arg({placeholder: "Choose a text transformation (Cmd + enter to rerun)",flags,hint: operations.join(' > '),},options.sort((a, b) => a.name.localeCompare(b.name)).map(option => {return {...option,preview: () => {try {if (option.value.parameter) throw '';return md(`<pre>${transformations[option.value.key](clipboardText)}</pre>`)} catch (e) {return '...'}},}}))rerun = flag?.rerun as boolean;clipboardText = await handleTransformation(clipboardText, transformation);operations.push(transformation.key);}await clipboard.writeText(clipboardText)await notify("Text transformation applied and copied to clipboard")
// Name: OCR// Description: Capture a screenshot and recognize the text using tesseract.jsimport "@johnlindquist/kit";//both win and linux implementations were created by chatgpt (gpt4), without _any_ tests!! 😅const captureScreenshot = async () => {const tmpFile = `/tmp/screenshot-${Date.now()}.png`;if (isMac) {await exec(`screencapture -i ${tmpFile}`);} else if (isWin) {const psScript = `Add-Type -AssemblyName System.Windows.Forms[System.Windows.Forms.SendKeys]::SendWait('%{PRTSC}')Start-Sleep -m 500$clipboardData = Get-Clipboard -Format Image$clipboardData.Save('${tmpFile}', [System.Drawing.Imaging.ImageFormat]::Png)`;await exec(`powershell -Command "${psScript.replace(/\n/g, '')}"`);} else if (isLinux) {// Check if gnome-screenshot is availabletry {await exec('gnome-screenshot --version');await exec(`gnome-screenshot -f ${tmpFile}`);} catch (error) {// If gnome-screenshot is not available, try using ImageMagick's 'import' commandawait exec(`import ${tmpFile}`);}}return tmpFile;};const recognizeText = async (filePath, language) => {const { createWorker } = await npm("tesseract.js");const worker = await createWorker();await worker.loadLanguage(language);await worker.initialize(language);const { data } = await worker.recognize(filePath);await worker.terminate();return data.text;};const languages = [{ name: "Spanish", value: "spa" },{ name: "French", value: "fra" },{ name: "Portuguese", value: "por" },{ name: "English", value: "eng" },];//@todo train a model for typescript (https://github.com/tesseract-ocr/tesstrain)// if ctrl is pressed, show a modal to select a languageconst selectedLanguage = flag.ctrl? await arg("Select a language:", languages): "eng";// Hide the Kit modal before capturing the screenshotawait hide();const filePath = await captureScreenshot();if (!await pathExists(filePath)) exit()const text = await recognizeText(filePath, selectedLanguage);if (text) {await clipboard.writeText(text.trim());await notify("Text recognized and copied to clipboard");} else {await notify("No text found in the screenshot");}// Clean up temporary fileawait remove(filePath);