Ramiro Araujo

Ramiro Araujo

// Name: Merge / Split Alfred clipboard
// Description: Merge or split clipboard content using Alfred app's clipboard
import "@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+v
import "@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.1
keystroke (get the clipboard)
end tell
`)
// Name: Open in WhatsApp
import "@johnlindquist/kit"
//get the text from the clipboard
let text = await clipboard.readText();
//normalize the text
text = text.replace(/[-() ]/g, "");
//validate if valid phone number
if (!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 from
if (!text.startsWith("+")) {
text = "+54" + text;
}
//open in WhatsApp
open(`https://wa.me/${text}`);
// Name: convert selected images
import "@johnlindquist/kit";
// Grab selected files
const files = (await getSelectedFile()).split("\n");
// Set up whitelist of formats
const supportedFormats = [".heic", ".png", ".gif", ".webp", ".jpg", ".jpeg"];
// Filter files based on supported formats
const selectedFiles = files.filter(file =>
supportedFormats.some(format => file.toLowerCase().endsWith(format))
);
// Notify if no files are selected
if (!selectedFiles.length) {
await notify("No supported files selected");
exit();
}
const convertHeic = await npm("heic-convert");
const sharp = await npm("sharp");
// Select the output format
const 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 libraries
for (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: Emoji Search
// Description: Search and copy emoji to clipboard using SQLite database
import "@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 table
const 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 emojilib
for (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' count
const 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 options
import "@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.js
import "@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 available
try {
await exec('gnome-screenshot --version');
await exec(`gnome-screenshot -f ${tmpFile}`);
} catch (error) {
// If gnome-screenshot is not available, try using ImageMagick's 'import' command
await 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 language
const selectedLanguage = flag.ctrl
? await arg("Select a language:", languages)
: "eng";
// Hide the Kit modal before capturing the screenshot
await 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 file
await remove(filePath);