一个更加现代化的基于终端的开发环境

490 阅读12分钟

自从我上次谈论我的开发环境以来,已经过去一年了。许多事情都没有改变:我使用neovim、zsh和tmux。然而,我做了一些改变,以提高可用性,使我的日常工作感觉更快。我正在使用一个新的终端程序,我的vim环境发生了巨大的变化(至少是在幕后),我开始使用一个相当整洁的基于键盘的窗口管理器。

这篇文章是上一篇文章的延续。我不会从头开始,而是讨论我在过去一年中对我的环境所做的一些有趣的改变:我开始使用的新工具,以及提高我工作效率的配置改变。

本文中讨论的所有配置文件和脚本都可以在我的dotfiles repo中找到,所以你可以随时在那里寻找更多的信息。此外,如果你有问题或建议,请随时打开问题!

终端升级

去年我使用苹果的Terminal.app,这是一个很好的终端客户端,但它确实有几个缺点。首先(也是让我开始关注其他终端的原因),它不支持真彩色。我的意思是,你可以把主题的调色板设置成你喜欢的任何颜色,但程序被限制在256种颜色。我使用的一些vim扩展程序确实受益于更多的颜色灵活性,所以Terminal.app开始感觉到了限制性。

我偶尔会遇到的其他问题是重绘时间和按键延迟。Terminal.app的速度相当快,但我在全屏的tmux会话中还是会偶尔看到闪烁。虽然几毫秒的额外按键延迟可能听起来不多,但当你每天90%的时间都在使用基于文本的应用程序和与外壳互动时,它就会变得很明显。

在过去的几年里,速度已经成为终端客户端的一个焦点。Alacritty自称是 "目前最快的终端模拟器",使用OpenGL进行硬件加速的渲染。iTerm2可能是最受欢迎的macOS第三方终端客户端,它在2018年8月发布了自己的硬件加速渲染引擎,使用苹果的Metal框架,版本为3.2。

我同时使用过Alacritty和iTerm2,虽然两者都相当不错,但过去几个月我一直在使用kitty。它也使用OpenGL进行渲染,所以即使是大窗口也感觉非常扎实。在功能方面,Kitty位于Alacritty和iTerm之间。另一方面,iTerm的功能非常丰富,它的内置功能可以处理tmux所提供的那种窗口分割,并且有各种菜单选项。Kitty的用户界面非常简约,但它有相当多的内置功能:它有一个定义明确的远程控制API,它支持扩展,窗口可以像tmux那样被分割(尽管我仍然更喜欢tmux),而且它提供非常精细的字体控制。根据我的经验,它也感觉比iTerm和Alacritty都快一些。

我非常喜欢的一个功能是kitty的远程控制协议。基本上,它可以让你从命令行或Python脚本中控制kitty窗口。你可以改变一个窗口的布局,设置颜色,更新标题,甚至发送任意的文本到一个外壳。我用它来设置我所有终端窗口的颜色。这比我去年使用Terminal.app的方法要灵活得多。要更新所有打开的Terminal.app窗口,你必须告诉Terminal.app使用一个预定义的主题,这意味着你必须在某个时候定义并安装该主题。使用iTerm,你可以使用动态配置文件来做类似的事情,但在配置文件更新和终端实际反映变化之间可能会有几秒钟的滞后。另一方面,通过kitty,我可以将任意的新颜色作为Python脚本中的字符串值发送到所有打开的kitty窗口,并立即看到结果。

using kitty's remote control protocol to update the terminal theme in multiple windows

kitty实际上是以猫为主题的,这也无伤大雅。它的扩展被称为 "小猫"。你怎么能不想要使用它呢?

kitty terminal client logo

管理虚拟环境

虽然我的shell没有改变(仍然是zsh),但我确实对我管理虚拟环境的方式做了一些升级,特别是围绕着它们如何被我的提示符所指示。

asdf

asdf是一个试图统一所有开发人员用来管理构建工具的*env和*vm程序的命令。每个开发工具似乎都有一个(或四个)。Node有nvmnodenvnodeenvn;Ruby有rvmchrubyururbenv;Python有pyenv;等等。它们中的每一个都做同样的工作,但它们的工作方式通常只是有点不同。

asdf试图使这些工具标准化。你安装一个基础工具,asdf,然后安装插件来管理特定的工具。插件类似于其他环境管理工具,但所有asdf插件都使用选项和子命令。

$ asdf plugin install nodejs
$ asdf nodejs install 10.16.3
$ asdf local nodejs 10.16.3
$ asdf plugin install python
$ asdf python install 3.7.7
$ asdf local python 3.7.7

运行asdf plugin list all ,会列出所有可用的插件;在写这篇文章的时候,有185个插件。这些包括除了语言解释器之外的一些命令行工具的插件,如ripgrep和sqlite

默认情况下,asdf的工作方式与其他大多数版本管理器一样。你在你的shell配置文件中添加一个脚本,然后在项目目录中创建一个.tool-versions 文件,告诉asdf它应该激活什么版本的工具。当你在该目录或子目录中时,asdf会激活指定的工具。asdf也可以与大多数其他版本管理器的配置文件一起工作,如nodenv的.node-version 文件。

direnv

direnv是各种nix shells(bash、zsh、tcsh......)的一个扩展,让你在每个目录上配置你的环境。例如,当我进入我目前正在进行的一个基于Node的项目(项目根目录或一个子目录)时,direnv会:

  • 在我的shell环境中添加一些云测试服务API密钥
  • 切换到本地安装的Node.js v10
  • 启用我本地托管的npm注册表(用于发布和安装测试版本的软件包)

当我离开这个目录时,direnv将我的shell环境恢复到之前的状态。

Direnv的工作方式是在bash shell中执行一个.envrc 文件,捕获输出,然后将其应用到当前的shell中。"在bash shell中执行 "的部分提供了很大的灵活性。例如,在Python项目中,我经常会添加一个.envrc 文件,比如:

use tools python@3.7.5
use pyenv

第一行是一个自定义命令(在我的direnvrc中定义),告诉direnv我想通过asdf使用Python 3.7.5。如果asdf还没有本地安装的Python 3.7.5,它将得到一个。

第二行也是一个自定义命令。它告诉direnv,我想要一个当前目录下的本地Python环境(基本上,我想要Node.js + npm的Python体验)。

当我启动一个Python项目时,我只需将这两行.envrc 文件放到项目根目录下,等待几秒钟,我的Python环境就可以开始使用了!

你可以自行安装和使用 direnv,比如在 Mac 上使用 homebrew。你也可以把它安装成asdf插件,这将使asdf和direnv之间的集成更加紧密:

$ asdf plugin install direnv

与asdf一样,direnv通过你的shell init文件(.zshrc )中的一个命令来钩住你的shell。然而,你不需要同时将asdf和direnv添加到你的shell init中。实际上,你可以用direnv来管理asdf,这有点快(direnv是一个二进制文件,所以它的设置时间比asdf的快一点),而且它还可以使你不必为两个不同的工具重复设置。

Powerlevel10k

当我写这篇文章的上一版本时,我正在使用purezsh提示主题的修改版。Pure是相当不错的,但它相当简约。当我开始更多地使用虚拟环境时,我希望我的提示符能显示更多关于我当前环境的信息。pure可以做到这一点,但它要求我在我的提示符设置文件中保留相当多的代码。

然后我发现了Powerlevel10k(p10k)。它和它的名字一样棒。这个主题功能丰富,允许我轻松地为我的环境中所有有趣的部分添加标签:

An Even More Modern Terminal-Based Development Environment

提示符上面的一行显示:

  • 当前的git分支
  • direnv在当前目录中是否处于活动状态(▼)。
  • 我是否在使用本地的npm注册表
  • 当前Node.js的版本(如果不是系统默认的)。
  • 我正在处理的npm包的当前版本

这些大部分都是p10k的内置功能,而不是的那个(npm注册表)只需要几行容易阅读的zsh脚本。最重要的是,即使启用了所有这些功能,提示符仍然能立即显示出来。

这种速度部分是由于p10k对性能的内在关注。另一部分是由于我如何让asdf、direnv和p10k一起工作。默认情况下,显示工具信息的提示往往会在每次呈现提示时调用该工具,以获得该工具的当前版本。例如,一个使用nvm显示Node.js版本的提示会在每次呈现提示时调用nvm版本。对于一个工具来说,这不是什么大问题,但如果有三或四个不同的东西,你的提示就会开始花几秒钟来渲染。

我没有用asdf与.tool-version 文件,而是用direnv通过环境变量来管理asdf。p10k中的asdf支持可以与asdf的环境变量以及版本文件一起工作。我的提示符不必在渲染时调用一堆不同的版本管理器--它可以只看环境变量--使其立即渲染。

改进复制和粘贴

tmux和vim的视觉模式提供了相当好的复制和粘贴体验,但绝对有一些改进的空间。我一直遇到的两个问题是:不得不在tmux窗格的一半位置选择文本,以及不能轻易地从远程会话中复制文本并粘贴到本地。

手指

几个月前,我的一个同事给我指出了一个整洁的tmux插件,它可以使复制文本到剪贴板的速度更快,即tmux-手指。一旦安装了这个插件,你可以用<前缀>F来激活它。该插件将使显示不饱和,并突出显示符合其内置正则表达式的字符串:文件路径、SHA哈希值、IP地址和其他一些有趣的模式。你还可以添加自定义模式。每个突出显示的字符串在开头都有一个或多个提示字符。输入提示字符会将字符串复制到剪贴板上。这比在tmux中启用复制模式,并手动选择文本进行复制要快得多。

using tmux-fingers to copy text

远程复制

在过去的一年里,我改进的一个相关问题是在如何将数据复制到剪贴板上。一段时间以来,我一直使用pbcopy和pbpaste在系统剪贴板和tmux之间复制数据。这在Mac上很好,但在Linux上却不工作。它在系统之间也不起作用。例如,如果我在一个远程系统上ssh'ed到一个tmux会话,在那个系统上用tmux复制文本会把它复制到那个系统的剪贴板。这通常不是我想做的事。

经过在互联网上的搜索,我了解到OSC 52,这是一个终端转义代码,可以访问本地系统的剪贴板,前提是终端支持OSC 52。幸运的是,小猫咪支持有很多关于如何使用OSC 52来复制文本的例子;我最后用这个例子作为我自己的term_copy脚本的基础。该脚本使用转义字符通过终端复制文本,同时也调用pbcopy或xclip来复制到主机剪贴板。我在tmux和vim中都使用这个脚本来复制文本到本地剪贴板,无论我是在本地工作还是通过ssh进入一个远程系统。

我的vimrc 中的这个片段将 y映射到term_copy 脚本中:

function! Yank(text) abort
let escape = system("term_copy", a:text)
if v:shell_error
echoerr escape
else
call writefile([escape], '/dev/tty', 'b')
endif
endfunction

# Selecting text and hitting ;y copies text to the local clipboard
noremap <silent> <Leader>y y:<C-U>call Yank(@0)<CR>

当使用tmux的复制模式和用tmux-fingers复制时,我的.tmux.conf 的这部分内容使用term_copy

# The standard 'yank' will copy with OSC 52
bind -T copy-mode-vi 'y' send -X copy-pipe-and-cancel 'term_copy < #{pane_tty}'

# Getting tmux-fingers to use term_copy is easy, too
set -g @fingers-main-action 'term_copy < #{pane_tty}'

这种设置意味着,无论我是在本地还是远程会话中,在shell中还是在vim中,我都可以选择文本并将其复制到本地剪贴板上,然后将其粘贴到我需要的地方。

NeoV(SCode)im

在过去的一年里,我的环境发生的最重要的变化可能是我转向了coc.nvim及其扩展的生态系统。coc的目标是把VS Code的力量带到(Neo)Vim,它主要通过移植VS Code的扩展来实现。Coc主要是用JavaScript编写的,在Node.js中运行,所以coc的扩展实际上可以是VS Code扩展的移植,而不是完全重写。这也意味着扩展是快速的,通常比纯粹的vim脚本等价物要快得多。

Coc在整合语言服务器方面真正大放异彩,语言服务器是实现语言服务器协议的服务器,如TypeScript的tserver或微软的Python语言服务器。这些给了coc一些特定的语言功能,如自动完成、悬停文档、列出和跳转到引用和定义的能力、错误检测、自动导入等等。

auto-importing a module in a TypeScript file using coc

不过,Coc不仅仅是语言服务器。vim有许多独立的LSP插件,neovim也有一个新的本地LSP实现。然而,LSP插件并不提供像自动完成弹出式的支持功能,也不支持像Prettier这样的外部工具。Coc提供了一个工具的生态系统,这些工具都能很好地协同工作,而且你不必单独管理它们。安装coc,使用:CocInstall 来安装插件,coc将从那里管理事情。

我已经把我的许多以IDE为中心的插件替换成了coc的等价物。YouCompleteMe和ALE用coc本身,typescript-tslint-plugin用coc-eslint,NERDtree用coc-explorer,vim-rettier用coc-rettier。我曾考虑过用coc的内置:CocList 命令来代替fzf.vim,但我还没有转换;它很好,但没有fzf那么快。

奖励:键盘控制的窗口

第三方窗口管理器是我的几个同事使用的工具,直到最近我才花了很多时间使用。我大部分时间都是在全屏模式下运行应用程序,但这样做有很多怪癖(特别是在多个显示器上)和限制(在macOS中你只能分割一次显示器),所以我决定尝试新的东西。

我看了几个不同的应用程序:moomSpectacleRectanglePhoenix。我最终选择了Phoenix,这是一个开源的窗口管理器,可以用JavaScript编写脚本。实际上,它只是一种让你用JavaScript控制窗口的方法;没有默认行为。这使得设置Phoenix的工作比其他的多一些,但它有无限的灵活性。

Phoenix提供了一个直接的API,让你可以注册全局键监听器,获得有关打开的窗口的信息,并将它们移动。例如,下面的片段将设置ctrl+shift+h ,使一个窗口填满显示器的左半部分,并设置ctrl+shift+l ,使其填满右半部分:

Key.on('h', ['ctrl', 'shift'], () => fill('left'))
Key.on('l', ['ctrl', 'shift'], () => fill('right'))

function fill(area) {
const window = Window.focused();
const screenFrame = window.screen().flippedVisibleFrame();
const frame = {
...screenFrame,
width: screenFrame.width / 2
};
if (area === 'right') {
frame.x += frame.width;
}
window.setFrame(frame);
}

配置脚本存储在~/.phoenix.js 。我的phoenix配置并不十分复杂(至少与一些例子相比不是很复杂)。大多数情况下,我只是用它来移动和调整窗口的大小,以适应一些预定义的区域,并在空间和桌面之间移动窗口,但还有更多的可能性!例如,你可以允许窗口在不同的地方移动。例如,你可以允许窗口自由调整大小,或者用一个按键激活预定义的桌面布局。

这与终端环境没有直接关系,但如果你必须处理外部窗口,至少你可以使用键盘!这是很重要的。

总结

本文介绍了我目前基于终端的开发环境的一些亮点。请看以前的版本,以了解关于完整设置的更多细节。

你是否有任何基于终端的实用程序、vim插件,或者使你富有成效的使用模式?请考虑分享它们。GitHub包含了数以千计的dotfiles repos(用户配置文件的标准术语),显示了其他人为了使自己的生活更轻松而想出的办法。此外,请随时将有关我的设置的建议或问题发布到我的dotfiles repo