清除最近打开文件列表的Alfred 2 Workflow

之前装了一个markdown的软件,发现不顺手就删掉了,然后markdown文件就被Xcode关联了(不知是之前就这样还是默认如此)。
于是一不小心Xcode的最近打开列表里多了一些无关紧要的md文件,然后发现不能单独删除,只能一次性全部清空,着实让我不爽。

Google一下立刻就查到了PlistBuddy这个工具(不知是系统自带还是装Xcode带的)。但是用起来却不怎么方便,
毕竟是面向普通plist文件的工具,所以需要知道plist文件里的项目名字啊什么的,每次都要记住一坨字符串然后敲到命令行有点麻烦。
一般想要删除某个最近打开项目时都是在启动某个App之后,遂想到常用的Alfred,可以写一个简单的workflow来搞定这些。

开始以为比较简单的小东西却花了一天的时间。虽然根本原因是我半吊子的python水平,不过还是觉得有些东西要记一下。

语言选择

因为之前也写过一个workflow,所以知道输出到Alfred的一些基本知识,知道需要自己把xml格式写好。
但那次是用的bash脚本,而且只输出一个条目,所以就随便糊弄了一下。
但这次需要列出的项目不少,一个一个的拼xml格式有点麻烦了,而且中间一旦出现引号尖括号的转义就更有点自找无趣的意思了。

通过参考别人的workflow的,发现用其他脚本做貌似比较省事。想到自己之前对python有点小了解,感觉是个练习的机会,于是就决定用python实现这次的功能。

实现方法

功能上比较简单,OS X下所有启用了最近打开功能的App都会在~/Library/Preferences下保存一个company_name.application_name.LSSharedFileList.plist文件,所以只需取出这个文件夹下所有带LSSharedFileList的文件,就可以让用户选择想要清除的App。

然后在选择App后需要列出所保存的最近打开列表。这里需要用到在/usr/libexec下面的PlistBuddy工具。它可以对plist文件进行操作,无需打开Xcode。列出plist里某一项是这么做的:

/usr/libexec/PlistBuddy -c "Print ItemName" plist_file_name

最后一步即选择要删除的项目了。这里同样用PlistBuddy,只是传入的命令不是Print,而是Delete:

/usr/libexec/PlistBuddy -c "Delete ItemName" plist_file_name

遇到的问题

思路很简单,但中间遇到不少问题。

1.输出内容到Alfred

这是首要解决的问题。因为没有任何的输出有问题了很难判断问题点在哪儿。

了解到有个alfred模块类,可以生成到Alfred的输出。引入进来后用起来确实很简便:需要输出时就Item对象,最后调用其write方法即可。

但是到最后了发现一个问题,就是要输出的条目内容如果不是纯英文的就会报错:

UnicodeDecodeError: 'ascii' codec can't decode byte 0xe5 in position 45: ordinal not in range(128)

再度求助Google后才知道python里经常被吐槽的编码坑,编码是需要自己指定的。最后在输出时用unicode函数做一次编码解决:unicode(title, 'utf-8')

2.执行外部命令

也是Google了一下知道subprocess这个模块,可以通过另起进程的方式执行别的命令,然后获取返回结果。在terminal里试了试,感觉可用。但是在Alfred里尝试时却怎么也出不来结果,然后Alfred自带的debug模式也不知道为什么什么反馈也没有。在这里花了一些时间,后来发现对subprocess的用法有问题。在创建Popen时,传入的命令需要用一个list,而不是只写到一个字符串就行。比如想执行ls -A /Library/Preferences命令,直接传入’ls -A /Library/Preferences’是不行的,需要这么写:
p = subprocess.Popen(['ls', '-A, '/Library/Preferences'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)

注意上面的例子显示的不是用户目录下的Library而是根目录下的,是因为在尝试时发现用户目录的’~’是需要自行展开的,如果想用subprocess
执行ls -A ~/Library/Preferences,需要先用os.path.expanduser('~')把’~’展开后再把’/Library/Preferences’连上去。

另外stderr这个不能省略,不然也取不到结果。不明白为什么。

3.模糊匹配

Alfred的workflow流程是单向的,即:trigger -> action -> output,
没有可以和用户交互的方式,比如想在trigger到自己的workflow,让用户从列表里选择一个项目后不执行action,或者执行action后再trigger一遍自己是不行的。
因为这个限制,在用户选择App时不能通过选择列出来的项目的方式,而是需要用户输入App的名称,然后再选择需要删除的item。

我想到的比较自然的方式是用模糊匹配的方式,动态过滤列表,直到最后只剩一个项目。然而我python用的不熟,不知道好的过滤方法,Google了一下也无果,但是知道了difflib的SequenceMatcher。通过这个东西可以知道两个字符串比较的一个ratio。所以最后的做法是动态改变列表的顺序,把最匹配的项目放到第一个。

成果

基本大的问题就是上面的3个,最后完成的就是这么一个Workflow。也算是第一次用python做了个小东西。还是挺有收获的。

看Alfred还支持ruby,也许后面会考虑把这个小东西用ruby改写试试:)