import { exec } from "child_process";
import { promisify } from "util";
import * as path from "path";
const execAsync = promisify(exec);
// Execute AppleScript and return result
async function runAppleScript(script: string): Promise<string> {
try {
const { stdout } = await execAsync(`osascript -e '${script.replace(/'/g, "'\"'\"'")}'`);
return stdout.trim();
} catch (error) {
const err = error as Error & { stderr?: string };
throw new Error(`AppleScript error: ${err.stderr || err.message}`);
}
}
// Escape string for AppleScript
function escapeForAppleScript(str: string): string {
return str.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
}
// ============ FILE DIALOGS ============
export interface PickFileOptions {
prompt?: string;
defaultLocation?: string;
fileTypes?: string[];
}
export async function pickFile(options?: PickFileOptions): Promise<string | null> {
const prompt = escapeForAppleScript(options?.prompt || "Select a file");
let locationClause = "";
if (options?.defaultLocation) {
const absPath = path.resolve(options.defaultLocation);
locationClause = `default location POSIX file "${escapeForAppleScript(absPath)}"`;
}
let typeClause = "";
if (options?.fileTypes && options.fileTypes.length > 0) {
const types = options.fileTypes.map(t => `"${escapeForAppleScript(t)}"`).join(", ");
typeClause = `of type {${types}}`;
}
const script = `
try
set chosen_file to choose file with prompt "${prompt}" ${locationClause} ${typeClause}
return POSIX path of chosen_file
on error
return "CANCELLED"
end try
`.trim();
const result = await runAppleScript(script);
return result === "CANCELLED" ? null : result;
}
export interface PickFolderOptions {
prompt?: string;
defaultLocation?: string;
}
export async function pickFolder(options?: PickFolderOptions): Promise<string | null> {
const prompt = escapeForAppleScript(options?.prompt || "Select a folder");
let locationClause = "";
if (options?.defaultLocation) {
const absPath = path.resolve(options.defaultLocation);
locationClause = `default location POSIX file "${escapeForAppleScript(absPath)}"`;
}
const script = `
try
set chosen_folder to choose folder with prompt "${prompt}" ${locationClause}
return POSIX path of chosen_folder
on error
return "CANCELLED"
end try
`.trim();
const result = await runAppleScript(script);
return result === "CANCELLED" ? null : result;
}
export interface PickFilesOptions {
prompt?: string;
defaultLocation?: string;
fileTypes?: string[];
}
export async function pickFiles(options?: PickFilesOptions): Promise<string[] | null> {
const prompt = escapeForAppleScript(options?.prompt || "Select files");
let locationClause = "";
if (options?.defaultLocation) {
const absPath = path.resolve(options.defaultLocation);
locationClause = `default location POSIX file "${escapeForAppleScript(absPath)}"`;
}
let typeClause = "";
if (options?.fileTypes && options.fileTypes.length > 0) {
const types = options.fileTypes.map(t => `"${escapeForAppleScript(t)}"`).join(", ");
typeClause = `of type {${types}}`;
}
const script = `
try
set chosen_files to choose file with prompt "${prompt}" ${locationClause} ${typeClause} with multiple selections allowed
set file_list to {}
repeat with f in chosen_files
set end of file_list to POSIX path of f
end repeat
set AppleScript's text item delimiters to "|||"
return file_list as text
on error
return "CANCELLED"
end try
`.trim();
const result = await runAppleScript(script);
if (result === "CANCELLED") return null;
return result.split("|||").filter(p => p.length > 0);
}
export interface SaveDialogOptions {
prompt?: string;
defaultName?: string;
defaultLocation?: string;
}
export async function saveDialog(options?: SaveDialogOptions): Promise<string | null> {
const prompt = escapeForAppleScript(options?.prompt || "Save file as");
const defaultName = escapeForAppleScript(options?.defaultName || "Untitled");
let locationClause = "";
if (options?.defaultLocation) {
const absPath = path.resolve(options.defaultLocation);
locationClause = `default location POSIX file "${escapeForAppleScript(absPath)}"`;
}
const script = `
try
set save_path to choose file name with prompt "${prompt}" default name "${defaultName}" ${locationClause}
return POSIX path of save_path
on error
return "CANCELLED"
end try
`.trim();
const result = await runAppleScript(script);
return result === "CANCELLED" ? null : result;
}
// ============ CLIPBOARD ============
export async function clipboardRead(): Promise<string> {
const script = `the clipboard as text`;
return await runAppleScript(script);
}
export async function clipboardWrite(text: string): Promise<void> {
const escaped = escapeForAppleScript(text);
const script = `set the clipboard to "${escaped}"`;
await runAppleScript(script);
}
// ============ SYSTEM ============
export interface NotifyOptions {
title: string;
message: string;
subtitle?: string;
sound?: string;
}
export async function notify(options: NotifyOptions): Promise<void> {
const title = escapeForAppleScript(options.title);
const message = escapeForAppleScript(options.message);
const subtitle = options.subtitle ? escapeForAppleScript(options.subtitle) : "";
const sound = options.sound || "";
let subtitleClause = subtitle ? `subtitle "${subtitle}"` : "";
let soundClause = sound ? `sound name "${sound}"` : "";
const script = `display notification "${message}" with title "${title}" ${subtitleClause} ${soundClause}`;
await runAppleScript(script);
}
export async function openUrl(url: string): Promise<void> {
const escaped = escapeForAppleScript(url);
const script = `open location "${escaped}"`;
await runAppleScript(script);
}
export interface SystemInfo {
computerName: string;
userName: string;
homeDirectory: string;
osVersion: string;
}
export async function getSystemInfo(): Promise<SystemInfo> {
const script = `
set computer_name to computer name of (system info)
set user_name to short user name of (system info)
set home_dir to POSIX path of (path to home folder)
set os_ver to system version of (system info)
return computer_name & "|||" & user_name & "|||" & home_dir & "|||" & os_ver
`.trim();
const result = await runAppleScript(script);
const [computerName, userName, homeDirectory, osVersion] = result.split("|||");
return {
computerName,
userName,
homeDirectory,
osVersion,
};
}
// ============ FINDER ============
export async function revealInFinder(filePath: string): Promise<void> {
const absPath = path.resolve(filePath);
const escaped = escapeForAppleScript(absPath);
const script = `
tell application "Finder"
activate
reveal POSIX file "${escaped}"
end tell
`.trim();
await runAppleScript(script);
}
export async function openWithDefault(filePath: string): Promise<void> {
const absPath = path.resolve(filePath);
const escaped = escapeForAppleScript(absPath);
const script = `
tell application "Finder"
open POSIX file "${escaped}"
end tell
`.trim();
await runAppleScript(script);
}
export async function getFinderSelection(): Promise<string[]> {
const script = `
tell application "Finder"
set selected_items to selection
if (count of selected_items) is 0 then
return ""
end if
set path_list to {}
repeat with item_ref in selected_items
set end of path_list to POSIX path of (item_ref as alias)
end repeat
set AppleScript's text item delimiters to "|||"
return path_list as text
end tell
`.trim();
const result = await runAppleScript(script);
if (result === "") return [];
return result.split("|||").filter(p => p.length > 0);
}
// ============ ADDITIONAL UTILITIES ============
export async function launchApplication(appName: string): Promise<void> {
const escaped = escapeForAppleScript(appName);
const script = `
tell application "${escaped}"
activate
end tell
`.trim();
await runAppleScript(script);
}
export async function createFolder(folderPath: string): Promise<void> {
const absPath = path.resolve(folderPath);
const escaped = escapeForAppleScript(absPath);
const script = `
do shell script "mkdir -p " & quoted form of "${escaped}"
`.trim();
await runAppleScript(script);
}
export async function fileExists(filePath: string): Promise<boolean> {
const absPath = path.resolve(filePath);
const escaped = escapeForAppleScript(absPath);
const script = `
tell application "System Events"
return exists file "${escaped}"
end tell
`.trim();
const result = await runAppleScript(script);
return result === "true";
}
// ============ SCREENSHOT ============
export interface ScreenshotOptions {
path: string;
fullScreen?: boolean;
windowId?: number;
region?: { x: number; y: number; width: number; height: number };
}
export async function takeScreenshot(options: ScreenshotOptions): Promise<string> {
const absPath = path.resolve(options.path);
let cmd = "screencapture";
if (options.region) {
const { x, y, width, height } = options.region;
cmd += ` -R${x},${y},${width},${height}`;
} else if (options.windowId) {
cmd += ` -l${options.windowId}`;
} else if (!options.fullScreen) {
cmd += " -w"; // interactive window selection
}
cmd += ` "${absPath}"`;
const { stdout } = await execAsync(cmd);
return absPath;
}
export async function screenshotToClipboard(): Promise<void> {
await execAsync("screencapture -c");
}
// ============ SCREEN INFO ============
export interface ScreenResolution {
width: number;
height: number;
scale: number;
}
export async function getScreenResolution(): Promise<ScreenResolution> {
const script = `
tell application "Finder"
set screen_bounds to bounds of window of desktop
set screen_width to item 3 of screen_bounds
set screen_height to item 4 of screen_bounds
return (screen_width as text) & "|||" & (screen_height as text)
end tell
`.trim();
const result = await runAppleScript(script);
const [width, height] = result.split("|||").map(Number);
// Get scale factor via system_profiler
let scale = 1;
try {
const { stdout } = await execAsync("system_profiler SPDisplaysDataType | grep -i resolution");
if (stdout.includes("Retina") || stdout.includes("@2x")) {
scale = 2;
}
} catch {
// Default to 1 if we can't determine
}
return { width, height, scale };
}
// ============ IMAGE TOOLS (SIPS) ============
export interface ImageInfo {
width: number;
height: number;
format: string;
space: string;
bitsPerSample: number;
}
export async function getImageInfo(imagePath: string): Promise<ImageInfo> {
const absPath = path.resolve(imagePath);
const { stdout } = await execAsync(`sips -g pixelWidth -g pixelHeight -g format -g space -g bitsPerSample "${absPath}"`);
const getValue = (key: string): string => {
const match = stdout.match(new RegExp(`${key}:\\s*(.+)`));
return match ? match[1].trim() : "";
};
return {
width: parseInt(getValue("pixelWidth"), 10) || 0,
height: parseInt(getValue("pixelHeight"), 10) || 0,
format: getValue("format"),
space: getValue("space"),
bitsPerSample: parseInt(getValue("bitsPerSample"), 10) || 0,
};
}
export interface ResizeOptions {
inputPath: string;
outputPath: string;
width?: number;
height?: number;
maxSize?: number;
}
export async function resizeImage(options: ResizeOptions): Promise<string> {
const input = path.resolve(options.inputPath);
const output = path.resolve(options.outputPath);
let cmd = `sips`;
if (options.maxSize) {
cmd += ` -Z ${options.maxSize}`;
} else if (options.width && options.height) {
cmd += ` -z ${options.height} ${options.width}`;
} else if (options.width) {
cmd += ` --resampleWidth ${options.width}`;
} else if (options.height) {
cmd += ` --resampleHeight ${options.height}`;
}
cmd += ` "${input}" --out "${output}"`;
await execAsync(cmd);
return output;
}
export interface ConvertOptions {
inputPath: string;
outputPath: string;
format: "jpeg" | "png" | "gif" | "tiff" | "bmp" | "heic";
}
export async function convertImage(options: ConvertOptions): Promise<string> {
const input = path.resolve(options.inputPath);
const output = path.resolve(options.outputPath);
await execAsync(`sips -s format ${options.format} "${input}" --out "${output}"`);
return output;
}
// ============ PDF TOOLS ============
export async function getPdfPageCount(pdfPath: string): Promise<number> {
const absPath = path.resolve(pdfPath);
const { stdout } = await execAsync(`mdls -name kMDItemNumberOfPages "${absPath}"`);
const match = stdout.match(/kMDItemNumberOfPages\s*=\s*(\d+)/);
return match ? parseInt(match[1], 10) : 0;
}
export interface MergePdfOptions {
inputPaths: string[];
outputPath: string;
}
export async function mergePdfs(options: MergePdfOptions): Promise<string> {
const inputs = options.inputPaths.map(p => `"${path.resolve(p)}"`).join(" ");
const output = path.resolve(options.outputPath);
// Use Python with Quartz (built into macOS)
const pythonScript = `
import Quartz
import sys
def merge_pdfs(input_paths, output_path):
pdf_out = Quartz.CGPDFDocumentCreateWithURL(None)
context = Quartz.CGPDFContextCreateWithURL(
Quartz.CFURLCreateWithFileSystemPath(None, output_path, Quartz.kCFURLPOSIXPathStyle, False),
None, None
)
for input_path in input_paths:
pdf = Quartz.CGPDFDocumentCreateWithURL(
Quartz.CFURLCreateWithFileSystemPath(None, input_path, Quartz.kCFURLPOSIXPathStyle, False)
)
if pdf:
for page_num in range(1, Quartz.CGPDFDocumentGetNumberOfPages(pdf) + 1):
page = Quartz.CGPDFDocumentGetPage(pdf, page_num)
media_box = Quartz.CGPDFPageGetBoxRect(page, Quartz.kCGPDFMediaBox)
Quartz.CGContextBeginPage(context, media_box)
Quartz.CGContextDrawPDFPage(context, page)
Quartz.CGContextEndPage(context)
Quartz.CGPDFContextClose(context)
merge_pdfs(sys.argv[1:-1], sys.argv[-1])
`.trim();
const inputList = options.inputPaths.map(p => path.resolve(p));
const args = [...inputList, output].map(p => `"${p}"`).join(" ");
await execAsync(`python3 -c '${pythonScript.replace(/'/g, "'\\''")}' ${args}`);
return output;
}
// ============ QUICK LOOK ============
export async function quickLook(filePath: string): Promise<void> {
const absPath = path.resolve(filePath);
await execAsync(`qlmanage -p "${absPath}" &>/dev/null &`);
}
// ============ NOTES APP ============
export interface Note {
name: string;
body: string;
folder?: string;
}
export async function createNote(options: Note): Promise<void> {
const name = escapeForAppleScript(options.name);
const body = escapeForAppleScript(options.body);
const folder = options.folder ? escapeForAppleScript(options.folder) : "Notes";
const script = `
tell application "Notes"
tell folder "${folder}"
make new note with properties {name:"${name}", body:"${body}"}
end tell
end tell
`.trim();
await runAppleScript(script);
}
export async function listNoteFolders(): Promise<string[]> {
const script = `
tell application "Notes"
set folder_names to {}
repeat with f in folders
set end of folder_names to name of f
end repeat
set AppleScript's text item delimiters to "|||"
return folder_names as text
end tell
`.trim();
const result = await runAppleScript(script);
if (result === "") return [];
return result.split("|||").filter(n => n.length > 0);
}
export async function listNotes(folderName?: string): Promise<Array<{ name: string; id: string }>> {
const folder = folderName ? escapeForAppleScript(folderName) : "Notes";
const script = `
tell application "Notes"
set note_list to {}
tell folder "${folder}"
repeat with n in notes
set end of note_list to (name of n) & ":::" & (id of n)
end repeat
end tell
set AppleScript's text item delimiters to "|||"
return note_list as text
end tell
`.trim();
const result = await runAppleScript(script);
if (result === "") return [];
return result.split("|||").filter(n => n.length > 0).map(item => {
const [name, id] = item.split(":::");
return { name, id };
});
}
export async function getNoteContent(noteId: string): Promise<string> {
const escaped = escapeForAppleScript(noteId);
const script = `
tell application "Notes"
set target_note to first note whose id is "${escaped}"
return plaintext of target_note
end tell
`.trim();
return await runAppleScript(script);
}