Espanso 的一些用例

Espanso 是一个花哨玩意儿,我尝试了数次研究出来一些有趣的玩法,记录一下。

区分是键入调用还是 search bar 调用

Espanso 的命令(其实叫 match,我习惯叫命令吧就)有两种调用方式——直接从键盘输入整个命令,或者通过 search bar,去查询和执行命令。

但 Espanso 没有提供任何方式来区分一个命令究竟是从键盘输入还是通过 search bar 调用。这可能是一个伪需求,但我就是想要试试。

解决方案利用 Espanso 的一个特性——命令内部的变量能够覆盖同名全局变量。正则表达式会引入变量,而如果通过 search bar 执行正则表达式命令,这个变量则不会被赋值。因此,我们这么干就行了——

1
2
3
4
5
6
7
8
9
global_vars:
- name: '_'
type: echo
params:
echo: 'TRUE'

matches:
- regex: '(?P<_>;)hello'
replace: '{{_}}' # _ 是 'TRUE' 则是 search bar 调用,否则是直接键入调用

使用键盘输入时,_会被赋值为;,使用 search bar 执行时,_没有被赋值,espanso 就去全局变量里找了。

这还是在 search bar 中执行正则命令出错时意识到这个解决方法的。也尝试过能不能局部变量里也定义一个_,但发现行不通,会报一个莫名其妙的循环依赖,espanso 自己还崩了,笑死。

Windows 下执行 shell 脚本

shell 脚本在 win 下默认走的是 Powershell,而且不支持配置成 git bash。

但并非不能——espanso 自己不用 git bash,那我自己用。

对官网的 curl 示例:

1
2
3
4
5
6
7
- trigger: ";ip"
replace: "{{output}}"
vars:
- name: output
type: shell
params:
cmd: "curl 'https://api.ipify.org'"

对应的改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- trigger: ";ip"
replace: "{{output}}"
vars:
- name: output
type: script
params:
args:
# 旧的解决方案——给定 bash 全路径
# - C:\Program Files\Git\usr\bin\bash.exe
- env
- bash
- -c
- | # 还能执行多行脚本
printf 'my ip: '
# 我不知道为啥 curl 需要加 -s 才能执行……不然会报莫名其妙的错
curl -s 'https://api.ipify.org'

这里必须要给定 bash 的完整路径,因为:

  1. espanso 执行 script 时,工作目录在C:\Windows\System32
  2. System32下有一个不能用的 bash,疑似是 WSL 用的
  3. 命令只写bash的时候,因为工作目录的缘故,会优先访问System32下面的 bash,即使 PATH 设置的时候让 git 的 bash 优先仍旧如此

然而,有两个解决方案:

  1. 使用sh,win 没有同名命令,但 sh 执行特定既有的脚本可能有问题
  2. (明显这个才是最优解)使用env bash,win 没有 env 这个命令,大概吧

屏蔽变量的内容(只执行命令,不返回它的结果)

espanso 的变量是 lazy 的——不被引用就不执行。有时候我们想要执行一个命令,但不想要它的标准输出流被拷出来,这就很麻烦。

但变量之间是可以互相依赖的,一般来说 espanso 会根据实际的依赖关系建立依赖树去按顺序执行变量,但是可以使用depends_on去主动指明依赖,即使没有使用它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- trigger: ';some-test'
replace: '{{var2}}'
vars:
- name: var1_hide
type: script
params:
args:
- python
- D:\some\script.py # 做一些带副作用的,可能操作标准输出流的操作

- name: var2
depends_on:
- var1_hide
type: echo
params:
echo: hello

关于退格键

espanso 执行命令后,会使用剪切板或模拟输入去将命令的结果输出出来。不想要输出?使用上面的方法,然后变量的值设为空即可。

而在这个步骤之前,espanso 会模拟输入一定数量的退格键,先删除输入的命令的内容……这就出问题了。比如我用 espanso 当软件快捷键用,这时候的一系列退格键可能会造成非预期的问题

我想到的最合适的方法就是直接使用 espanso 的 search bar 去搜索和执行命令,使用这个方法似乎是不会输入退格键的……再不然就是让在 espanso 输入退格键时保证应用窗口是失焦的,不知道有啥方法更优雅地做这个。

把 Espanso 当作 utools 用

仔细看看 Espanso,我们能发现什么?

  1. Alt+Space 唤起一个 Search Bar,能匹配和选择命令去执行
  2. 原生提供 Form,提供输入栏和选择框(下拉式或列表式),其中输入栏可以给定默认值,选择框可以使用脚本给定所有选项
  3. Form 可以和 Script 协同使用,Form 的值可以喂给 Script,反之亦然
  4. Script 的执行结果可以贴到当前输入框中

这就造成了一种非常轻量的类似 utools 的工具——我调用命令,我通过表单填写参数,我通过当前输入框获取命令的执行结果,但最重要的是,定义 Espanso 的一个新命令是非常容易的,不像 utools 或类似的工具,插件上来就是什么生命周期,还可能要写前端,麻烦不死你。

简单实现一个 base64 和文本互转的命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
- trigger: ';b64'
replace: '{{result}}'
vars:
- name: form
type: form
params:
layout: |
文本和 Base64 互相转换,只填一个
文本转 base64: [[original_text]]
base64 转文本:[[encoded_text]]
- name: result
type: script
params:
args:
- python
- -c
- |
import sys
import base64
def text_to_base64(text, encoding='utf-8'):
"""文本转 Base64"""
text_bytes = text.encode(encoding)
base64_bytes = base64.b64encode(text_bytes)
return base64_bytes.decode('ascii')
def base64_to_text(base64_string, encoding='utf-8'):
"""Base64 转文本"""
try:
base64_bytes = base64_string.encode('ascii')
text_bytes = base64.b64decode(base64_bytes)
return text_bytes.decode(encoding)
except Exception as e:
return f"Base64 解码失败:{e}"
if sys.argv[1]: # 文本转 base64
print(text_to_base64(sys.argv[1]))
elif sys.argv[2]:
print(base64_to_text(sys.argv[2]))
else:
pass # 毛都没输入,就什么也不输出
- '{{form.original_text}}'
- '{{form.encoded_text}}'

动态图片命令

某个 Match 使用image_path而非replace时,espanso 对它的处理方式有异——不执行任何变量,直接以image_path原始内容作为图像路径。这事儿文档里也不写。

相关 issue 中,官方的解决方案是使用 html 或 markdown 富文本来替代嵌套图片的需求,这个算是方便,但在许多时候不可用——富文本类型的剪贴板我试了手头的应用,只有 word 支持(需要使用 html 而非 markdown),qq,vscode 等都不主持,会忽略掉图片。

尝试了一下,找到一个不那么完美的解决方案,仅适用于 windows 且需要利用 nircmd:

  1. 通过某种手段获取图像路径
  2. 使用nircmd clipboard将图像路径保存到剪切板
  3. 使用nircmd sendkeypress ctrl+v模拟输入粘贴(这一步也可以省略,让用户自己去粘贴)

下面是一个文本生成二维码并粘贴的完整示例(文本使用表单输入以兼容中文问题):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
- trigger: ';qrcode'
label: 文本生成二维码
replace: '{{clipboard}}' # 这里引用 clipboard 只是为了让这个变量执行,没有其他意思
vars:
- name: form
type: form
params:
layout: |
文本内容:[[text]]
- name: image_path
type: script
params:
args:
- python
- -c
- |
import sys
import qrcode
import tempfile
import os
from pathlib import Path
def generate_qrcode_to_tempfile(data: str, file_format: str = 'png') -> str:
qr = qrcode.QRCode(
version=1, # 控制二维码大小 (1-40)
error_correction=qrcode.constants.ERROR_CORRECT_L, # 容错率
box_size=10, # 每个小格子的像素大小
border=4, # 边框大小
)
qr.add_data(data)
qr.make(fit=True)
with tempfile.NamedTemporaryFile(
suffix=f'.{file_format}',
prefix='qrcode_',
delete=False, # 不自动删除,以便返回路径
dir=tempfile.gettempdir()
) as tmp_file:
temp_path = tmp_file.name
try:
img = qr.make_image(fill_color="black", back_color="white")
img.save(temp_path, format=file_format.upper())
return temp_path
except Exception as e:
# 如果出错,清理临时文件
if os.path.exists(temp_path):
os.unlink(temp_path)
raise RuntimeError(f"生成二维码失败:{str(e)}")
sys.argv[1] and print(generate_qrcode_to_tempfile(sys.argv[1]))
- '{{form.text}}'
- name: write_clipboard
type: script
params:
args:
- nircmd
- clipboard
- copyimage
- '{{image_path}}'
- name: clipboard
type: script
depends_on:
- write_clipboard
params:
args:
- nircmd
- sendkeypress
- ctrl+v

本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 协议 ,转载请注明出处!