Operations 18 min read

Boost Mac Project Opening Speed with a Custom Alfred Workflow

This guide shows how to create an Alfred Workflow that quickly searches local Git repositories on macOS, determines their types, caches results, and opens projects with the appropriate editor, terminal, or file explorer, dramatically reducing the time spent navigating directories.

BaiPing Technology
BaiPing Technology
BaiPing Technology
Boost Mac Project Opening Speed with a Custom Alfred Workflow

Introduction

Are you tired of the tedious process of opening projects in the terminal, SourceTree, or Finder? This tutorial explains how to use Alfred Workflows to search local Git repositories and open them instantly with a specified application.

Demo

The demonstration performs the following actions:

Open the project with the default editor.

Open the project with a chosen Git GUI.

Open a terminal in the project directory.

Open the project folder in Finder.

Assign a specific editor to the project.

Repeat step 1 to open the project with the newly assigned editor.

These small optimizations accumulate into a significant efficiency boost when frequently switching projects.

Technologies Used

txiki
AnyScript

(humorous)

AppleScript
rollup
Alfred Workflows

What Is txiki.js ?

txiki.js

is a tiny yet powerful JavaScript runtime.

Why Choose txiki.js Over Node.js ?

Using Node.js requires the user to have a pre‑installed environment, which adds overhead for non‑frontend developers. txiki.js is a lightweight alternative; the compiled executable is under 2 MB and can be packaged inside an .alfredworkflow file, resulting in a final size of about 800 KB, reducing distribution cost.

Environment Variables

idePath : Name of the application used to open a project. If left empty, the project folder opens in Finder.

workspace : Directory where projects are stored. The default is $HOME/Documents, but multiple directories can be configured using commas.

Finding Local Git Projects

The core algorithm recursively scans directories, identifies Git repositories (by the presence of a .git folder), handles submodules, and determines project types based on characteristic files.

export async function findProject(dirPath: string): Promise<Project[]> {
  const result: Project[] = [];
  const currentChildren: ChildInfo[] = [];
  let dirIter;
  try {
    // tjs is the global API of txiki.js
    dirIter = await tjs.fs.readdir(dirPath);
  } catch (error) {
    return result;
  }
  for await (const item of dirIter) {
    const { name, type } = item;
    currentChildren.push({
      name,
      isDir: type === 2,
      path: path.join(dirPath, name),
    });
  }
  const isGitProject = currentChildren.some(({ name }) => name === '.git');
  const hasSubmodules = currentChildren.some(({ name }) => name === '.gitmodules');
  if (isGitProject) {
    result.push({
      name: path.basename(dirPath),
      path: dirPath,
      type: await projectTypeParse(currentChildren),
      hits: 0,
      idePath: '',
    });
  }
  let nextLevelDir: ChildInfo[] = [];
  if (!isGitProject) {
    nextLevelDir = currentChildren.filter(({ isDir }) => isDir);
  }
  if (isGitProject && hasSubmodules) {
    nextLevelDir = await findSubmodules(path.join(dirPath, '.gitmodules'));
  }
  for (let i = 0; i < nextLevelDir.length; i++) {
    const dir = nextLevelDir[i];
    result.push(...(await findProject(path.join(dirPath, dir.name))));
  }
  return result;
}

export async function findSubmodules(filePath: string): Promise<ChildInfo[]> {
  const fileContent = await readFile(filePath);
  const matchModules = fileContent.match(/(?<=path = )[\S]*/g) ?? [];
  return matchModules.map(module => ({
    name: module,
    isDir: true,
    path: path.join(path.dirname(filePath), module),
  }));
}

These functions locate both regular Git projects and Git submodules, returning their name, absolute path, and inferred type.

Determining Project Type

The type detection checks for specific files (e.g., cargo.toml for Rust, pubspec.yaml for Dart, .xcodeproj for AppleScript, app or gradle for Android, various package.json patterns for JavaScript frameworks, etc.). If none match, the type is marked as unknown.

function findFileFromProject(allFile: ChildInfo[], fileNames: string[]): boolean {
  const reg = new RegExp(`^(${fileNames.join('|')})`, 'i');
  const findFileList = allFile.filter(({ name }) => reg.test(name));
  return findFileList.length === fileNames.length;
}

function findDependFromPackage(allDependList: string[], dependList: string[]): boolean {
  const reg = new RegExp(`^(${dependList.join('|')})`, 'i');
  const findDependList = allDependList.filter(item => reg.test(item));
  return findDependList.length >= dependList.length;
}

Cache File

The cache stores discovered projects to avoid repeated scans. Each entry contains name , path , type , hits (click count for ranking), and idePath (custom editor).

{
  "editor": {
    "typescript": "",
    ...
  },
  "cache": [
    {
      "name": "fmcat-open-project",
      "path": "/Users/caohaoxia/Documents/work/self/fmcat-open-project",
      "type": "typescript",
      "hits": 52,
      "idePath": ""
    },
    ...
  ]
}

When searching, the tool first checks the cache; if no match is found, it performs a recursive folder scan and merges new results into the cache, preserving click counts and editor settings.

async function combinedCache(newCache: Project[]): Promise<Project[]> {
  const { cache } = await readCache();
  const needMergeList: { [key: string]: Project } = {};
  cache.filter(item => item.hits > 0 || item.idePath)
       .forEach(item => { needMergeList[item.path] = item; });
  newCache.forEach(item => {
    const cacheItem = needMergeList[item.path] ?? {};
    const { hits = 0, idePath = '' } = cacheItem;
    item.hits = item.hits > hits ? item.hits : hits;
    item.idePath = idePath;
  });
  return newCache;
}

Optimization

To speed up searches, multiple workspace directories can be specified (comma‑separated) and processed in parallel. The cache reduces subsequent search times, though device performance and directory depth still affect speed.

async function batchFindProject() {
  const workspaces = workspace.split(/,|,/);
  const projectList: Project[] = [];
  for (let i = 0; i < workspaces.length; i++) {
    const dirPath = workspaces[i];
    const children = await findProject(dirPath);
    projectList.push(...children);
  }
  return projectList;
}

Quick Opening with macOS open Command

The open command can launch files, directories, or applications. Supported editors include VSCode, Sublime, WebStorm, Atom, Android Studio, Xcode, Typora, and Git GUIs such as SourceTree, Fork, and GitHub Desktop. Terminals like Terminal and iTerm2 are also supported.

open -a "Visual Studio Code" /Users/caohaoxia/Documents/work/self/fmcat-open-project
open -a SourceTree /Users/caohaoxia/Documents/work/self/fmcat-open-project
open -a iTerm2 /Users/caohaoxia/Documents/work/self/fmcat-open-project
open /Users/caohaoxia/Documents/work/self/fmcat-open-project

Application Priority

The final application used follows this order: force=1 default app > project‑type app > project‑specific app > global default app > Finder.

Conclusion

The resulting tool, named "Cheetah", provides rapid project opening on macOS. It is open‑source, currently in internal testing, and welcomes feedback via the repository's Issues page.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

AutomationmacOSAlfred Workflowtxiki.js
BaiPing Technology
Written by

BaiPing Technology

Official account of the BaiPing app technology team. Dedicated to enhancing human productivity through technology. | DRINK FOR FUN!

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.