app launcher的模型很简单,基本就是一个输入框加上一个列表。使用比较多的app launcher,像是rofi,虽然灵活快速,但是难以定制。受到这篇帖子鼓舞:using_fzf_as_a_dmenurofi_replacement,决定用fzf代替rofi实现一个app launcher,同时支持一些基本的操作。
fzf是一个模糊搜索工具,但是只支持终端,因此需要搭配像是alacritty这样的终端来使用。界面很美观,而且支持定制,同时支持bind操作调用脚本和获取用户输入,因此具备了一切app launcher需要的元素。
# 界面
GUI使用alacritty提供。alacritty支持通过--class
参数设置窗口类型,因此搭配i3可以获得我们想要的窗口样式,比如:
for_window [class="fzfmenu"] floating enable, resize set height 400, resize set width 1200, move position center, focus
以上操作分别是:对于类型为fzfmenu的窗口,启用浮动功能(也就是类堆叠),将尺寸调整为1200x400
,居中,聚焦。
fzf同样可以进行主题定制,添加以下环境变量以启用catppuccin
主题:
set -Ux FZF_DEFAULT_OPTS "\
--color=bg+:#313244,bg:#1e1e2e,spinner:#f5e0dc,hl:#f38ba8 \
--color=fg:#cdd6f4,header:#f38ba8,info:#cba6f7,pointer:#f5e0dc \
--color=marker:#b4befe,fg+:#cdd6f4,prompt:#cba6f7,hl+:#f38ba8 \
--color=selected-bg:#45475a \
--multi"
# 实现逻辑
首先是最重要的命令:
path = os.path.realpath(__file__)
subprocess.call(
[
"fish",
"-c",
f"alacritty \
--class fzfmenu \
-e fish -c \
\"fzf \
--bind 'start,change:reload:python {path} picker {{q}}' \
--bind 'enter:become(nohup python {path} run {{}} > /dev/null 2>&1 &)'\" \
",
]
)
在这里,我们规定了alacritty的窗口类型,以在i3中设置它的窗口样式。
--bind 'start,change:reload:python {path} picker {{q}}'
设置了在fzf启动和输入发生改变时重新运行该命令。其中{path}
是该脚本的路径,此处可忽略。该脚本的标准输出即为fzf上显示的选项。
--bind 'enter:become(nohup python {path} run {{}} > /dev/null 2>&1 &)'
设置了在选择项目后运行的命令。由于脚本运行结束后alacritty终端窗口自动关闭,因此需要使用nohup,并将标准输出重定向到/devnull
当中。此处的{}
即为选中项目的字符串结果,将其传入脚本中以运行。
由于这只是个简单的脚本,不想分成太多的文件,因此按照如下方式做判断,从而进行递归调用:
def main():
argv = sys.argv
if len(argv) == 1:
call_fzf()
else:
if argv[1] == "picker":
run_plugins_picker(" ".join(argv[2:]))
elif argv[1] == "run":
run_plugins(" ".join(argv[2:]))
在run_plugins_picker
函数中,根据输入的前缀判断该启用哪个插件:
def run_plugins_picker(input: str):
if input.startswith("wd "):
window_jump_picker(input)
elif input.startswith("kl "):
killer_picker(input)
else:
open_applicaton_picker(input)
由于不想要多个插件同时启用从而影响性能和干扰选择,因此只允许同一时间启用一个插件。
然后对于每个插件内部,使用两个函数定义插件逻辑:
def killer_picker(input: str):
input = input.lstrip("kl ")
if len(input) == 0:
return
output = (
subprocess.check_output(["bash", "-c", f"pgrep -fa {input}"]).strip().decode()
)
path = os.path.realpath(__file__)
for line in output.splitlines():
if path in line:
continue
print("kl " + line)
def killer_runner(output: str):
pid = output.lstrip("kl ").split(" ")[0]
subprocess.call(["bash", "-c", f"kill -9 {pid}"])
上面的插件可以根据输入列举出当前存在的进程,并且可以根据用户选择杀掉它。很简单但很实用的一个模式。在picker
中,我们调用命令,并将存在的选项打印到标准输出中。
为什么要在输出前面加一个kl
?这就是fzf不灵活的一个地方,因为fzf只能根据传入的所有关键词进行模糊搜索,因此输入字符一定要包含在我们想要的结果中,否则无法被列出。
当用户选择后,递归调用了run_plugins
函数,根据前缀选择执行killer_runner
。此处传输的output即为选择的内容。在picker
中,我们打印了前缀+pgrep -fa xxx
输出的内容,因此我们的output的内容格式如下:
kl 7755 alacritty
其中kl
是插件前缀,7755
是进程pid,alacritty
是进程名称。
因此对于创建一个插件,有以下步骤:
- 新建
plugin_name_picker(input: str)
函数,列举所有表单选项; - 新建
plugin_name_runner(output: str)
函数,确定具体执行逻辑; - 将上面两个函数分别补充到
run_plugins_picker
以及run_plugins
函数中。
后续如果需要改进,大概就是可以将插件抽象出来,分散到独立文件中,并且在主函数中实现插件的读取了。但就当前来说,目前的代码已经足够实现我的需求了。