惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

H
Help Net Security
博客园_首页
酷 壳 – CoolShell
酷 壳 – CoolShell
G
Google Developers Blog
Jina AI
Jina AI
C
Check Point Blog
Apple Machine Learning Research
Apple Machine Learning Research
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
GbyAI
GbyAI
Last Week in AI
Last Week in AI
罗磊的独立博客
Hugging Face - Blog
Hugging Face - Blog
阮一峰的网络日志
阮一峰的网络日志
The Cloudflare Blog
M
MIT News - Artificial intelligence
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
J
Java Code Geeks
WordPress大学
WordPress大学
博客园 - 聂微东
月光博客
月光博客
博客园 - 司徒正美
T
The Blog of Author Tim Ferriss
大猫的无限游戏
大猫的无限游戏
Microsoft Security Blog
Microsoft Security Blog

少数派

派早报:Google 发布 Fitbit Air 等 - 少数派 「新人报到」確認需求,再開始 - 少数派 从 SOLO 独立开发者社区,我看到了越来越多开发者开始做自己的产品 - 少数派 我怎么管理那些"不常做,但总会忘"的生活事项 - 少数派 人形机器人量产元年,数据才是具身智能的“生死线” - 少数派 BuhoLaunchpad 高度还原 Mac 启动台:开发历程与思考 - 少数派 五年陪伴依然不舍,DIY 换壳后让罗技 MX Master 3 继续服役 - 少数派 新玩意 240|少数派的编辑们最近买了啥? - 少数派 一日一技|为什么你应该关闭 iOS 的键盘声音 - 少数派 我做了个插件和 Skills,一键提取任何网站的设计规范 Design.md - 少数派 住在三四线城市的你,该开始录播客了 - 少数派 甘南秘境,大白高国 - 少数派 AI的审美:谁让把我变成川内倫子 - 少数派 返工怎能不烦恼,打工人片单总有一部是你的「嘴替」 - 少数派 为了让「上厕所」更健康,我做了一个小工具 - 少数派 AI + Skill,能够让生成的文章去除 AI 味吗? - 少数派 新玩意|韶音OpenDots ONE 耳夹式耳机 - 少数派 《美满》| 在每一个春天的晚上相爱(362) - 少数派 新玩意|优篮子 PS01 MagSnap 磁吸支架 - 少数派 自我整合手记 | 我开始早睡了:用稳定规则,为自由托底 - 少数派 用龙虾(OpenClaw)两个多月,我最深的12个体会 - 少数派 听歌时间到,12 张你可能错过的 2025 华语乐坛好专辑 - 少数派 承诺能追吗 - 少数派 macOS 26启动台没了? 我做了个不一样的App启动器 - Keboard - 少数派 《四海为家的人》| INTJ对话INTJ(361) - 少数派 你发过的那些黑历史,是时候一次清干净了 - 少数派 新玩意:安安静静玩,越玩越专注:计客密码机 - 少数派 iPad 用户首次体验 Android 平板:vivo Pad6 Pro - 少数派 数据逻辑强 - 少数派 极北行+ | 一路向北,探访日本至北之地 | 001 - 少数派
用 Scriptable 做个 iTerm 风格小部件 - 少数派
2022-08-08 · via 少数派

iOS 平台可以自定义小部件的 app 有很多,有那么几种是和代码相关的,例如 JSBox、Taio 和本文的这款 Scriptable。不用码的小部件,可以根据 app 提供的方式进行自定义,而用码的小部件,则可以完全根据自己的需求来写规则,并且可以组合不同类型的信息。有码就有了更强大的自定义属性,但同时又一定程度上提高了门槛。这时候就会出现很多已经写好的示例提供给使用者,不想研究的人开箱即用,懂码的人也可以根据需求自己改改。

虽然 Scriptable有官方推荐的小部件库,但是有一些小部件是无法做到开箱即用的,例如本文要讲的这一款 iTermWidget。这是一款长的像 iTerm 的 Widget。iTerm 是一款  macOS 平台的命令行工具。命令行就是用纯文字敲出来和展示出来的东西,而 iTerm 通过区分不同代码的颜色来让信息更加易读,这样的展示方式成为了一种特色,才出现了现在这个小部件。

想要一个这样的小组件,是因为其他 App 自带的小部件所能展示信息的方式比较有限,通常我只能选择开发者做好的样子。而这款 iTermWidget 最大的特点就是字小,意味着展示的信息够多。其次它没有过多复杂的展现方式,仅仅是依靠文字和换行颜色,从信息的传达上不够直观但足够简洁。可以拥有展示多达 6 行信息的小部件(并且还可以进一步自定义),是我一直想要的。

正常情况下,在软件自带的 Gallery 里面「GET」到这个小组件后直接运行,是没有办法成功的。我在前两次下载这个 Scriptable 的时候就败在了这一步,当时就删掉了。这款软件内的小部件都是用 JavaScript 来写的,没有代码基础的人就完全无从下手。而刚好最近我在学 JavaScript,心想也许能解决之前没有搞懂的问题,想再尝试一下。但不用担心,下面的内容不涉及到 JavaScript 的具体语法等内容,仅仅是一些通俗易懂的常规操作。

仅需3步,让它先跑起来

在摸索了一番之后,终于搞清楚了能让小部件先成功显示出来的步骤,总结为以下三点,比较适合没有代码基础的朋友跟着尝试一下:

  1. 将 6 个基本信息填入「TODO」
  2. 配置一个 Cache.js 文件
  3. 获取一个属于你自己的 OpenWeatherMap API KEY

完成以上三步,再运行小部件,就能看到示例当中运行成功的样子了。

零、下载 iTermWidget

首先,在 App Store 下载 Scriptable,这款软件是完全免费的。

然后,在 Gallery 中下载好这个 iTermWidget 。点击「Scripts」页面里小部件右边的「···」,进入代码的页面。不要怕码,下面会一点点告诉怎样做,就像改稿子一样。

一、将6个基本信息填入「TODO」

小部件所展示的信息源自每个使用者的信息库,因此作者设定了这 6 个 「TODO」项,让大家根据自己的实际情况来进行预定义。(但是这也造成了这款 Widget 无法开箱即用的结果)

打开代码即可看到第 26 行就标出了 「TODO」的字样,作者已经把需要预定义的内容罗列在了这里,下面让我们来一一填入:

注意!下方只需替换「TODO」这四个字母,它两边的「’」符号是要保留的!

  1. NAME: 就是你想要给自己起的名字,任意即可,哪怕保留「TODO」也是可以的。
  2. WEATHE_API_KEY:这个内容比较复杂,留在第三部分展开来讲。
  3. WORK_CALENDAR_NAME:这是你要展示的个人日历的名称,我用的是「work」,你可以根据自己的实际情况填写,注意,这里开始就需要和日历一一对应了,如果有误,是跑不成功的。
  4. PERSONAL_CALENDAR_NAME:同上,我用的是「personal」。
  5. PERIOD_CALENDAR_NAME:作者默认加了这项,作为男生我其实没有用到,所以「先」简单同「Personal」,仅是为了跑成功。
  6. PERIOD_EVENT_NAME:这一项是具体日历时间的名称,我没有,所以我随意写了个「p」(这一项写错了也是不影响代码跑成功的)

好了,你已经填写好了需要预定义的内容了(第 2 项需要在后面展开说),接下来该进入下一步了。

二、配置 Cache.js 文件

这个 iTermWidget 的运行需要用到缓存,简单说就是这个程序需要依赖另一个小程序作为工具。而这一个 Cache.js 文件,也不需要大家写,有作者 (EvanDColeman) 已经写好了,直接拿来用就好了。

1. 在 Scriptable 中点击右上角新建一个文件

2. 将所有代码(为了不影响观感,贴在了文章最后)复制粘贴进这个文件

3. 点击「Done」,退出,并重命名为「 Cache」即可。

这两个文件同时存在,才能顺利运行

三、获取一个 OpenWeatherMap 的 API KEY

OpenWeatherMap 是一个获取天气信息的服务。而要想从这个服务商获取到天气信息,就需要注册一下,生成一个属于你的 API KEY,来去向这个服务商索取这项服务。为什么要有这个 KEY 呢,因为这项服务,会根据使用次数和功能的多少来进行收费。(所以各位要保存好自己的 KEY,不要透露给别人。)但对于普通使用者来说,免费的额度是完全完全够用了(大概是每天1000次,个人用户一天肯定是不会超的),因此我们只需要去生成这个 API KEY,就接近成功了。

  1. 访问 openweathermap.org,注册一个账号。
  2. 登陆之后,点击「Pricing」,别怕,是「购买」一个「不用花钱」的服务。
  3. 点击「Subscribe One Call by Call」,这里需要填入一下自己的付款信息,但是最终是不需要付款的,只有使用次数超过了免费额度才会扣钱,后面会说避免扣款的方法。
  4. 都确认好之后,右上角个人名称,下拉菜单中的「My API keys」,进入到 API KEY 的管理页面。
  5. 我已经忘记是否有一个默认 KEY 了,如果没有,那么就点一下 「Generate」,就会出现一个随机码形式的一个 KEY,这个 KEY 可要保存好。
  6. 将这个 KEY 填入到第一大步骤中的第 2 条,那么「TODO」的部分就全部完成了。
  7. 别急!还差一小步,可以帮你避免万一被扣费的步骤!点击名字的「My Service」后,可以在上方看到一个「Billing plans」,可以看到已订阅的「One Call by Call」,将「Calls per day」改成 1000,就是免费额度的上限。那么这样子就不怕超额度扣费了。
绿色遮盖的部分,就是属于个人的 API KEY 了,一定要保存好不要外泄
将 Calls per day 设置成 1000, 就不会突破免费额度了

完成,显示。

好了,当完成了以上三步之后,在主屏幕设置好这个小组件,然后运行这个 Script,稍作等待,就可以看到这个小组件能够顺利展示内容了。那么,自定义这个 iTermWidget 的第一步就达到了。上述的讲述过程,完全照顾了不懂代码的朋友(因为我也不怎么懂)。而如果想要深度的自定义一下,只需要后续再稍微摸一点代码的规律。

完成以上三步,主屏幕就会正常显示这个示例了。

后续

其实如果你仔细的读了 iTermWidget 的介绍页面,会发现作者已经写好了配置步骤(但仅有上面提到的两项内容,他默认了大家懂得如何填写 openweathermap 的 apikey ),只不过第一次尝试下载下来点击发现不好用,就想什么鬼啊就删掉了(正如前两次的我)。而这一次我仔细看了之后发现只需要简单配置一下就好了。只不过第三步作者没有明说,对于不了解 API KEY 的朋友确实有点小小的门槛。

接下来可以对显示内容进行自定义,比如最初步的,可以关注一下:

  1. 第 90 行显示的红字内容,就是 iTermWidget 上面第一行的文字,可以自行调整。
  2. 第 96 行显示的就是第二行的文字, ${NAME} 就是会被TODO第一项所替代,而它左右两个反括号「`」内的红色文字就是剩余的显示文字,可以替换成其他文字或者 EMOJI 表情也可以。比如

更加「有用」的自定义,当然是替换掉整一行的显示内容啦。比如经过我稍稍的调整,显示出了以下的内容:删掉了一些日历,然后补充了一个展示行情的内容。这一块内容就需要进一步处理码了,我也在思考需要加一些什么内容进去比较好,同时也要想想如何实现以及如何讲述(比如这个显示的行情,我就是把组件库里面的 Crypto 部分码摘过来改吧改吧实现的)。

希望我能在短期内研究出其他自定义的方式,并且能用简单的不用深入理解码、仅仅靠复制粘贴,就能实现最终的效果。

这个文章也许有(二),但也不一定。

目前只保留了这三行,未来还会继添加更多信息

来自 EvanDColeman 的 Cache.js,https://github.com/evandcoleman/scriptable

// NOTE: This script was written by evandcoleman: https://github.com/evandcoleman/scriptable

class Cache {
  constructor(name) {
    this.fm = FileManager.iCloud();
    this.cachePath = this.fm.joinPath(this.fm.documentsDirectory(), name);

    if (!this.fm.fileExists(this.cachePath)) {
      this.fm.createDirectory(this.cachePath);
    }
  }
  async read(key, expirationMinutes) {
    try {
      const path = this.fm.joinPath(this.cachePath, key);
      await this.fm.downloadFileFromiCloud(path);
      const createdAt = this.fm.creationDate(path);

      if (expirationMinutes) {
        if ((new Date()) - createdAt > (expirationMinutes * 60000)) {
          this.fm.remove(path);
          return null;
        }
      }

      const value = this.fm.readString(path);

      try {
        return JSON.parse(value);
      } catch (error) {
        return value;
      }
    } catch (error) {
      return null;
    }
  }
  write(key, value) {
    const path = this.fm.joinPath(this.cachePath, key.replace('/', '-'));
    console.log(`Caching to ${path}...`);

    if (typeof value === 'string' || value instanceof String) {
      this.fm.writeString(path, value);
    } else {
      this.fm.writeString(path, JSON.stringify(value));
    }
  }
}

module.exports = Cache;