Macotron

macotron.fs

Read, write, list, and rename files, open them or show them in Finder, move them to the Trash, and watch a file or folder for changes.

// Give new screenshots on the Desktop a short name.
for (const name of macotron.fs.list("~/Desktop")) {
  const m = name.match(/^Screenshot (\d{4}-\d\d-\d\d) at (.+)\.png$/);
  if (!m) continue;
  const dest = "~/Desktop/shot-" + m[1] + ".png";
  if (!macotron.fs.exists(dest)) macotron.fs.rename("~/Desktop/" + name, dest);
}

Paths

Every call expands a leading ~ to your home folder. Macotron is not sandboxed and applies no path rules of its own: a plugin can read and write anything your user account can, outside the workdir too. macOS privacy rules still apply. Folders such as Desktop, Documents, and Downloads can make macOS ask once whether Macotron may open them, and a refused folder makes the call throw.

Use absolute paths or paths that start with ~. Relative paths do not resolve against the plugin's folder.

All calls are synchronous. When a call fails, it throws an error whose message starts with the call name, for example fs.read failed: …. open, reveal, and trash do not throw; they report failure in their return value.

Reading

read(path: string): string

The whole file as UTF-8 text. Throws when the file is missing or is not valid UTF-8.

readBytes(path: string): string

The whole file as a base64 string. Use this for images and other binary files.

exists(path: string): boolean

Whether a file or folder exists at path. Never throws.

list(path: string): string[]

The names of the entries in a folder, not full paths. Hidden files such as .DS_Store are included. The order is not sorted. Throws when path is not a folder.

Writing

write(path: string, content: string): void

Writes content as UTF-8, replacing the file if it exists. Missing parent folders are created. The write is atomic: Macotron writes a temporary file and moves it into place, so a reader never sees half a file.

rename(from: string, to: string): void

Moves or renames a file or folder. Throws if to already exists, so check with exists() first.

Opening and the Trash

open(path: string, options?: { app?: string }): boolean

Opens the file or folder in its default app, as a double-click in Finder does. app is a bundle ID to open it with instead, such as "com.apple.TextEdit". Returns false when the path does not exist or no app has that bundle ID.

reveal(path: string | string[]): boolean

Opens a Finder window with the items selected. Paths that do not exist are skipped. Returns false when none exist.

trash(path: string | string[]): { trashed: string[]; failed: Array<{ path: string; error: string }> }

Moves each item to the Trash, where the user can still put it back. Paths come back as you passed them, so you can match them against your own list.

const { trashed, failed } = macotron.fs.trash(["~/Desktop/old.png", "~/Desktop/older.png"]);
for (const f of failed) console.error(f.path, f.error);

Watching

watch(path: string, callback: (event: { path: string; type: string }) => void): () => void

Calls callback when something changes, and returns a function that stops the watch. Throws when path does not exist. Changes are grouped and delivered about 0.3 seconds after they happen.

event.path is the full path of the item that changed. event.type is "created", "modified", or "deleted". A rename reports "created", for both the old and the new name.

Watching a folder reports changes to everything inside it, subfolders included. Watching a file watches its parent folder, so the callback also gets events for the file's neighbors. Compare event.path when you only care about one file.

const stop = macotron.fs.watch("~/Downloads", ({ path, type }) => {
  if (type === "created" && path.endsWith(".heic")) console.log("New photo:", path);
});
// later: stop();

Watches stop by themselves when Macotron reloads plugins.

See also