Logo

site iconBryan

英文昵称 Singee,INTJ,经济学+法学双专业,产品经理、工程师、投资人。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

Bryan RSS 预览

使用 TypeScript 撰写 OmniFocus 脚本

2023-12-13 10:08:49

OmniFocus 4 即将发布!在我多年管理我的待办的过程中,我尝试过 Todoist、滴答清单、Things、Sorted 等等几乎所有市面上的 TODO 软件,但最终,OmniFocus 终成我一直以来的最终选择。而谈及 OmniFocus 的强大性,不得不提的就是他强大的自动化能力 —— Omni Automation

Omni Automation 实际上是基于 JS 脚本的,而编写纯 JS 脚本的过程…… 一言难尽。虽然 Omni Automation 官方提供了 TypeScript 的定义文件,但一方面难以做好类型检查,另一方面其详尽程度仍有待提升(长久不更新、大量使用 any 等),此外,由于缺乏打包工具,代码逻辑的复用也显得颇为困难(我甚至很长一段时间都是靠着 Mac 版本 OmniFocus 的一个 bug 实现的逻辑复用)。

为了庆祝 OmniFocus 4 的面世,我决定将我个人开发并使用的方案整理开源,包括打包脚本和类型定义,还有我使用的一些工具函数及脚本,希望可以让更多人能够愉快地编写 OmniFocus Script。

使用

  1. 使用此模板创建一个仓库
  2. 克隆你创建的仓库
  3. 运行 pnpm install 安装依赖项
  4. 运行 pnpm build 构建脚本

脚本源码放在 src 目录中,编译结果(可被 OmniFocus Scripts 使用的)放在 dist 目录中。

撰写脚本

src 目录内的任何不以 _ 开头的 TypeScript 文件都将被视为 OmniFocus 脚本并编译(_ 开头的脚本文件被保留用于工具函数)。

任何脚本都必须遵循以下模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export const action = new PlugIn.Action(function (selection) {
// do anything you want
});

action.validate = function (selection) {
// do anything you want
};

export const meta: Meta = {
label: "...",
description: "...",
identifier: "...",
author: "...",
version: "0.1",
};

其中:

  1. actionmeta 是必需的,action.validate 是可选的
  2. meta 必须是脚本的最后一部分。它之后不可以有任何内容。

构建与使用

运行 pnpm build,构建后的脚本(以 .omnifocusjs 结尾)将被放置在 dist 目录下。

你可以直接将 dist 目录中的脚本拷贝到 OmniFocus 的脚本目录,也可以利用脚本进行同步。

如果你使用 iCloud 保存 OmniFocus 脚本,可以直接使用 pnpm sync 自动将构建好的脚本同步到 iCloud 中的 OmniFocus 脚本目录;如果你不使用 iCloud 而是使用了自定义路径,可修改 sync.sh 文件改变目标路径。

End

此方案我个人已用一年有余,但一方面开源版本可能有些错误,另一方面可能有更多的定制化需求。

欢迎进入 仓库 页面提交 Issue 和 PR!

[备忘] Go init 行为

2023-12-12 06:29:38

总结

基础规则:

  1. 所有的 init 函数都在一个 Goroutine 中执行(但请参见下面的特殊注意)
  2. 如果 package a 引用了 package b,那么 a 的 init 一定在 b 的 init 运行完成后运行
  3. main package 的 main 函数一定在其他 init 函数均运行完成后再运行(即运行顺序为 package 的 init -> main 的 init -> main 的 main)
  4. 同一 package 中的多个文件中的 init 执行顺序未定义,同一文件中的 init 自上而下运行
  5. 如果 package a 同时引用了 package b 和 c,那么 b 与 c 的 init 顺序在 Go1.21 及之后定义

在 Go1.20 及之前:

  1. 如果 package a 引用了 package b,那么 b 的 init 一定在 a 之前运行
  2. 但是,如果 package a 同时引用了 package b 和 c,只要 b c 之间没有引用关系,b c 的执行顺序是不定的

在 Go1.21 及之后:

  1. 对于无引用关系的包(即 Go1.20 及之前的中的第 2 点),按照其包名字母序决定引用顺序(例如 a 一定在 b 之前执行,github.com/xxx/xxx 一定在 gitlab.com/xxx/xxx 之前执行)

特殊注意:

  1. 如果 init 存在阻塞,那么用于运行 init 的 goroutine 可能创建新的 goroutine,这会导致某些 init 代码并发运行
  2. 存在阻塞的情况下,不会保证无引用关系的 package 的 init 完成先后顺序(参考示例 c)
  3. 存在阻塞的情况下,如果 package a 依赖了 package b,那么 a 的 init 一定在 b 的 init 运行完成后开始运行(参考示例 d)

示例项目

https://github.com/singee-study/go-init

参考

The Go Memory Model
Go 1.21 Release Notes

[随笔] 一开始就公布定价

2023-11-25 17:13:06

看到了 FeedbackTrace 这个项目,看起来不错,但是我不会去使用,一个主要的原因就是它没有公布定价。

如果去选择一个一个直接面向 C 端的第三方产品,我一定会选择已经有了明确定价的 —— 否则我将承担巨大的未来的风险。如果未来定价不满足我的预期,我需要去考虑怎么迁移数据、怎么抹平用户体验差异等,一方面可能带来不确定的时间成本,另一方面可能产生糟糕的终端用户体验。

面对公测期间的价格和上线后不一致的做法,各个产品有各个产品不同的思路,最统一的就是降价后下一期账单自动降价、涨价保持原有价格不变或至少保持原有价格一定时间。特别的,Rewind 在 Early Access 期间的策略是调低定价后为历史订单全额退款、调高价格保持用户的原有价格不变,而 ChatGPT 的策略则是调低定价后给予用户差额价格退款并给予额外补偿。

多说一句,公测免费并不是不预先提供价格点的「借口」,完全可以预先告诉哪些是付费点但是公测期间可以免费使用(例如 Omnivore 明确在文档中说明了它的哪些功能未来可能是收费的)。

因此,我的产品,也一定在最初就给出一个收费点定价方案 —— 哪怕不够完美。大体来说,我会提前将价格和收费点列明,公测期间可以免费使用收费功能(或部分收费功能),而预先开始订阅收费版本的用户(即早期支持者)承诺未来如果存在价格调整他们一定可以「使用最低且不高于现有的价格得到最多的服务」。

[随笔] Server Action

2023-10-27 07:48:02

最近在一个自己的小 Side Project 中使用了一下 Server Action,感受大概是

  1. 更多的其实是替换原来的 /api 路由,可以把它想做一个 TypeSafe 的 RPC,原来需要自己去定义 API 然后生成代码,这些 Server Action 直接帮你做了
  2. 因此,感觉原来用了 Next.js 的 /api 路由的,迁移到 Server Action 会有较大的体验提升
  3. Server Action 本质只能向前端发送一个 JSON Object,缺少了标准 HTTP/RPC 的 StatusCode 和 Headers 的能力,因此如果真的想去很好的用它可能 Server 和 Client 仍然需要包一层
  4. 目前 Server Action 实质上缺少触发前端状态更新的逻辑,如果很需要其实可以利用 WebSocket 或者利用「包的那层」来手搓,实现起来其实也不难
  5. 我遇到 Server Action 最主要的问题其实是它触发调用的那层做的事情实在是太多了,会触发很多 React 内部的状态转换逻辑,另外我发现它与 useReducer 一起用甚至还有 bug(哪怕是目前 Next.js 14 Stable 了)

综上,全栈项目玩玩可以,其余的…没啥必要

[随笔] Swift 异步:Task vs DispatchQueue

2023-09-21 09:31:41

在 Swift 上,执行一个异步的函数大体上有两种办法

  • Task
  • DispatchQueue

背景知识:线程和队列

Swift 同时支持多线程和异步,因此

  • 存在主线程和多个后台线程
  • 每个线程存在若干队列(若干个全局 global 队列(每个优先级一个)、自定义队列(人为创建))

背景知识:队列优先级

在队列层面,存在优先级的概念(在 Task 中叫 priority,在 DispatchQueue 中叫 qos)

  • userInteractive:最高优先级,适用于 UI 操作(例如动画等)(在 Task 中被弃用)
  • userInitiated:较高优先级,适用于用户触发的操作
  • default:中优先级,默认(在 Task 中被弃用)
  • high, medium, low:高中低优先级(仅在 Task 中存在)
  • utility:较低优先级,适用于耗时的后台任务
  • background:最低优先级,适用于耗时的后台任务
  • unspecified:继承于 Thread.current.qualityOfService(在 Task 中被弃用)

背景知识:main

main 同时隐含着两个概念:main thread 和 main queue,事实上无需特意区分(可以认为只有 main thread 才有 main queue,而 main thread 的不同 queue 之间并无特殊区别)

main 的主要用处在于其是直接和用户交互的,只有在 main 才能修改 UI、如果 main 繁忙用户会感觉到 UI 刷新卡顿

  • 对于 Task 而言,在 main 执行使用 Task { @MainActor
  • 对于 DispatchQueue 而言,在 main 执行使用 DispatchQueue.main

注意,Task 的 @MainActor 实际上并不是让闭包代码在 main 执行,而是让其他执行它的 concurrency-aware 代码逻辑在 main 上执行(但是目前存在非 concurrency-aware 的代码,因此可能标记了 @MainActor 实际上仍然是在非 main 执行的,这种时候需要手动切换到 main;不过这种情况 XCode 会有警告,所以不必过于担心)

DispatchQueue 相关

  • 依赖的是 Grand Central Dispatch(GCD) libdispatch
  • Dispatch.main 是在主线程执行的,其他均不保证实际执行线程
  • 执行顺序先进先出
  • 队列类型分为 serial 和 concurrent 两种,serial 在执行完一个任务后才会开始执行另一个、concurrent 会同时执行多个任务(故不保证任务的结束顺序);main 是 serial 类型的、默认也是 serial 类型的
  • 执行时存在 sync 和 async 两种,sync 会等待这个任务完成后返回,async 会直接返回
  • 因上述特性,在 main 线程执行 DispatchQueue.main.sync 会导致死锁
  • 无法直接获得执行结果

Task 相关

  • Task 有 child 和 detached 两种类型,区别在于后者无法访问到调用方可见的变量
  • 来自于 main 创建的 child Task 会始终在 main 执行,否则(除非标记 @MainActor)不保证执行的线程/队列
  • 已提交的任务可取消、可获取(等待)结果(获取结果需要 await)
  • 如果和 DispatchQueue 类比,可以认为 Task 与 DispatchQueue.async 执行上的行为一致

额外:RunLoop

  • RunLoop.current 返回当前的线程循环、.main 返回主线程循环
  • 通常情况下,无需特别注意 RunLoop.main 和 DispatchQueue.main 的区别,二者都是在 main 上执行逻辑
  • RunLoop.main 中执行的逻辑可以被外部用户操作暂停,而 DispatchQueue.main 不会,因此在处理滚动时可能更希望使用 RunLoop.main,而其他通常场景则一般使用 DispatchQueue.main