1
头图

公众号名片
作者名片

foreword

Are you still troubled by too many projects and can't find the directory? Does the tedious operation of opening projects in 终端 , SourceTree , Finder pain you?

Today, you (the Mac user) will say goodbye to these troubles.

The book is connected to the previous "Overlap Generator" . Today we continue to use Alfred Workflows to develop a tool that can search the local Git warehouse, and quickly use the specified application to open the warehouse directory.

Stream saver

 # 项目开源地址,现已支持 Alfred、uTools(插件市场审核中),Raycast 扩展将于 Q2 内完成开发
# Alfred 用户请进入 cheetah-for-alfred 项目的 release 下载 .alfredworkflow 直接导入使用。
https://github.com/cheetah-extension

Show Time

看看使用效果

In order to save traffic for everyone, the recording quality has been lowered and the operation speed has been accelerated.

The following operations are done in the demo:

  1. Opens the specified project using the default editor.
  2. Open the project with the specified Git GUI application.
  3. Open a terminal in the project directory.
  4. Open the project directory in Finder .
  5. Specifies an editor for the project.
  6. Re-execute the step 1 , and the editor to open the project is the editor set in the step 5 .

A single operation may not be complicated, but when you need to switch projects frequently at work, or when you want to operate project files, a little optimization accumulates to a significant improvement in efficiency.
tip: The above operations can be customized shortcut keys.

technology used

  • txiki
  • AnyScript (funny)
  • AppleScript
  • rollup
  • Alfred Workflows

What is txiki.js?

txiki.js is a small but powerful JavaScript runtime.

Why txiki.js instead of Node.js ?

Assuming that Node.js is selected, the Node.js environment needs to be configured on the user's device, which is standard for front-end friends, but it is hard to say for friends of other types of work. This undoubtedly increases the user's use cost.

txiki.js can be regarded as a simplified version of Node.js , the compiled executable file is less than 2MB , packaged in .alfredworkflow It can be used out of the box, and the total size is further compressed to 800+KB , which reduces the user's use cost and makes it more convenient to promote and spread.

Next, the old tailor will take you to do needlework and teach you how to sew these things together.

Disadvantages of txiki.js

Packaged in .alfredworkflow within the executable file in the first run, Mac OS gives a security warning, you need 系统偏好设置 -> 安全与隐私 is allowed to run, if you are concerned about security issues, you can download txiki.js source code to build the executable file and replace it with the Alfred workflows folder in the runtime folder.

environment variable

The configuration is in Alfred Workflows and the code can be read during execution.

idePath

The application name used to open the project, the application in the /Applications directory can directly fill in the name, ends with .app (It can be omitted after testing .app but you need to ensure that the spelling of the App name is correct). When the application path is empty, the project folder will be opened in Finder .

If the application is not in the /Applications directory, it needs to fill in its absolute path.

workspace

The directory where the project is stored, the closer it is to the level of the project, the better. The more levels, the slower the search speed. The default directory is Documents in the user folder, such as /Users/ronglecat/Documents .

Multi-directory configuration is now supported, separated by commas.

Find local Git projects

To complete this tool, you must first find out which projects are managed by Git (sorry, friends who use SVN ).

How to tell if a folder is a project?
It's very simple, just judge whether the directory contains the .git folder. The core secrets are as follows:

 // 在指定目录中查找项目
export async function findProject(dirPath: string): Promise<Project[]> {
  const result: Project[] = [];
  const currentChildren: ChildInfo[] = [];
  let dirIter;

  try {
    // tjs 为 txiki.js 的全局 api
    dirIter = await tjs.fs.readdir(dirPath);
  } catch (error) {
    return result;
  }

  // 获取当前文件夹下的所有文件、文件夹
  for await (const item of dirIter) {
    const { name, type }: { name: string; type: number } = item;
    currentChildren.push({
      name,
      isDir: type === 2,
      path: path.join(dirPath, name),
    });
  }

  // 判断是否为 Git 项目
  const isGitProject = currentChildren.some(
    ({ name }: { name: string }) => name === '.git'
  );

  // 判断目录下是否包含 submodule
  const hasSubmodules = currentChildren.some(
    ({ name }: { name: string }) => 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: boolean }) => isDir
    );
  }

  // 如果是包含 submodule 的项目,将 submodule 的目录也找到
  if (isGitProject && hasSubmodules) {
    nextLevelDir = await findSubmodules(path.join(dirPath, '.gitmodules'));
  }

  // 递归查找项目
  for (let i = 0; i < nextLevelDir.length; i += 1) {
    const dir = nextLevelDir[i];
    result.push(...(await findProject(path.join(dirPath, dir.name))));
  }

  return result;
}

// 查找项目内的 submodule
export async function findSubmodules(filePath: string): Promise<ChildInfo[]> {
  // 读取 .gitmodules 文件内容
  const fileContent = await readFile(filePath);
  // 匹配 Submodule 名称、路径,进入下一轮递归,因为 Submodule 项目目录下也会有 .git 文件夹,所以可以被判断为 Git 项目
  const matchModules = fileContent.match(/(?<=path = )([\S]*)(?=\n)/g) ?? [];
  return matchModules.map((module) => {
    return {
      name: module,
      isDir: true,
      path: path.join(path.dirname(filePath), module),
    };
  });
}

These two functions can find all Git and Git Submodule projects in the specified file path, and get the project name, absolute path, and project type.

Determine the project type

The above mentioned judging the project type, in fact, this is still an incomplete function, because of the limitations of the author's knowledge, it is not very clear how to judge projects in many other languages, and only some of the determinable types are currently done. code show as below:

 // 判断项目下的文件列表是否包含需要搜索的文件列表
function findFileFromProject(
  allFile: ChildInfo[],
  fileNames: string[]
): boolean {
  const reg = new RegExp(`^(${fileNames.join('|')})$`, 'i');
  const findFileList = allFile.filter(({ name }: { name: string }) =>
    reg.test(name)
  );

  return findFileList.length === fileNames.length;
}

// 判断 npm 依赖列表中是否包含指定的 npm 包名称
function findDependFromPackage(
  allDependList: string[],
  dependList: string[]
): boolean {
  const reg = new RegExp(`^(${dependList.join('|')})$`, 'i');
  const findDependList = allDependList.filter((item: string) => reg.test(item));

  return findDependList.length >= dependList.length;
}

// 获取 package.json 内的 npm 依赖列表
async function getDependList(allFile: ChildInfo[]): Promise<string[]> {
  const packageJsonFilePath =
    allFile.find(({ name }) => name === 'package.json')?.path ?? '';
  if (!packageJsonFilePath) {
    return [];
  }
  const { dependencies = [], devDependencies = [] } = JSON.parse(
    await readFile(packageJsonFilePath)
  );
  const dependList = { ...dependencies, ...devDependencies };
  return Object.keys(dependList);
}

// 解析项目类型
async function projectTypeParse(children: ChildInfo[]): Promise<string> {
  if (findFileFromProject(children, ['cargo.toml'])) {
    return 'rust';
  }
  if (findFileFromProject(children, ['pubspec.yaml'])) {
    return 'dart';
  }
  if (findFileFromProject(children, ['.*.xcodeproj'])) {
    return 'applescript';
  }
  if (findFileFromProject(children, ['app', 'gradle'])) {
    return 'android';
  }
  // js 项目还可以细分
  if (findFileFromProject(children, ['package.json'])) {
    if (findFileFromProject(children, ['nuxt.config.js'])) {
      return 'nuxt';
    }
    if (findFileFromProject(children, ['vue.config.js'])) {
      return 'vue';
    }
    if (findFileFromProject(children, ['.vscodeignore'])) {
      return 'vscode';
    }

    const isTS = findFileFromProject(children, ['tsconfig.json']);
    const dependList = await getDependList(children);

    if (findDependFromPackage(dependList, ['react'])) {
      return isTS ? 'react_ts' : 'react';
    }

    if (findDependFromPackage(dependList, ['hexo'])) {
      return 'hexo';
    }

    return isTS ? 'typescript' : 'javascript';
  }
  return 'unknown';
}

What can I do with the project type?
There are currently two applications:

  1. The search results display the icon corresponding to the item type
    项目类型不同展示的图标不同
  2. Different settings can be made for project types. Currently, different editors can be set for different types of projects.

cache file

After the above steps, we have got all the Git items in the specified directory, but each search will still take a long time.

The factors that affect the time are 2 :

  1. device performance.
  2. The level of the project storage folder and the number of projects.

In terms of device performance, it can only be solved by users themselves. We can make some optimizations for the second point.

In order to achieve the out-of-the-box effect, the current default project storage directory is $HOME/Documents , the directory level is higher, the directory is more complex, and a search time may be relatively long.

It is recommended to configure the directory closest to the project, modify the field of the receiving directory, you can separate multiple paths with commas, and search recursively after looping, which can slightly optimize the search time.

 // 在多个工作目录下搜索项目,工作目录以英文逗号分隔
// 例:/Users/caohaoxia/Documents/work,/Users/caohaoxia/Documents/document
async function batchFindProject() {
  const workspaces = workspace.split(/,|,/);
  const projectList: Project[] = [];
  for (let i = 0; i < workspaces.length; i += 1) {
    const dirPath = workspaces[i];
    const children = await findProject(dirPath);
    projectList.push(...children);
  }
  return projectList;
}

Although the above time has been optimized for some time, there is still a significant lag when searching. What is the original intention of our work? quick! Open projects faster!

Here we are introducing the "cache" file!

First let's take a look at its structure:

 {
  "editor": {
    "typescript": "", // 可以配置一个专属于 typescript 项目的编辑器,所有 typescript 默认编辑器将会改变
    ...
  },
  "cache": [
    {
      "name": "fmcat-open-project",
      "path": "/Users/caohaoxia/Documents/work/self/fmcat-open-project",
      "type": "typescript",
      "hits": 52,
      "idePath": ""
    },
    ...
  ]
}

cache

You can see that the configuration file contains a cache field, which is used to store the searched item list. Each item has the following fields:

name : Project name.
path : The absolute path to the project directory.
type : The item type.
hits : The number of hits, used for sorting.
idePath : The bound editor.

When performing an item search, the item in the cache list will be matched first. If there is no result, a recursive folder search will be performed, and the searched results will be merged into the cache list, so there is no need to worry about the disappearance of clicks and editor configuration.

 // 更新缓存时合并项目点击数、编辑器配置
async function combinedCache(newCache: Project[]): Promise<Project[]> {
  // 从缓存文件内读取 cache
  const { cache } = await readCache();
  // 筛选有点击记录和编辑器配置的项目
  const needMergeList = {} as { [key: string]: Project };
  cache
    .filter((item: Project) => item.hits > 0 || item.idePath)
    .forEach((item: Project) => {
      needMergeList[item.path] = item;
    });
  // 合并点击数
  newCache.forEach((item: Project) => {
    const cacheItem = needMergeList[item.path] ?? {};
    const { hits = 0, idePath = '' } = cacheItem;
    item.hits = item.hits > hits ? item.hits : hits;
    item.idePath = idePath;
  });
  return newCache;
}

// 写入缓存
export async function writeCache(newCache: Project[]): Promise<void> {
  try {
    const { editor } = await readCache();
    const cacheFile = await tjs.fs.open(cachePath, 'rw', 0o666);
    const newEditorList = combinedEditorList(editor, newCache);
    const newConfig = { editor: newEditorList, cache: newCache };
    const historyString = JSON.stringify(newConfig, null, 2);
    await cacheFile.write(historyString);
    cacheFile.close();
  } catch (error: any) {
    console.log(error.message);
  }
}

// 从搜索结果中过滤
export async function filterWithSearchResult(
  keyword: string
): Promise<ResultItem[]> {
  const projectList: Project[] = await batchFindProject();
  writeCache(await combinedCache(projectList));
  return output(filterProject(projectList, keyword));
}

editor

In the write cache function writeCache , a function that merges the editor configuration will be called to list all the types of the project, and merge it with the editor field in the cache file.

 // 合并编辑器
function combinedEditorList(
  editor: { [key: string]: string },
  cache: Project[]
) {
  const newEditor = { ...editor };
  const currentEditor = Object.keys(newEditor);
  cache.forEach(({ type }: Project) => {
    if (!currentEditor.includes(type)) {
      newEditor[type] = '';
    }
  });
  return newEditor;
}

refresh cache

When local items are moved, deleted, or added, the cache file becomes unreliable. What are the ways to refresh the cache?

  1. Entering an item keyword that cannot exist locally, and the cache matching result is empty will trigger a recursive search of the folder.
  2. Add an item at the bottom of the result list to continue searching regardless of the cache, directly triggering recursive folder search.
  3. ⚠️Forbidden Technique⚠️ Delete the cache file, the next search will rebuild the cache file, but the project hits and editor configuration will be lost.

sort

Before returning to the project candidate list, you need to do a sorting first. There are three cases here, and they are ranked as follows according to the priority:

  1. The search keyword is congruent with the item name.
  2. The item name header matches the keyword.
  3. Include keywords only.

The three cases are arranged in descending order according to the item hits , and finally merged into an array and output to Alfred Workflows .

 // 过滤项目
export function filterProject(
  projectList: Project[],
  keyword: string
): Project[] {
  const reg = new RegExp(keyword, 'i');
  const result = projectList.filter(({ name }: { name: string }) => {
    return reg.test(name);
  });

  // 排序规则:项目名称以关键词开头的权重最高,剩余的以点击量降序排序
  const congruentMatch: Project[] = []; // 全等匹配
  const startMatch: Project[] = []; // 头部匹配
  const otherMatch: Project[] = []; // 包含匹配
  result.forEach((item) => {
    if (item.name.toLocaleLowerCase() === keyword.toLocaleLowerCase()) {
      congruentMatch.push(item);
    } else if (item.name.startsWith(keyword)) {
      startMatch.push(item);
    } else {
      otherMatch.push(item);
    }
  });

  return [
    ...congruentMatch.sort((a: Project, b: Project) => b.hits - a.hits),
    ...startMatch.sort((a: Project, b: Project) => b.hits - a.hits),
    ...otherMatch.sort((a: Project, b: Project) => b.hits - a.hits),
  ];
}

// 输出待选列表给 Alfred
export function output(projectList: Project[]): ResultItem[] {
  const result = projectList.map(
    ({ name, path, type }: { name: string; path: string; type: string }) => {
      return {
        title: name,
        subtitle: path,
        arg: path,
        valid: true,
        icon: {
          path: `assets/${type}.png`,
        },
      };
    }
  );
  return result;
}

// 从缓存中过滤
export async function filterWithCache(keyword: string): Promise<ResultItem[]> {
  const { cache } = await readCache();
  return output(filterProject(cache, keyword));
}

// 从搜索结果中过滤
export async function filterWithSearchResult(
  keyword: string
): Promise<ResultItem[]> {
  const projectList: Project[] = await batchFindProject();
  writeCache(await combinedCache(projectList));
  return output(filterProject(projectList, keyword));
}

quick open

Mac OS provides a quick command to open the specified file and directory with software--- open .

 open .
# 使用 Finder 打开当前目录

open 目录路径
# 使用 Finder 打开指定目录

open 文件路径
# 使用文件类型对应的默认程序打开文件

open -a 应用名称 文件/目录路径
# 使用指定应用打开指定文件、目录
# 例:open -a "Visual Studio Code" /Users/caohaoxia/Documents/work/self/fmcat-open-project
# tip: 如果应用名称包含空格需要使用引号包裹

open command is the core of our completion tool, and it has been tested to support the applications that can be called with open -a syntax:

Editor/IDE

  • VSCode
  • Sublime
  • WebStorm
  • Atom
  • Android Studio
  • Xcode
  • Typora

Git GUI

  • SourceTree
  • Fork
  • GitHub Desktop

terminal

  • Terminal (built-in terminal)
  • iTerm2

Calling example:

 open -a "Visual Studio Code" /Users/caohaoxia/Documents/work/self/fmcat-open-project
# 使用 VSCode 打开项目

open -a SourceTree /Users/caohaoxia/Documents/work/self/fmcat-open-project
# 使用 SourceTree 打开项目

open -a iTerm2 /Users/caohaoxia/Documents/work/self/fmcat-open-project
# 打开 iTerm2 默认位置为项目目录

open /Users/caohaoxia/Documents/work/self/fmcat-open-project
# 在 Finder 中打开项目目录

Knowing the method to quickly open the project, combined with the project address we got above, you can do where you want to play.

application priority

There are now three places within the tool to define the application used to open the project:

  1. The idePath default application configuration in the environment variable.
  2. The application configuration for the project type in the cache file.
  3. Application configuration for each item in the cache file.

In addition, in order to realize the binding of shortcut keys and applications, an environment variable force is added. The usage method is as follows:

设置快捷键

设置 force 为 1

The final application priorities are:

force is the default app configuration for 1 > Project Type App Configuration > Project App Configuration > Default App Configuration > Finder

Without any apps set up, the bottom line is Finder.

family portrait

The functions completed above are completed by concatenating Alfred Workflows to complete this tool. For reasons of space, as well as the implementation of functions such as opening the application, opening the configuration file, and backing up the configuration file for the project are not explained in detail, everyone is interested You can download and experience it.

The configuration of Alfred Workflows is well understood, that is, the function configuration and the flow chart of the whole project. Double-clicking a flow block opens the configuration details.

全家福

summary

This is a tool that the author starts from his own pain points, analyzes needs, and gradually implements it. It is named "Cheetah". I hope it can open the project as quickly as the cheetah runs.
At present, the project is still in the internal testing stage, and the small partners in the team have already used it and received rave reviews.

I also hope that friends who are reading can try it. If you have any suggestions or questions, please comment or mention it under the open source project Issues .

Alfred Workflows is indeed an excellent personal workflow tool, and there are many similar tool flow tools, such as uTools , Raycast and so on.
The migration of uTools has been completed, and supports all functions of the Alfred version, Windows users can also use it, after the plugin is approved, you can search and install, please expect.

In short, if you have inefficient repetitive work, boldly try to develop a tool of your own~

For more exciting things, please pay attention to our public account "Hundred Bottles Technology", there are irregular benefits!


百瓶技术
127 声望18 粉丝

「百瓶」App 技术团队官方账号。