likes
comments
collection
share

猎豹Cheetah插件开发 - Alfred 篇

作者站长头像
站长
· 阅读数 2

大家好,本系列第四篇为 Alfred 篇,介绍如何在 Cheetah for Alfred 1.0 的基础上引入前面完成开发的核心模块。

既然有了核心模块,也需要回过头来好好升级一下 Cheerah for Alfred 1.5 版本了~

本文仅阐述与 1.0 版本有差别的地方,少水一点 😄。

项目配置

引入核心模块

1.51.0 最大的区别就是引入了之前抽离的核心模块,替换掉 1.0 代码中与核心模块重合的逻辑。

npm install cheetah-core
# or
yarn add cheetah-core

将核心模块添加到依赖以后,就可以直接调用了。

types

不仅核心模块内有使用 txiki.jsAPI,Alfred 项目内也有很多地方用到了 txiki.jsAPI,这边引入了 txiki.jstypes 定义,方便后续开发。

cheetah-for-alfred
├─ ...
├─ declares
│  ├─ ffi.d.ts
│  └─ txikijs.d.ts
├─ ...

tsconfig.json 中的 include 字段添加 declares 目录即可:

// tsconfig.json
{
  ...
  "include": [
    "src/**/*",
    "declares/**/*" // 看这里
  ],
  ...
}

runtime

此次除了引入核心模块外,还对项目中的 txiki.js 可执行文件进行了升级,从原来的 v19.0.0-alpha 升级为 v22.4.1,API 使用方面有一些区别,但是大同小异。

功能实现

Alfred 区别于 uTools 插件开发的方式,很多操作需要在 Workflows 中使用功能块处理,在流程关键位置使用 txiki.js 运行 js 文件(任意语言,只要有运行环境即可)完成处理后,结果使用指定格式输出到控制台,下一个流程即可获取使用。

1.51.0 一致,分别有以下三个 js,在构建时需要有三个入口,rollup 配置如下:

import typescript from 'rollup-plugin-typescript';
import resolve from 'rollup-plugin-node-resolve';
import commonjs from 'rollup-plugin-commonjs';
import { uglify } from 'rollup-plugin-uglify';

const production = process.env.NODE_ENV === 'production';

const devInput = {
  test: 'src/test.ts',
};

const productionInput = {
  index: 'src/index.ts',
  hits: 'src/hits.ts',
  choose: 'src/choose.ts'
};

export default {
  // 这里input要改成数组形式或者对象形式,对象形式可以修改打包的文件名,键对应的就是打包的文件名
  input: production ? productionInput : devInput,
  // 输出配置要改成拆分包的配置,以为多入口打包默认会执行打包拆分的特性。所以输出格式要改成amd
  output: {
    dir: 'dist',
    format: 'esm'
  },
  plugins: [
    resolve(),
    commonjs(),
    typescript(),
    production && uglify(),
  ]
}

因为 Alfred Workflows 仅支持文件导出形式的发布,没有插件市场,这边可以使用混淆压缩,进一步减少导出的插件文件体积。

搜索项目并输出列表

搜索项目并输出列表的入口在 Script Filter 功能块中使用,输入框中接收到的值会传递到 $1 中,在 txiki.js 执行入口文件后可以作为运行时参数获取,用于筛选项目。

猎豹Cheetah插件开发 - Alfred 篇

import { filterWithCache, filterWithSearchResult, Project } from 'cheetah-core';

import { output, initCore, errorHandle } from './utils';
import { ResultItem } from './types';

// 判断是否需要刷新缓存
const needRefresh: boolean = Array.from(tjs.args).includes('--r');

// 项目搜索关键词
const keyword: string = (Array.from(tjs.args).pop() as string) ?? '';

(async () => {
  try {
    initCore();
    let projects: Project[] = await filterWithCache(keyword);
    let fromCache = true;
    // 如果缓存结果为空或者需要刷新缓存,则重新搜索
    if (!projects.length || needRefresh) {
      projects = await filterWithSearchResult(keyword);
      fromCache = false;
    }

    const result: ResultItem[] = output(projects);
    // 如果是从缓存中获取的内容,最后加上刷新的入口
    if (fromCache) {
      result.push({
        title: '忽略缓存重新搜索',
        subtitle: '以上结果从缓存中获得,选择本条将重新搜索项目并更新缓存',
        arg: keyword,
        valid: true,
        icon: {
          path: 'assets/refresh.png',
        },
      });
    }
    if (!result.length) {
      result.push({
        title: `没有找到名称包含 ${keyword} 的项目`,
        subtitle: '请尝试更换关键词',
        arg: keyword,
        valid: false,
        icon: {
          path: 'assets/empty.png',
        },
      });
    }
    console.log(JSON.stringify({ items: result }));
  } catch (error) {
    errorHandle(error);
  }
})()

基本逻辑没有改变,将原来项目内的项目搜索函数替换为核心模块提供的函数。 另添加了 initCore 函数用于初始化核心模块,添加 errorHandle 处理错误。

export function initCore() {
  const workspaces = (getEnv('workspace') || '').split(/[,,]/);

  const PlatformTypeMap = {
    darwin: PlatformType.MAC,
    windows: PlatformType.WINDOWS,
    linux: PlatformType.LINUX,
  };

  let platformType: PlatformType =
    PlatformTypeMap?.[tjs.platform] ?? PlatformType.MAC;

  init({
    cachePath,
    workspaces: workspaces.join(','),
    platformType, // Alfred 下没什么用
  });
}

initCore 向核心模块注册了缓存文件位置,工作区配置,这样核心模块就能从 global 对象上获取并使用这两个参数了。

errorHandle 在下面详解。

打开项目并更新点击量

项目每一次被选择后其点击量都会加 1,点击量越高,项目在搜索列表中的排序越前,方便用户下一次选择使用。

import { readCache, writeCache, getEnv, Project } from 'cheetah-core';
import { errorHandle, initCore } from './utils';

// 项目搜索关键词
const projectPath: string = (Array.from(tjs.args).pop() as string) ?? '';
const force = getEnv('force', '0') === '1';

(async () => {
  try {
    initCore();
    // 获取路径对应的项目详情
    const { cache: cacheList = [], editor } = await readCache();
    const targetProject = cacheList.find(
      (item: Project) => item.path === projectPath
    );
    if (!targetProject) {
      return;
    }

    // 更新点击量
    targetProject.hits += 1;
    await writeCache(cacheList);

    const { idePath, type } = targetProject;

    // 自定义编辑器覆盖默认环境变量中配置的编辑器
    const priorityIdePath = force
      ? tjs.getenv('idePath')
      : idePath || editor[type] || tjs.getenv('idePath');

    // 输出两个参数,以英文逗号分割
    console.log([projectPath, priorityIdePath].join(','));
  } catch (error: any) {
    errorHandle(error, true);
  }
})();

上面的代码主要是引入了核心模块中的缓存读写函数,读取到项目信息以后,给点击量加 1,然后重新写入缓存。

完成点击量增加操作后,获取当前环境变量中的默认应用 idePath,如果环境变量中 force1 则取 环境变量中 idePath 的值,否则将按照下面的顺序决定打开项目的应用: 缓存文件内项目中配置的idePath > 缓存文件内配置的项目类型应用 > 环境变量中idePath 的值。

获取到项目绝对路径以及打开应用的名称后,即可使用 Run Script 功能块执行打开命令了:

open -a "$idePath" {query}

猎豹Cheetah插件开发 - Alfred 篇

为项目指定应用

与 uTools 版插件一致,也添加了一个为项目快速配置编辑器的入口。 同样是使用了核心模块提供的缓存读写函数,在搜索后选择项目后即可打开文件选择窗口,选择想要为项目配置的应用。

import { readCache, writeCache, getEnv, Project } from 'cheetah-core';
import path from 'path-browserify';

const projectPath = getEnv('projectPath');
import { errorHandle, initCore } from './utils';

(async () => {
  try {
    initCore();
    // 项目搜索关键词
    let idePath: string = (Array.from(tjs.args).pop() as string) ?? '';
    idePath = idePath.split(',')?.[0] ?? '';
    if (!idePath) return;
    idePath = path.basename(idePath);
    // 获取路径对应的项目详情
    const { cache: cacheList = [] } = await readCache();
    const targetProject = cacheList.find(
      (item: Project) => item.path === projectPath
    );
    if (!targetProject) {
      return;
    }

    // 更新编辑器
    targetProject.idePath = idePath;
    await writeCache(cacheList);
  } catch (error: any) {
    errorHandle(error, true);
  }
})();

因为 Alfred Workfows 功能块间传输数据时会替换上一个模块的输出,所以要将项目绝对路径先存在运行时的环境变量内,在入口文件执行时再取出使用,选择的应用名称则作为运行时参数使用。

猎豹Cheetah插件开发 - Alfred 篇

猎豹Cheetah插件开发 - Alfred 篇

获取到项目绝对路径与应用名称后即可写入缓存完成配置了。

错误处理

项目中创建了一个错误处理函数如下:

export function errorHandle(error: any, notice: boolean = false) {
  const errorCode: number = error.message;
  const errorMessage = ErrorCodeMessage[errorCode];
  if (notice) {
    console.log(`Error:${errorMessage}`);
    return
  }

  console.log(
    JSON.stringify({
      items: [
        {
          title: 'Error',
          subtitle: errorMessage,
          arg: '',
          valid: false,
          icon: {
            path: 'assets/empty.png',
          },
        },
      ],
    })
  );
}

Script Filter 功能块中报错为直接输出到列表:

猎豹Cheetah插件开发 - Alfred 篇

如果需要系统通知的话,需要 Workflows 的功能块配合,这边直接输出报错信息即可,后续处理过程如下:

新建判断功能块,如果输入内容以 Error 开头则判断为报错信息,后面连接一个参数拆分器,其作用与 js 中的split 类似。 猎豹Cheetah插件开发 - Alfred 篇

猎豹Cheetah插件开发 - Alfred 篇

最后连接发送系统提示功能块,text 输入框为上一个功能块处理完的结果,取第二个参数。 猎豹Cheetah插件开发 - Alfred 篇

构建发布

猎豹Cheetah插件开发 - Alfred 篇 Alfred Workflows 的发布比较简单,项目构建完以后直接在 Alfred 中右键插件名称,在菜单中选择 Export,后在导出面板中填写插件信息,插件描述等信息,点击 Export 后选择一个存放路径就完成了。

猎豹Cheetah插件开发 - Alfred 篇

小结

至此 Cheetah for Alfred 1.5 的开发、发布已经完成,相信各位看官已经对 Alfred Workflows 开发有一定的了解了,可以尝试创建一些工作流,代替自己完成一些繁复机械的工作。

下一篇将介绍 Cheetah for Raycast 的开发,敬请期待~