乍听之下,不无道理;仔细揣摩,胡说八道

0%

“实战Elisp”系列旨在讲述我使用Elisp定制Emacs的经验,抛砖引玉,还请广大Emacs同好不吝赐教——如果真的有广大Emacs用户的话,哈哈哈。

序言

上一篇文章的末尾,我说有一个更优雅的办法实现快速设置任务的开始时间,本文便来揭秘这个办法。

阅读全文 »

本文是“实战Elisp”系列的第一篇。本系列旨在讲述我使用Elisp定制Emacs的经验,抛砖引玉,还请广大Emacs同好不吝赐教——如果真的有广大Emacs用户的话,哈哈哈。

序言

用Emacs的org-mode安排业余时间颇有些时日,渐渐地开始编写一些Elisp函数来改善自己的使用体验。

日程管理中一个常见的需求,便是给任务设定一个开始时间。在org-mode中,这可以调用函数org-schedule实现:将光标移动到一个任务上,再按下C-c C-s,会出现一个日历界面,帮助选择日期和时间来作为日程的开始时间。具体效果如下图所示

阅读全文 »

序言

9102年都已经过去好几天了,现在才来产出年终总结。

个人项目

cuckoo——定时提醒工具

18年的总结中,我提到自己开发了一个名为cuckoo的工具,用来代替macOS的提醒事项、日历,以及由crontab调用的shell脚本。这个目标在19年得以实现,cuckoo已经完全取代了它们。

cuckoo实现了以下功能:

  1. 创建一次性和周期性的提醒。cuckoo甚至可以在正确的二月最后一天弹出提醒——不管是平年还是闰年;
  2. 利用alertercuckoo可以在提醒弹出后推迟它(5分钟、10分钟),或推迟到指定的时刻;
  3. 利用Server酱cuckoo可以把提醒以微信消息推给手机;
  4. 利用ControlPlane,实现按场景提醒——比如10点钟若在公司就提醒自己开晨会,若在家则绝不弹出。

我还提供了给Emacs用的minor mode和Alfred Workflow,以提高易用性:

  1. 在Emacs的org-mode中启用这个minor mode后,只需要按下C-c r便可为光标所在的条目创建提醒;
  2. 一个条目切换至DONECANCELLED状态时,也会自动更改cuckoo中任务的状态(感兴趣的读者可以移步之前的文章);
  3. Alfred Workflow便于创建一次性提醒——比如提醒自己在25分钟后打开支付宝的蚂蚁庄园看看有没有鸡贼。

wa——Alfred Workflow脚本

在18年入手MBP后不久,我便入手了Alfred,并购买了Powerpack。平均每天使用Alfred 110次,大多是Snippets(auto expansion真香)、Clipboard(临时存储文字和图片的绝佳位置)、Workflow。常用的Workflow都是我自己开发的:

  1. unit用于快速输入不同时间长度的秒数的,例如输入6天的秒数6 * 24 * 60 * 60 * 1000
  2. upload用于上传图片到GitHub,把GitHub当图床用(感兴趣的读者可以移步这篇文章);
  3. gt用于获取指定日期的UNIX时间戳、int用于获取一些预设的时间戳(例如“昨天0点”);
  4. yl用于精确设定macOS的音量;
  5. bqb用于斗图(感兴趣的读者可以移步这里)。

这些脚本都收集在名为wa的私有仓库中。由于需求比较稳定,这个仓库的迭代不多。

jjcc——将LISP语言编译为x64汇编

不害臊地说,jjcc是一个用Common Lisp写就的编译器,运行在SLIME中,如果投喂它某种LISP方言代码,就可以编译出跑在macOS上的x64汇编代码。这是我的第一款生成汇编指令的编译器,为此还恶补了不少汇编语言知识,尤其是x64的calling convention。这款编译器的开发过程写成了文章发表在博客和知乎上,感兴趣的读者可以移步这个专栏

在完成了蹩脚的自定义函数特性后,我读了《An Incremental Approach to Compiler Construction》这篇论文,它循序渐进地开发一个Scheme语言到x86汇编的编译器,并且阶段划分得更好,后来我也按照论文的思路重新实现了一遍。

savemoney——未完待续的RescueTime代替品

19年10月份时RescueTime Premium到期,由于太鸡肋了便不再续费。我仍然有time-tracking的需求,于是打算自己动手写一个代替品。皇天不负有心人,我找到了active-win这个库,它可以获取当前有焦点的窗口的元信息。基于这个库我写了两个脚本:

  1. savemoney.js,每隔一秒调用active-win获取当前激活窗口的元信息,然后写入到Redis中;
  2. accounting.js,不停地从Redis中取出数据,运算后写入到MySQL中。

目前仅仅是将数据记录在了MySQL中,没有做报表和统计。这两个脚本通过Launchd在每次登录后自动运行。

写作

19年在GitHub博客一共发表了28篇博文,数量差强人意,质量亟待提高。在读了利用金字塔原理写出好文章后,我逐渐改进自己的写作方法,以期写得更有条理。

除了GitHub博客和SegmentFault,我还在知乎上发表了一些文章,大多是关于jjcc编译器的。刚才我说文章的质量亟待提高,如果你看过jjcc编译器系列的文章的话,一定会很同意我的观点。

博客和SegmentFault的受众很小,于是我又开通了微信订阅号,希望可以在技术圈子里更多的展示自己——有没有值得展示的内容另说。在订阅号上发表文章多了一种无形的压力,毕竟这些内容更容易被同学、同事、同行看到(前提是人家乐意看)。万一写的很糟糕,可就糗大了。目前订阅号关注者寥寥,有兴趣的可以微信搜索“小打小闹写点bug”关注。

学习、进修

19年读完的书不多:

  1. 《MongoDB in Action》,没什么太大的收获;
  2. 《High Performance MySQL》,看过后确实有些帮助,切实根据书中的指导调优过生产环境的MySQL;
  3. 《Algorithms》,光看没练手;
  4. 《Linux Shell Scripting Cookbook》,看着看着幡然醒悟这东西只要用到的时候查阅就足够了,遂弃之。

还有许多在读经典书籍,如《Clean Architecture》、《重构》。刚开始我会在上下班搭地铁时读这些书,后来将看书时间固定在了每天下午一点至一点半。这些在读的书像一条队列,我每天会读队头的书,然后放到队尾。这样一来,每天都在涉猎不同领域的内容。

在18年9月,我闭门造车地整理了一份Web后端软件工程师的技能树,再据此来寻找要读的书。例如,上述的《MongoDB in Action》是依据“后端知识/文档数据库/MongoDB”找的,《Linux Shell Scripting Cookbook》是依据“后端知识/命令行操作”找的。技能树整理得好不好暂且按下不表,但渐渐地我忘记了看书的初衷,成了“为看而看”。看似每天中午都在学习,实际上由于目的性不强,收获不大。由于每天读不同类型的书,同一个主题的学习过程也变得支离破碎。这个学习方法已经到了迫切需要优化的地步。

macOS更新换代——AppleScript来袭、Chrome上位

19年10月升级到macOS Catalina,然后问题便接踵而至。首当其冲的是alerterterminal-notifier没法用了,无法在右上角弹出提醒。一番折腾后不见起色,只好先用AppleScript代替,让cuckoo可以弹出提醒。AppleScript的display notification功能远不及alerter那么丰富,聊胜于无吧。

Firefox也开始闹别扭。只要打开Firefox稍微用两下,就会有一个名为FirefoxWebCP Extension的进程疯狂地使用CPU,Firefox内的各标签页也纷纷失灵转圈,几天下来都是如此。没办法,只好起用Chrome。稍微磨合后发现Chrome其实挺不错,各方面都今非昔比。以前之所以一直坚守在Firefox的阵营,主要是因为:

  1. Firefox的Pocket插件更好用——早年间不需要打开Pocket的网站即可查看自己的列表,不过自从Firefox集成Pocket后,这个优势已经荡然无存;
  2. Firefox的Vimperator更好用——这也一样,Vimperator逝者已逝,继承者Vim vixen和Chrome的Vimium大同小异;
  3. Firefox的地址栏搜索浏览历史更好用,这一点迄今未被Chrome超越——Chrome的地址栏要么搜不到,要么必须输入更多关键词,然后还是搜不到。

希望Mozilla在2020年可以修复这个问题,让我重回Firefox的怀抱。

CL虐我千万遍,我待CL如初见

2019年的Common Lisp依然让人哀其不幸怒其不争,我也依然痴迷于这门古怪的语言。但痴迷不能当饭吃,要将CL投入到实际应用实在太难。且不说Quicklisp上库的数量远不及PyPI和NPM,质量也令人抓狂。这不,都9102年了,访问一个返回JSON数据的HTTP接口,还得先用drakma发出请求(也许不支持HTTP/2),再用flexi-streams将字节数组转换为UTF-8编码的字符串,再用cl-json解析一番。拿到一个列表对象后,再用carcdrassoc一顿操作猛如虎,才能拿到需要的数据。

2020年,MAKE CL GREAT AGAIN!

记账

在年中的时候,萌生了换记账软件的想法,因为挖财用起来越来越不爽了,而且整天记流水账也没什么收获。一番摸索后,我决定尝试一下复式记账法,并选择了GNU Cash——早年间用过一次,但没看入门手册就开始用,根本玩不转。这一次倒是读了手册,但GNU Cash的UI和操作方式还是无法让我心动;之后知道了beancount,却无法在我的系统中顺利运行;最终我选择了ledger,它是一个命令行程序,不负责记录,只负责读取手打显诚意的交易明细,然后产出报表。Emacs有一个ledger-mode插件,两者配合用来记账超痛快。

结尾

在2020年我希望至少能完成:

  1. 发布cuckoo
  2. 发布wa
  3. 开发一个alerter的代替品;
  4. 写更多的博文,让微信订阅号的粉丝涨到130;
  5. 给CL写一些库解决一些常见的需求

最后

序言

我在上大学的时候并没有接触过VCS(版本控制系统)。虽然曾经在Google Code发布过去项目,但是以压缩包的形式发布的;与室友合作开发计算机网络这门课的课程设计时,也没有用上。直到入职第一家公司后才真正开始使用,当时用的是Git,此后也始终没用过其它的VCS——SVN仅仅耳闻未曾使用——转眼间已经用了六年多的Git了。

尽管日常使用问题不大,但对于Git的内部运行原理我仍然是一知半解——也不是我谦虚,基本就是不懂吧。例如,使用git addgit commitgit branch等命令的时候,Git在背后究竟做了什么,我是答不上来的。好在互联网上有许多这方面的资料可供学习,我硬着头皮看了不少文档和博客后,总算是习得了一些皮毛。

现在,我试着循序渐进地讲解一遍吧。

git add的时候发生了什么?

首先创建出一个仓库并向其中添加一个文件

1
2
3
4
5
mkdir git-test
cd git-test
git init
echo 'hello' > a
git add .

到此为止,暂时不要提交改动。现在,我来看看Git到底在背后做了些什么。Git的秘密都藏在叫做.git的目录中,尤其是其中的objects目录。用tree命令查看这个目录的结果如下

1
2
3
4
5
.git/objects
├── ce
│   └── 013625030ba8dba906f756967f9e9ca394464a
├── info
└── pack

与运行git add前相比,多出了一个叫ce的目录,以及位于其中的叫013625030ba8dba906f756967f9e9ca394464a的文件。这个文件其实就是a的一个“副本”,其中存储着文件a的内容。但是不能用cat直接查看,因为Git对这个文件做了压缩。可以用pigz来得到压缩前的原文,示例代码如下

1
pigz -d < .git/objects/ce/013625030ba8dba906f756967f9e9ca394464a

结果为

1
blob 6hello

Git生成这个文件的规则其实不复杂。首先Git会计算原文件的长度,即6(之所以是6,是因为用echo和重定向写入文件a时,添加了一个换行符)。然后,Git将一个固定的前缀blob(此处有一个空格)、文件长度、一个空字符(ASCII码为0的字符),以及文件内容这四者连接成一个字符串,并计算这个字符串的SHA1摘要。具体到文件a,可以用下面的命令试着计算

1
printf "blob 6\0hello\n" | shasum

或者用Git内置的hash-object子命令会更简单

1
git hash-object a

不管是哪一个命令,算出来的摘要都是ce013625030ba8dba906f756967f9e9ca394464a。然后Git会取前两个字符(ce)作为目录名,在.git/objects下创建新的目录。以从第三个字符开始的剩余内容(013625030ba8dba906f756967f9e9ca394464a)为文件名,将方才拼接好的内容压缩后写如文件。这种文件用Git的术语来讲叫做blob对象,稍后还会遇到tree类型和commit类型的对象。

git commit的时候发生了什么?

接下来提交改动

1
2
3
git config user.email 'foobar'
git config user.name 'foobar'
git commit -m 'test'

此时会发现.git/objects下新增了两个文件

1
2
3
4
5
6
7
8
9
.git/objects
├── 09
│   └── 76950c1fdbcb52435a433913017bf044b3a58f # 新的
├── 14
│   └── c77e71bd06df41e1509280cfba045e1db2aa5f # 新的
├── ce
│   └── 013625030ba8dba906f756967f9e9ca394464a
├── info
└── pack

git cat-file -t可以查看这两个新文件的类型

1
2
git cat-file -t 14c77e71bd06df41e1509280cfba045e1db2aa5f # 输出commit
git cat-file -t 0976950c1fdbcb52435a433913017bf044b3a58f # 输出tree

也可以用git cat-file -p以可读的方式输出新文件的内容。例如用git cat-file -p 0976950c1fdbcb52435a433913017bf044b3a58f输出tree类型的对象的内容,结果为

1
100644 blob ce013625030ba8dba906f756967f9e9ca394464a	a

tree类型的对象中记录着Git所追踪的文件的元信息,包括文件的权限、在Git中的对象类型、对象摘要,以及文件名。另一个commit类型的对象中存储着本次提交的信息,用git cat-file -p查看的结果如下

1
2
3
4
5
tree 0976950c1fdbcb52435a433913017bf044b3a58f
author foobar <foobar> 1576676836 +0800
committer foobar <foobar> 1576676836 +0800

test

第一行表示这个commit对象指向的是哪一个tree对象,从这个tree对象出发,可以遍历仓库中直到本次提交为止、所有被Git追踪的文件。commit指向treetree可以指向blob也可以指向其它的treeblob就像是树中的叶子节点,不再指向其它的对象,它们之间的关系如下图所示

git branch的时候发生了什么?

Git的branch子命令用于创建新分支——虽然我平时更多地使用git checkout -b。既然addcommit的时候,Git会创建出blobtree,以及commit类型的对象,那么创建新分支的时候,Git是不是也会创建名为branch的对象呢?答案是否定的。

Git的分支非常简单——它仅仅是指向某个commit对象的引用,就像是*nix系统中的符号链接一样。所有分支都存储在.git/refs/heads之下。例如文件.git/refs/heads/master中便存储着master分支上的最新提交的摘要

1
cat .git/refs/heads/master # 输出14c77e71bd06df41e1509280cfba045e1db2aa5f

这就是在Git中创建新分支的成本很低的原因——不过是复制一下当前分支在.git/refs/heads下的同名文件而已。我创建一个新分支develop并提交一个新文件b.git/objects下会多出三个文件

1
2
3
4
git checkout -b develop
echo 'good' > b
git add b
git commit -m 'new branch'

三个新文件分别存储着文件b的内容(一个blob对象)、文件b的元信息(一个tree对象),以及本次提交(一个commit对象)。这些文件中没有任何关于develop分支的信息,develop分支仅仅是一个存在于.git/refs/heads/目录下的同名文件。

git merge一个子代时发生了什么?

develop分支是从master分叉出来,将develop合并回master时,Git会进行一次fast-forward的合并。虽然名字很唬人但其实Git做的事情非常简单,只需要将.git/refs/heads/master文件的内容修改为与develop相同的摘要即可。

也可以要求Git不使用fast-forward。先用git reset --hard HEAD^1master分支回退到第一次提交的状态,然后使用下列的命令再次将develop合并进来

1
git merge --no-ff develop

这一次,Git不再简单地修改.git/refs/heads/master文件了事,而是会创建一个新的commit对象。在我的电脑上,这个新的commit对象的摘要为d1403bb629c7a636c724069b22875ed882b54bcc,使用git cat-file -p看看它的内容

1
2
3
4
5
6
7
tree e960ed43b8e6b5fe9b4e57b806f70796da820056
parent 14c77e71bd06df41e1509280cfba045e1db2aa5f
parent db891542d3e44448433ba86c7cd636d8aec3da54
author foobar <foobar> 1576679608 +0800
committer foobar <foobar> 1576679608 +0800

Merge branch 'develop'

有趣的是,这个commit对象有两个“父级”的commit,而不像平常所认识的树形数据结构那般只有一个“父节点”。显然,这两个父节点分别是合并前的master分支的最新一次提交,以及develop的最新提交。

虽然创建了一个新的commit对象,但其实develop分支的最新提交持有的便是整个仓库的最新版本,所以不需要创建新的tree,合并所产生的commit直接与develop分支的最新提交共用同一个tree对象便足够了——在上面输出内容的第一行的摘要,就是develop分支的最新commit所指向的tree对象的摘要。

至此,终于解决了我一直以来的一个困惑。我曾天真地以为,Git在合并两个分支的时候,会将待合进来的分支中的所有多出来的改动,复制到要合进去的分支中去。这都是因为我没有理解分支的本质,Git的分支并不是一根水管,没有哪一个提交是只能装在一个特定的分支中的。Git合并的时候,就像是在一个immutable的树上做修改,只需要创建不多的新committree对象,再引用已经存在的旧committree对象即可。否则,哪能快速地完成两个分支的合并呢。

后记

没想到还写了蛮多内容的,经过这么几次试验,我对Git的核心原理也算略知一二了,暂时不打算继续深入。各位读者如果有兴趣,可以试着制造一次有冲突的合并,然后看看冲突解决的前后,.git/objects目录下会有什么变化。

最后,在摸索Git原理的过程中,我找到了不少优质的参考资料,这里一并奉上:

  1. https://nfarina.com/post/9868516270/git-is-simpler
  2. https://maryrosecook.com/blog/post/git-from-the-inside-out
  3. http://www-cs-students.stanford.edu/~blynn/gitmagic/ch08.html
  4. https://git-scm.com/book/en/v2/Git-Internals-Git-Objects

序言

以前用Windows的时候,我在QQ中添加了很多自定义表情,其中有很多我还为它们设置了短语,以便可以便捷地发出去。后来微信用得多了,也在微信中收集了很多的表情。很多时候,表情包真的是一图胜千言,而且比起直白地说出同样的话,发图片显得更有意思。比如说,某一天你的同事在谈论他朋友的一些事情时,你可以发一张

当然了,很多时候可能手头没有这张图片,或者一时找不到,也可以来一句“你说的这个朋友到底是不是你自己.jpg”,异曲同工。每每遇到这些有趣的图片的时候,我便会将它们保存下来以备不时之需。不过收集得多了之后,在想要用的时候便发现找表情也不是一件特别容易的事情。比如说,我的表情包目录中已经有92张图片了,即便我明确地知道我要找的就是上面这张图,文件名也没记错,但要在九十多张图片中一眼看到它,还是颇具难度的。

好在,咱是程序员,在很多事情上可以动动手指写写代码来予以辅助——找表情图片这件事情,恰好是其中之一。

准备素材

首先,我要祭出神器Alfred。我会编写一个Alfred的Workflow,它起码要能够方便我按名字找到图片,并且能够复制到粘贴板中。这么一来,我就可以直接在IM软件的聊天窗口中发出选好的表情图。

其次,正所谓巧妇难为无米之炊,如果没有表情图片的储备,那制作这么个工具也就毫无意义了。因此,我还得准备一个目录,用来存放所有将来可能会用上的图片文件,这个目录便是~/OneDrive/图片/表情包。OneDrive上1TB的存储空间,放点图片也是绰绰有余了——当然了,也不是非要把图片目录放到一个同步网盘里。

最后,这些搜集回来的图片还不能就这么晾着,必须给它们取一些容易记忆的名字,毕竟之后就靠名字来找它们了。给表情图片文件命名很简单,因为许多图片中含有一两句关键的话。比如说,下面这张图

直接用图中的文字来命名即可。

编写Workflow

现在可以开始编写Workflow了。它的最终形态是下图这样的

作为Workflow入口的是一个keyword为bqbFile FilterFile Filter是一个挺强大的工具,它本身就可以完成我所需要的功能,见如下演示

可见,File Filter本身便足矣完成搜索图片并复制到粘贴板的需求,本篇文章也就到此为止了——才怪,光有File Filter还不够,因为收集回来的表情图片的尺寸并不统一,直接在IM工具中发送的效果并不好,可能有霸屏之嫌。比如文章开头的图片,用来自ImageMagick套件的identify程序可以看到这张图片足足有527像素宽405像素高,一下子占据了半个聊天窗口——群聊的时候,还是应当照顾一下群里其他人的感受的。

因此,Workflow中的第二个对象,便负责将图片缩放为合适的尺寸。第二个对象是一个Run Script Action,Alfred在运行后第一个File Filter后便会接着调用这个对象中所指定的外部脚本。这个对象的配置如下图所示

其中的脚本内容如下

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash
# 将图片等比缩放为300像素的宽度

filename=$(basename "${1}")
suffix="${filename##*.}"
if [ "${suffix}" = 'gif' ]
then
echo -n "${1}"
else
sips --resampleWidth 300 "${1}" --out /tmp > /dev/null 2>&1
echo -n "/tmp/${filename}"
fi

运行这个脚本后,便可以在/tmp目录下得到与原始图片同名的、宽度缩放为300像素的新图片。刚开始我也打算处理.gif文件,但试验后发现sips把GIF缩放成了一张静态图,于是便不处理GIF文件了。

第三个对象很简单,是一个Argument and Variables Utility,配置很简单,直接上图比较直观

第四个对象又是一个Run Script Action,用于将缩放后的图片复制到粘贴板中——没错,这本来是File Filter完成的工作。这个对象同样会调用一个External Script,内容如下

1
2
3
#!/bin/bash
cd /Users/liutos/SourceCode/applescript/
osascript copy_file_to_clipboard.scpt ${1}

咦,复制文件到粘贴板的逻辑呢?别着急,在/Users/liutos/SourceCode/applescript/copy_file_to_clipboard.scpt这个文件中,内容如下

1
2
3
4
5
#!/usr/bin/osascript
# 方法来自这里:https://superuser.com/questions/1132777/copy-an-image-to-clipboard-from-the-mac-terminal
on run args
set the clipboard to POSIX file (first item of args)
end run

它借助AppleScrippt来实现复制文件的功能。

最后一个对象是Alfred内置的Post Notification,用于在一切就绪后在右上角弹出提醒,反馈给Workflow的使用者,它的配置如下图所示

至此,这个集查找、缩放,以及复制图片于一身的Workflow,便大功告成了。怎样?是不是已经跃跃欲试了?

后记

等跃跃欲试的感觉褪去后便会发现,这个Workflow还相当地不成熟:

  1. 使用AppleScript复制文件后,只要在IM软件中一粘贴便会立即发送出去,让人有点猝不及防。我希望的效果,是类似于在浏览器中右键复制一张图片那般的;
  2. 它基于Alfred的File Filter来查找目录下的文件,但File Filter的搜索能力并非很强。虽然从上面的动图看来,它支持以拼音来搜索,但很多时候稍微多打几个字母,便什么结果也没有了。如果可以支持模糊查找,甚至全文搜索乃是极好的;
  3. 表情图片的文件名需要自己维护,每次收集到新的表情时都需要自己手打显诚意。若是有一个配套的工具可以从图片中提取出文字来自动命名便更好了——OCR了解一下?;
  4. 最后,当收集的表情图片多起来后,许多图片便不好找了,毕竟谁也无法很容易地记忆九十多张图片的名字。渐渐地,很多图片的使用率也会下降,变成了鸡肋。而当它们真的派上用场的时候,早已忘记了它们正静静地躺在目录下等待召唤。或许,我需要一个可以自动阅读我的聊天内容并向我推荐表情图的AI助理?

等哪天有空了,说不定我会按上面的思路稍微改进一下吧,哈哈。