本文通过在 iTerm2 中使用我个人编写的 oh-my-zsh 主题,展示一种使命令行界面更加精致美观的方法,但是这个方法只能在 macOS 上使用。因为该方法依赖的工具栈是 iTerm2 + zsh + oh-my-zsh ,其中的 iTerm2 至关重要,而 iTerm2 只有 macOS 的版本。因此,本文主要呈献给 macOS 用户与决定将成为 macOS 用户的读者。

在这个自定义主题中,我替换了 af-magic 主题中的分割线。

如下为 af-magic 主题原本的样式:

经过我的改写后,呈现的样式为:

可见,原来使用连续 ‘-’ 表示的分割线被我改成了使用图片表示的精致分隔栏。那么这个是怎么做到的呢?这得从 iTerm2 的图片显示功能说起。

iTerm2 的图片显示功能

iTerm2 能够在终端中显示图片,它提供了多个与之相关的、功能强大的脚本,但它们所依赖的工具不过是 base64 这个简单的工具。并且从 3.2.0 版本开始,它支持 Retina 屏幕,造福了广大 macOS 用户。

如下是 iTerm2 显示图片的 xterm 协议代码:

1
ESC ] 1337 ; File = [optional arguments] : base-64 encoded file contents ^G

为了便于阅读,其中的元素之间都用空格分隔,在实际编写中是很紧凑的。

其中 optional arguments 由单个或多个 key=value 组成,键值对之间使用封号分隔。以下列出了几个常用的键:

描述
width 图片显示的宽度
height 图片显示的高度
preserveAspectRatio 是否为了适应显示的宽度与高度进行拉伸
inline 是否在行内显示

代码的 ’:' 之后的部分是图片进行 base-64 编码的结果。一般直接通过一下命令获得图片的编码结果:

1
base64 < ./path/to/aim_picture.jpg

这行代码调用 base64 命令,并重定向它的标准输入流为 ./path/to/aim_picture.jpg ,即目标图片。

下图是 iTerm2 官网给出的显示图片的示例,其中的 imgcat 命令是基于 base64 的脚本:

iTerm2 能显示图片,但是要在 oh-my-zsh 的主题中加入图片,我还需要了解这些主题的构成。接下来以 af-magic 主题为例说说这方面的知识。

af-magic 主题的基本要素

af-magic 是一个 oh-my-zsh 自带的主题。如下图所示:

它主要由三部分组成,

  • 当前位置的路径与输入提示符
  • 位于当前位置的路径之上的有限长度分隔线
  • 与当前位置的路径处于同一行的,位于该行最右侧的用户名和主机名 (上图未显示)

该主题简单朴素、清晰简洁,在美观与实用性上达到了一个很棒的平衡。同它的外在表现类似,它的源码也无比简单清晰,以下是它的关键部分的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
...

# primary prompt
PROMPT='$FG[237]------------------------------------------------------------%{$reset_color%}
$FG[032]%~\
$(git_prompt_info) \
$FG[105]%(!.#.»)%{$reset_color%} '


...

# right prompt
if type "virtualenv_prompt_info" > /dev/null
then
RPROMPT='$(virtualenv_prompt_info)$my_gray%n@%m%{$reset_color%}%'
else
RPROMPT='$my_gray%n@%m%{$reset_color%}%'
fi

...

如上代码可知, oh-my-zsh 的主题是由 shell script 编写的,没有什么复杂的操作。难点主要在于:

  1. 如何在终端内显示特定颜色的文字;
  2. 大写变量的作用;
  3. 诸如 %n 之类的格式符的作用。

在此处, %n 表示用户名, %m 表示主机名。上面部分的代码块给 PROMPT 变量赋值,该变量的值用于显示命令行左边部分的内容,即分隔线、当前位置的路径和输入提示符。下面一部分给 RPROMPT 变量赋值,该变量的值用于显示命令行右边部分的内容,在该主题中是灰色的 “用户名@主机名” 。 具体说明可以参考 zsh 开发手册

我要修改由‘-’组成的分隔栏,就需要改写 PROMPT 变量的值。

在 oh-my-zsh 主题中添加图片

弄清楚了 iTerm2 如何显示图片af-magic 主题的源码是怎样 这两个问题,之后的工作就简单了。

首先来看一段 iTerm2 官方提供的最简单的显示图片的示例代码段:

1
2
3
4
printf '\033]1337;File=inline=1;width=100%%;height=1;preserveAspectRatio=0'
printf ":"
base64 < "$1"
printf '\a\n'

这段代码在第一行先准备好打印的相关参数,第二行打印冒号,而第三行重定向 base64 命令的标准输入流,将脚本的第一个参数表示的文件作为它的标准输入流,其运行结果就 base64 编码的文件内容。不过这里有一个理所应当限制, $1 表示的文件是图片类型的。

我需要做的事情很简单,只不过是将上面的示例代码改写,并赋值给 ag-magic 主题源码中的 PROMPT 变量,替换那些连续的 '-' 。

所以,很自然地写下如下所示的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...

DIVIDER='$FG[237]------------------------------------------------------------%{$reset_color%}'

if [ -f "$MAGIC_DIVIDER" ]; then
DIVIDER="\033]1337;File=inline=1;height=1;preserveAspectRatio=0:$(base64 < $MAGIC_DIVIDER)\a\n"
fi

# primary prompt
PROMPT='%{$fg[blue]%}%~\
$(git_prompt_info) \
$FG[105]%(!.#.»)%{$reset_color%} '

PROMPT="$DIVIDER
$PROMPT"


...

该段代码先给 DIVIDER 赋上初始值,初始值是原版的分割线,然后判断环境变量 MAGIC_DIVIDER 的值是否表示一个文件的地址,如果是,重新给 DIVIDER 复制值,此次为从 iTerm2 官方示例中改写成的图片代码,最后在将 DIVIDER 的值拼接到 PROMPT 中。我将如下图片的地址赋值给 MAGIC_DIVIDER ,便以为大功告成了:

然而结果与期望的相差很大,没有精致的分隔栏,只有一大堆看不懂的 base64 编码的数据。

这里的问题在于, zsh 的 PROMPT 并不是如 iTerm2 官网中示例一样通过 printf 指令打印的, iTerm2 无法处理它的结果,将它转换成图片。我的解决方案是,将 printf 的打印结果赋值给 DIVIDER 变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
...

MAGIC_IMAGE="\033]1337;File=inline=1;height=1;preserveAspectRatio=0:$(base64 < $MAGIC_DIVIDER)\a\n"
DIVIDER='$(printf $MAGIC_IMAGE)'

# primary prompt
PROMPT='%{$fg[blue]%}%~\
$(git_prompt_info) \
$FG[105]%(!.#.»)%{$reset_color%} '

PROMPT="$DIVIDER
$PROMPT"


...

在此代码中,重新给 DIVIDER 变量赋予的值已经是经 iTerm2 处理过的结果了,再将它拼接到 PROMPT 变量中,就完美解决了这个问题。结果如下图所示:

总结

当我完成对我的命令行以更精致为目的的改造后,第一时间分享给我的好友把玩,他觉得太过花哨,但是对如何实现这一方面很感兴趣。因此,我向他讲解完其中的原理后,也抽出时间来写下了这篇 blog ,同时将我那微不足道的命令行主题分享给大家。

我回顾整个短暂的编写过程后觉得,在主题中显示图片后呈现的样式是否美观其实并不重要。美观,牵涉了太多东西,命令行各个部分的配色、使用的字体和字体的样式、图片尺寸与内容等,让一个软件工程师去协调好林林总总的美术要素本身就不现实,更不用说达到登堂入室的水准了。在这个过程中,我将平时 收集 的各种小知识 整理提炼 ,然后有能力去 创造 —— 这三个步骤才是值得留意的。

最后,我分享一下自己的 oh-my-zsh 主题的源码: mephis-magic