Я посвящу этой теме целый пост, потому что она «не помещается в окно Овертона»; чтобы люди хотя бы начали понимать, что я пытаюсь описать, мне обычно приходится показывать видео. Итак, вот то самое видео:
Людей обычно удивляет происходящее в моменты 0:11, 0:21 и 0:41. Когда я говорю «удивляет», то не просто имею в виду, что их удивляет, как я это сделал, а то, что это вообще возможно.
Объясню, что происходит в видео:
0:00 Начинаем с Windows Terminal, открытого на ноутбуке.
0:02 Я нажимаю
Ctrl + Shift + 5
, открывается новая вкладка терминала, которая подключается поssh
к моему домашнему десктопу и сразу запускает tmux.0:03 tmux запускает мою оболочку по умолчанию (
zsh
). zsh показывает приглашение командной строки, а затем асинхронно загружает всю конфигурацию.0:08 При помощи
zoxide
я выполняю нечёткий поиск недавней папки.0:09 Начинаю вводить команду ripgrep. zsh автоматически завершает команду и я подтверждаю её, нажав
Ctrl + F
.0:11 Нажимаю
Сtrl + K
,F
; эти горячие клавиши приказывают tmux поискать во всём предыдущем выводе имена файлов. Имена файлов выделены голубым.0:12 Я удерживаю
N
для перемещения между файлами. Их довольно много, поэтому мне требуется время, чтобы найти нужный.0:21 Нажимаю
O
, чтобы открыть выбранный файл в приложении по умолчанию (nvim
). tmux запускает его в новой панели. Стоит иметь в виду, что всё это по-прежнему выполняется на удалённом сервере; приложение открывает удалённый файл в удалённой панели tmux. Мне не нужно, чтобы эта кодовая база клонировалась локально на мой ноутбук.0:26 Пытаюсь пройти по множеству ссылок при помощи rust-analyzer, но мне не удаётся это сделать, потому что RA не понимает макросы в этом файле. На 0:32 мне наконец удаётся найти работающий и перейти по ссылке.
0:38 Я нажимаю
Ctrl + K
,H
, приказывая таким образом переключить фокус обратно на левую панель.0:39 Снова нажимаю на
N
. Панель по-прежнему находится в «режиме копирования», поэтому все предыдущие файлы всё ещё находятся в фокусе поиска. Они снова подсвечиваются, и tmux выбирает следующий файл в порядке поиска.0:41 Нажимаю на
O
, открывается другой файл, но в том же экземпляреnvim
.0:43 Нажимаю на
B
, отображаются буферы открытых файлов. В частности, там видно, что предыдущий файл по-прежнему открыт. Прежде, чем завершить поток, я пару раз переключаюсь между двумя файлами.
Но зачем?
VSCode надоел мне своей тормознутостью , особенно при работающем плагине vim, а также кучей конфликтов привязок клавиш между редактором, плагином vim, терминалом и управлением окнами. Я попробовал zed, но в то время он был довольно сырым (к тому же у него всё ещё есть проблема кучи конфликтов привязок клавиш).
Я перешёл к работе с nvim в терминале, но мне быстро надоело то, сколько времени приходится тратить на копипастинг имён файлов в редактор; я часто копипастил файлы со столбцами из ripgrep, возникала синтаксическая ошибка и чтобы открыть файл, их приходилось редактировать вручную. Это сильно бесило. Мне нужен был аналог Ctrl-клика из VSCode, чтобы можно было выбрать произвольный путь к файлу и открыть его так же удобно, как и перейти к нему. Поэтому я начал работать в tmux и настроил всё самостоятельно.
Меня иногда спрашивают, почему tmux. Именно поэтому! В этом-то и причина! (Ну, и в сохраняемости сессий.) Терминалы безумно мощны, но большинство из них показывает лишь малую долю своей мощи пользователю. Мне нравится tmux, несмотря на его возраст, баги и устаревший синтаксис, потому что в этом смысле он очень хорошо расширяем.
Как это работает
Поиск имён файлов в проскролленых строках
Он реализован одной лишь конфигурацией tmux:
# Прошу прощения
# См. "search-regex.sh", чтобы понять, что это всё означает
# TODO: добавить имена переменных оболочки
bind-key f copy-mode \; send-keys -X search-backward \
'(^|/|\<|[[:space:]"])((\.|\.\.)|[[:alnum:]~_"-]*)((/[][[:alnum:]_.#$%&+=@"-]+)+([/ "]|\.([][[:alnum:]_.#$%&+=@"-]+(:[0-9]+)?(:[0-9]+)?)|[][[:alnum:]_.#$%&+=@"-]+(:[0-9]+)(:[0-9]+)?)|(/[][[:alnum:]_.#$%&+=@"-]+){2,}([/ "]|\.([][[:alnum:]_.#$%&+=@"-]+(:[0-9]+)?(:[0-9]+)?)|[][[:alnum:]_.#$%&+=@"-]+(:[0-9]+)(:[0-9]+)?)?|(\.|\.\.)/([][[:alnum:]_.#$%&+=@"-]+(:[0-9]+)?(:[0-9]+)?))'
А вот содержимое search-regex.sh
:
start_delim='(^|/|\<|[[:space:]"])'
relative_path='(\.|\.\.)'
start_path="($relative_path|[[:alnum:]~_\"-]*)"
component='[][[:alnum:]_.#$%&+=@"-]'
intermediate_paths="(/$component+)"
line_no='(:[0-9]+)'
file_end="($component+$line_no?$line_no?)"
end="([/ \"]|\.$file_end|$component+$line_no$line_no?)"
echo "$start_delim$start_path(${intermediate_paths}+$end|${intermediate_paths}{2,}$end?|$relative_path/$file_end)"
# Тестовые случаи вырезаны для краткости
Я не буду объяснять всё регулярное выражение, можете изучить его сами. Для создания всего этого мне потребовалось гораздо больше времени, чем я ожидал.
Открытие выбранного файла в новой панели с запущенным nvim
На самом деле, это трюк, состоящий из нескольких этапов.
Открываем выбранный файл в приложении по умолчанию
Тут всё не так уж плохо. Это снова tmux.
# `cd` важен на случай, если это относительный путь. `echo | bash` используется для выполнения расширения тильды.
bind-key -T copy-mode-vi o send-keys -X copy-pipe \
'cd #{pane_current_path}; xargs -I {} echo "echo {}" | bash | xargs open' \; \
if -F "#{alternate_on}" { send-keys -X cancel }
У меня есть и версия, всегда открывающая редактор в текущей панели, а не запускающая его в приложении по умолчанию. Например, для просмотра файлов JSON я использую fx
, а для их редактирования — nvim
.
# сохраняем буфер, а затем открываем редактор в текущей панели
bind-key -T copy-mode-vi O send-keys -X copy-pipe-and-cancel \
'tmux send-keys "C-q"; xargs -I {} tmux send-keys "${EDITOR:-vi} {}"; tmux send-keys "C-m"'
Открываем новую панель с запущенным nvim
Здесь есть трюк: я создал скрипт оболочки (на самом деле, скрипт Perl), который используется как приложение по умолчанию для всех текстовых файлов.
Настраивать такую кучу ассоциаций файлов вручную очень мучительно. Я напишу отдельный пост о скриптах, устанавливающих мои dotfile в систему. Я не использую Nix частично потому, что у всех друзей, работающих с Nix, возникают ещё более странные баги, чем раньше, а частично потому, что мне не нравится философия, не позволяющая выполнять установку в среде выполнения. Я хочу выполнять установку в среде выполнения и отслеживать то, что я сделал. Это тоже тема для отдельного поста.
Самое важное здесь следующее:
# не используйте ``, чтобы аргументы могли иметь встроенные конвейеры
my @split = ('tmux', 'split-window', '-h', '-P', '-F', '"#{pane_id}"', $editor, @args);
open(my $fd, '-|', @split) || die "can't open pipeline: $!";
Это передаётся обратно в tmux. Скрипт очень тупой, он предполагает, что tmux работает на той же машине, где находится и файл, что в нашем случае именно так. Это неплохо бы проверять — я просто использую отдельную вкладку эмулятора терминала для каждого важного мне экземпляра tmux; например, я часто держу открытой одну вкладку Windows Terminal для WSL на локальном ноутбуке, ещё одну для десктопа и ещё одну для удалённой рабочей машины через VPN.
На самом деле, здесь происходит не только это: например, я перевожу синтаксис
file:line:column
в понятный для vim вид и переопределяюxdg-open
, чтобы он не выполнил выход с ошибкой на:line
; но чаще всего здесь всё просто и не очень интересно.
Открытие файла в запущенном экземпляре nvim
Это скрипт Perl, заставляющий tmux отправлять нажатия клавиш в запущенный экземпляр nvim (на самом деле, это предыдущий скрипт Perl, чтобы оба можно было привязать к одним горячим клавишам вне зависимости от того, открыт ли nvim):
my $current_window= trim `tmux display-message -p "#{window_id}"`;
my $pane = trim `tmux list-panes -a \\
-f '#{&&:#{==:#{window_id},$current_window},#{==:#{pane_current_command},$editor}}' \\
-F '#{pane_id}'`;
# ...
# выходим из режима копирования, чтобы эти клавиши не отправлялись напрямую tmux
`tmux send-keys -t $pane -X cancel 2>/dev/null`;
# Escape по какой-то причине не передаётся, как escape, если встречается рядом с какими-то другими клавишами
`tmux send-keys -t $pane Escape`;
my $args = join ' ', @args;
my $cmd = $editor eq 'nvim' ? 'drop' : 'open';
`tmux send-keys -t $pane ":$cmd $args" Enter`;
`tmux select-pane -t $pane -Z`;
Что мне даёт такая система
Мне не нужен сложный терминал локально, достаточно чего-нибудь с красивыми шрифтами. Всё, что сложно, выполняется через tmux, и это здорово, потому что работает и в Windows без необходимости установки отдельного терминала.
Система редактора работает, даже если редактор не поддерживает удалённый скриптинг. nvim поддерживает RPC, но эта система работала, даже когда я пользовался
helix
иkakoune
.Можно было бы написать всё так, чтобы сложные скрипты эмулятора терминала находились в редакторе, а не в tmux (то есть
:terminal
в nvim), но это тоже привязало бы меня к редактору, а встроенные в редакторы терминалы обычно не очень хороши.
А ты действительно хочешь пользоваться tmux?
Рад, что мы этого коснулись. Последнее, что заставляло меня оставаться на tmux — это сохранение сессий, а Ansuz недавно выпустил автономный инструмент, реализующий сохранение и ничего кроме. Поэтому в ближайшем будущем я планирую перейти на kitty, что позволит мне продолжать пользоваться всеми этими скриптами и не потребует засовывать целый второй эмулятор внутрь эмулятора терминала, что, как я надеюсь, уменьшит количество загадочных багов, с которыми регулярно сталкиваюсь.
Я выбрал kitty, а не wezterm потому, что интеграция с ssh реализована через интеграцию с оболочкой, а не запуском серверного процесса, поэтому её не нужно устанавливать на удалённом хосте. Это было не так важно в случае tmux, потому что tmux есть везде, но wezterm почти нигде не установлен по умолчанию.
... и стоило ли оно того?
Если честно, то да. Теперь на борьбу с редактором я трачу гораздо меньше времени.
стало гораздо проще выполнять отладку, когда что-то пойдёт не так (инструменты отладки VSCode в основном рассчитаны на авторов расширений плагинов, и запуск их нетривиален). С плагинами vim можно просто добавить операторы
print
в исходники на Lua и начать разбираться, что же произошло.Все мои привязки клавиш удобны для меня!
Редактор меньше тормозит.
Для терминала гораздо проще писать скрипты через tmux, чем при помощи написания плагина VSCode, для чего обычно требуется настраивать целый тулчейн typescript и выполнять переключение контекста на новый проект.
Тем не менее, я не могу в здравом рассудке рекомендовать эту систему кому-то ещё. Все мои скрипты ненадёжны и могут поломаться от малейшего ветерка. Так что если их писали не вы, вам сложно будет понять, откуда начинать отладку.
Ну ладно, но мне всё равно это нужно
Если вам нужно что-то похожее, но вы не хотите писать собственные инструменты, то могу порекомендовать следующее:
fish + zoxide + fzf. Так вы получите этапы 4, 5 и в какой-то степени 6.
«Встроенная функциональность редактора» — нечёткий поиск, полнотекстовый поиск, вкладки и окна, «открыть недавний файл».
qf, позволяющий как бы реализовать функцию «выбрать файлы в выводе терминала» этапа 6. Однако придётся не забыть передавать ему вывод в конвейере, поэтому это не сработает постфактум и не сработает, если инструмент интерактивен. Стоит отметить, что в нём жёстко прописан CLI в стиле vi (
vi +line file.ext
), так что, возможно, вам придётся форкнуть его или всё равно добавить скрипт, заменяющий $EDITOR. Дополнительную информацию см. в последнем посте Джулии Эванс.e позволяет в какой-то степени реализовать функцию «преобразовать
file:line
в то, что понимает редактор» этапа 8. Я не слышал об этом инструменте, пока не написал собственный в буквальном смысле с тем же названием и возможностями, забыл поместить его в PATH и получил предложениеcommand-not-found
установить его. Смешно.vim --remote filename
,code filename
илиemacsclient filename
позволяют в какой-то степени реализовать этап 12. Проблема здесь в том, что не все они поддерживаютfile:line
, и вам придётся вносить изменения при переходе на другой редактор. Но, наверно, люди не так часто меняют редакторы.
Чему же мы научились?
Терминалы гораздо мощнее, чем считается! Если использовать терминалы, которые можно скриптовать, то это сильно расширит ваши возможности.
Можно в какой-то мере воссоздать большинство этих фич без скриптинга терминала, если вас устраивает привязка к редактору.
Для создания такой системы требуется куча работы, потому что никто авторов этих инструментов не подумал об этих фичах заранее.
Надеюсь, пост был интересным! Мне всегда любопытно, какие инструменты и как используют другие люди, так что можете написать мне письмо и рассказать о своей системе.