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.
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 WorkflowsWhat Is txiki.js ?
txiki.jsis 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-projectApplication 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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
BaiPing Technology
Official account of the BaiPing app technology team. Dedicated to enhancing human productivity through technology. | DRINK FOR FUN!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
