Как стать автором
Обновить

Mein Konfig: экскурсия по dotfiles

Время на прочтение76 мин
Количество просмотров15K
Приветствую! Я люблю рассматривать чужие dotfiles и рассказы об устройстве рабочего места и окружения. Во-первых, любопытно как работают другие люди. Во-вторых, нередко находишь какой-нибудь полезный трюк или идею. А повышение продуктивности разработчика ПО часто идёт за счёт, казалось бы, мелочей (много кто помнит, что Ctrl-T посылает SIGINFO сигнал, показывающий прогресс копирования cp или dd?). Вот и решил рассказать про своё рабочее окружение, в том числе dotfiles. Перечислю список секций этой статьи, чтобы было примерное представление о чём пойдёт речь: Ввод, ОС, ФС, X11, MRA и MDA, Сеть, Jail, Демоны, MTA, MUA, WWW, PGP, IRC, st, tmux, cd, git, zsh, zsh completion, zsh history, ZLE, zsh prompt, zsh misc, less, grep, .zshenv, autoenv, .zprofile, t, Музыка, mpv, Картинки, Архивы, Feeds, Hjson, *tex*, File transfer, Games, БД, ЯП, Python, Go, C, redo, TAI64, Vim, tags, .vimrc, ~/.vim/pack, ~/.vim/plugin, ~/.vim/ftplugin. Безусловно будут спорные holywar высказывания, так что везде иметь в виду и добавлять фразу «по моему личному мнению».




Коротко замечу, что я yet another средненький программист, сидевший за компьютерами ещё со школы. Знакомство с Unix-like системами началось с Mandrake Linux 20+ лет назад. Далее шесть лет на FreeBSD с версии 4.5, семь лет на Debian и немного Ubuntu. Восемь лет назад вернулся на FreeBSD.

Ввод



Начну с устройств ввода, для полноты картины, без конкретных моделей железа, дабы это не восприняли как рекламу.

Клавиатура — мой хлеб, основной (единственный?) инструмент используемый сутки напролёт. И она обязана быть тактильной. Не забуду день когда приобрёл тактильную клавиатуру ради попробовать. До неё я много лет использовал ThinkPad ноутбук, идеально зная его клавиатуру, к тому же относительно качественную, не то что современные «островные». Перед подключением тактильной я проверил свою скорость в typespeed: 300+ символов в минуту. Проверил через час работы скорость на новой, прежде мне невиданной и с чуть другой раскладкой: 400+ символов. Просто сменив клавиатуру, на треть увеличил скорость своего ввода!

К тому же, получая стандартизованную раскладку, без необходимости каждый раз переучиваться на очередной модели ноутбука или дешёвой плёночной клавиатуры из-за чуть другого расположения клавиш. Даже в дальние командировки с кучей вещей, тащу с собой весящую больше килограмма клавиатуру. И не смотря на то, что её стоимость выше чем некоторых готовых ПК или ноутбуков — она стоит того.

Всегда использовал Cheery MX Blue переключатели, но мог бы работать и на Brown. Сейчас у меня клавиатура без каких-либо надписей на клавишах — полностью чёрная. Зачем? А почему бы и нет, понтоваться. Но, возникла одна проблема из-за этого: я активно использую правые Super и Menu клавиши и, так как в ряд находятся Alt, Super, Menu, Ctrl одинакового размера, то не могу на ощупь понимать где точно находится мой большой палец. Поэтому заменил клавишу Super на ту, где нарисован Windows логотип в кружочке, который осязаю и уже не теряюсь. Так что на всей клавиатуре у меня только логотип Windows виден.

Мышками не пользуюсь уже где-то лет восемь, везде перейдя на трэкболы. К каждому трэкболу нужно привыкать, но с ним получается быстрее и точнее выделять элементы текста в случайных местах. Плюс он занимает меньше места, так как стоит на месте. Но требует частой прочистки от скапливающейся, даже по несколько раз на дню, пыли и грязи.

Сейчас у меня трэкбол с четырьмя кнопками и колесом прокрутки — очень удобная штука! Big balls matters — большой тяжёлый шарик удобнее («занимает» ощутимую площадь — меньше напряжения пальцев) и с ним можно совершать более «далёкие» движения из-за его массы и инерции при прокрутке.

Если без тактильной клавиатуры жизни нет (мне проще вернуться назад домой за ней, потратив время, чем пытаться работать за ноутбучными или плёночными целый день, сжигая свои нервы из-за ущербного КПД), но про трэкбол такого уже не скажу. Кроме них я бы использовал «тензометрический джойстик» (TrackPoint), так как руки вообще не нужно убирать с клавиатуры.

Из обязательных материальных вещей и дома и на работе имеется плюшевый Beastie рядом с компьютером.

ОС



Я использую только свободное ПО. Остальному табу. Кто предлагает использовать несвободное ПО или форматы — того прошу пересмотреть своё предложение, иначе никакого взаимодействия у меня с ним не будет. Этическая сторона для меня важнее. И да, например RAR формат имеет open-source unrar декомпрессор, однако являющийся несвободным, из-за запрета делать на его основе компрессор.

И на работе, и дома, и на VPS у меня FreeBSD ОС. Потому что она гораздо лучше для моих потребностей и ценностей. Даже FidoNet я годами пользовался исключительно на FreeBSD (похоже это было единственное использование редактора GoldEd отличного от Vi(m)). Но это не означает что я одобрительно отношусь к permissive лицензиям, категорически предпочитая GPL-семейство copyleft-ных.

Я не устанавливаю бинарные пакеты — и вся ОС и все программы в ней (многие из портов) собираются из исходного кода. Все скачиваемые в /usr/ports/distfile исходники хранятся вечно, дабы была возможность пересобрать абсолютно всё в системе без зависимости от доступа к Интернету. Срез портов же использую той версии, что поставлялась к RELEASE версии дистрибутива. Изредка обновляя выборочные из upstream-а. Не сторонник bleeding edge обновлений.

Несколько десятков программ, всё увеличивающееся количество, у меня ставится в домашней директории (так как я единственный пользователь на компьютерах) используя GNU Stow. Устанавливаю LLVM более новой версии в ~/local/stow/llvm, Mutt в ~/local/stow/mutt, а stow уже сделает все необходимые символические ссылки в ~/local/bin, ~/local/share и подобных директориях.

В итоге получаю программы сосредоточенные в отдельной директории (а-ля chroot), но при этом мне достаточно в PATH добавить знание только о ~/local/bin вещах, в CFLAGS о ~/local/include, и т.д… Установкой в глобальный /usr/local мне пришлось бы ещё как-то хранить информацию о конкретных файлах касающихся заданной программы (чем и занимаются пакетные менеджеры) — с stow всё сильно проще и удобнее. Плюс пакетные менеджеры зачастую являются distribution/OS-специфичными, тогда как stow везде одинаков и минималистичен: одна Perl программа.

Изначально я использовал stow для хранения в git-репозитории своих dotfiles. Другого более удобного способа не видел как всё это иметь в порядке. Хотя вариант с хранением самого HOME в git, но «перенаправленном» через --git-dir, тоже выглядит ничего. Но в нём не могу взять и убрать/добавить файлы касающиеся только XXX программы (без создания коммитов с удалением и revert-ом), тогда как в stow делаю stow -D XXX или stow -S XXX.

Все мои программы на Си подразумевают только статическую линковку. И часть софта целенаправленно стараюсь ставить только в статически слинкованном варианте. Holywar тема, есть много за и против с обеих сторон, но для меня преимущества статической линковки, как для обычного конечного пользователя, стали очевидны когда воочию лицезрел сотни миллисекунд затрачиваемые на запуск исполняемого файла ImageMagick-ового convert с кучей зависимых динамических библиотек. Тогда как сама конвертация JPEG занимала несколько десятков миллисекунд. Это неприемлемо. Спокойно готов разменивать место на жёстком диске (если вспомнить про статически линкуемые программы на Go) на скорость их запуска.

Производительность и потребление памяти жутко зависят от используемых libc, malloc библиотек. И если бы пришлось использовать GNU/Linux, то выбор бы пал на Void Linux с musl и runit из коробки.

А аргумент про обновление одной .so, дабы исправить уязвимость, для меня является брехнёй, ибо в нормальной системе всегда должны быть исходники из которых можно бы было полностью всё пересобрать. Скорость компиляции Си или Go — очень высоки, так что пересборка сотен программ во всей ОС не будет проблемой. Хочу чтобы система работала быстро, имела хорошую отзывчивость и была простой.

ФС



Везде ZFS файловая система, евангелистом которой являюсь. Колоссальное удобство, замена RAID/LVM/flashcache, гарантированное обнаружение нарушенной целостности. Удобнейшая возможность создания полных резервных копий, в том числе инкрементальных, через снимки и zfs send/recv. Более высокая производительность из-за прозрачной компрессии, дедупликации, клонов, больших блоков линейно размещённых на диске, resilvering-а только полезных данных — конечно же когда задача позволяет задействовать перечисленное. Надёжный RAID, без дорогостоящих vendor-lockin контроллеров, в теории спасающих от write-hole. Скорость и удобство создания снимков и их отката ощутимо меняют привычки. Бывает, даже лень использовать средства git-а, вместо этого делая snapshot/rollback ZFS. Можно сказать, выбираю ОС по наличию добротной и хорошей реализации ZFS — что пока сводит выбор к FreeBSD, illumos и возможно NetBSD.

ZFS используется поверх GPT разделов. Во-первых, они дают универсально (в отличии от BSD-специфичного glabel) работающие метки для блочного устройства, не зависящие от контроллеров, их положения в компьютере, от WWN-ов и серийников дисков (можно будет dd-ой «скопировать» диск). Во-вторых, этим можно сделать выравнивание по 4KiB-секторам. В-третьих, разместить swap-раздел вне ZFS ZVOL-а. В-четвёртых, с конца диска можно зарезервировать небольшое пространство для потенциального нивелирования разницы в размере между дисками разных производителей и моделей — будет обидно если новый диск будет чуть меньше старого и на него нельзя будет переехать dd копированием.

На моей практике, диски до сих пор упорно твердят что размер их сектора 512 байт. Поэтому ZFS принудительно приходится говорить о 4KiB секторах (vfs.zfs.min_auto_ashift=12) во время создания pool. Ощутимо влияет на производительность (но зависит от задачи).

Прозрачная LZ4 компрессия всегда включена. Этот алгоритм умеет делать быстрый fallback до «прозрачного» режима работы, поэтому overhead-а на несжимаемых данных не будет заметно. Кроме того, с выключенной компрессией sparse блоки файла будут записываться на диск, чего не хочется. На не загрузочных dataset-ах использую Skein хэш в качестве контрольной суммы — как минимум, оно быстрее SHA256, а не криптографическим функциям просто не доверяю.

atime везде по умолчанию выключен, за ненужным overhead-ом. Единственное место где он остаётся — раздел с почтой в Maildir-ах. А также нужно не забывать про recordsize=1M на dataset-ах где складируются фильмы, музыка и фотографии, чтобы размещать их более линейно на диске.

Swap раздел в /etc/fstab имеет приписку .eli (/dev/gpt/SWAP.eli), чтобы FreeBSD автоматически сделала одноразовый эфемерный ключ для прозрачного GELI шифрования этого раздела. Так можно будет не беспокоится о том, что какие-либо ценные данные осядут в открытом виде на диске.

Важные данные, с точки зрения конфиденциальности, у меня хранятся на отдельных ZFS pool-ах на зашифрованных GELI устройствах, в свою очередь являющимися ZVOL-ами. После загрузки компьютера, если мне нужно работать со своими персональными данными (почта) или рабочими, то вхожу под root-ом, запускаю скрипт подключения GELI устройства, импортирования дополнительных ZFS pool-ов. Это позволяет мне не «открывать» разделы с важной информацией еслия со своим ноутбуком нахожусь в небезопасном месте, всё равно имея работающую систему. Но в моём подходе нет аутентификации данных на незашифрованных разделах. Иногда я делал mtree слепок незашифрованной части файловой системы, храня его на стороннем накопителе, чтобы понять не было ли чего модифицировано.

Резервные копии делаю как просто ручным выполнением команды типа zfs send -R… | zstd | gpg -e… > ..., так и самописным zsnap скриптом, просто вызывая его с snap аргументом для создания очередного инкрементального snapshot, sync /path аргументом для сброса на диск отсутствующих на нём инкрементальных дампов.

X11



Давным давно я много лет использовал исключительно родной терминал FreeBSD, без запуска X11. В отличии от GNU/Linux, в нём отлично поддерживалась и кириллица и мышка и прокрутка текста. Но сейчас всегда запускаю X11, так как в нём проще использовать удобный шрифт, да и бывает надо посмотреть картинку или PDF-ку.

Xorg-у не позволяю брать управление мышкой на себя (Option «AutoAddDevices» «false» в /etc/X11/xorg.conf), используя «системную» мышь под управлением moused: Option «Device» "/dev/sysmouse". Это позволяет конфигурировать кнопки мышки глобально и для терминала и для X11, плюс, если не изменяет память, были какие-то загвоздки к hotplug-ом в X11.

Для бывшего трэкбола Logitech Marble, без колеса прокрутки, включал его эмуляцию через зажимание одной из маленьких кнопочек. Демон (корректнее говорить «деймон», ибо это «daemon», а не «demon») moused запускался с особыми параметрами для заданного USB-устройства:

----- /etc/devd/mymouse.conf -----
attach 20 {
        device-name "ums[0-9]";
        match "vendor" "0x046d";
        match "product" "0xc408";
        action "/usr/sbin/moused -p /dev/$device-name \
            -I /var/run/moused.$device-name.pid -w 5 -m 2=4";
};


Раз залезли в правила devd, то вот ещё один скрипт для выставления bitperfect режима работы внешней USB звуковой карты:

----- /etc/devd/myaudio.conf -----
attach 20 {
    device-name "uaudio[0-9]";
    match "vendor" "^0x0b05$";
    match "product" "^0x17f3$";
    action "/etc/devd/myaudio-asus.sh $vendor $product";
};


----- /etc/devd/myaudio-asus.sh -----
#!/bin/sh
[ $# -eq 2 ] || exit 1
vendor=$1
product=$2
uaudio=$(sysctl dev.uaudio | sed -n "s/^dev\.uaudio\.\([^.]*\)\..*vendor=$vendor
    product=$product.*\$/\1/p")
pcm=$(sysctl dev.pcm | sed -n "s/^dev\.pcm\.\([^.]*\)\.%parent: uaudio$uaudio\$/\1/p")
/sbin/sysctl dev.pcm.$pcm.bitperfect=1


Переключение раскладки (английскийкириллица) делается CapsLock-ом:

----- /etc/X11/xorg.conf -----
Section "InputDevice"
        Identifier  "Keyboard0"
        Driver      "kbd"
        Option "XkbRules" "xorg"
        Option "XkbLayout" "us,ru"
        Option "XkbOptions" "grp:caps_toggle,grp_led:caps"
EndSection


Я много пишу на обоих языках, часто переключаясь, поэтому варианты с нажатием двух клавиш отпадают. Shift+CapsLock позволяют сделать начальную задумку CapsLock-а. При этом, на клавиатуре, при переключении на кириллицу, загорается CapsLock индикатор — поэтому не нужно нигде в tray или какой-либо панели показывать какой сейчас язык включён, ибо у меня настоящая лампочка для этого.

Запуском X11 руководит:

----- ~/.xinitrc -----
#!/bin/zsh
xsetroot -solid \#222222
xset b off
xrdb ~/.Xresources
xmodmap ~/.Xmodmap
~/bin/xstatusbar.sh &
rm -f ~/.ssh/agent ; ssh-agent -a $SSH_AUTH_SOCK
exec ~/src/suckless/dwm/dwm


  • xsetroot выставляет цвет фона. Лишь бы не бил по глазам.
  • xset b отключает почернение экрана после timeout-а, но при этом энергосберегающий режим не отменяется, выключающий питание монитора через DPMS.
  • SSH агент запускается один раз на всю X11-сессию. Так как мой .xinitrc является zsh скриптом, то перед его выполнением загружается ~/.zshenv, в котором определён SSH_AUTH_SOCK.


~/.Xmodmap содержит mapping-и между мультимедийными клавишами моей клавиатуры и XF86 событиями:

----- ~/.Xmodmap -----
keycode 92 = XF86AudioRaiseVolume
keycode 170 = XF86AudioLowerVolume
keycode 190 = XF86AudioMute
keycode 184 = XF86AudioNext
keycode 189 = XF86AudioPrev
keycode 147 = XF86AudioPlay


Эти события позволяют в mpv проигрывателе переключать трэки и управлять громкостью. Больно надо ли это? Нет, но почему бы и не задействовать мультимедиа клавиши?

----- ~/.Xresources -----
Xft.dpi: 96
Xft.hinting: true
Xft.antialias: true
Xft.autohint: false


содержит настройки отображения шрифтов. Далеко не всем нравится когда autohinting отключён, но мне так комфортнее.

В качестве оконного менеджера использую DWM. Мне его полностью хватает для всех нужд с конца 2000-х. dwm без проблем работает и с несколькими мониторами. Смысла в i3, awesome или подобных менеджерах не вижу для себя, а dwm, как минимум, более минималистичен с точки зрения исходного кода. По сути у меня единственная non-default настройка:

----- ~/src/suckless/dwm/config.h -----
static const Rule rules[] = {
    { "Xombrero", NULL, NULL,  1 << 8, 0, -1 },
    { NULL, NULL, "Terminal9", 1 << 8, 0, -1 },
    { NULL, NULL, "Terminal8", 1 << 7, 0, -1 },
    { NULL, NULL, "Terminal7", 1 << 6, 0, -1 },
    { NULL, NULL, "Terminal6", 1 << 5, 0, -1 },
    { NULL, NULL, "Terminal5", 1 << 4, 0, -1 },
    { NULL, NULL, "Terminal4", 1 << 3, 0, -1 },
    { NULL, NULL, "Terminal3", 1 << 2, 0, -1 },
    { NULL, NULL, "Terminal2", 1 << 1, 0, -1 },
    { NULL, NULL, "Terminal1", 1, False, -1 },
};


автоматически присваивающая соответствующий тэг окнам с TerminalX названием. По сути перемещает окно с заданным названием на нужный рабочий стол.

dwm имеет встроенный statusbar, который обновляется скриптом (многие скрипты написаны на коленке и не модифицируются с момента создания, поэтому выглядят не элегантно):

----- ~/bin/xstatusbar.sh -----
#!/bin/sh
while :; do
    life=$(apm -l)
    flags="$(ls /tmp/stargrave-flags | perl -e 'print join " ", sort map
        { chomp and $_ } <>')"
    topinfo=$(top -b -d 1 0 | sed s/[,:]//g)
    meminfo=$(echo "$topinfo" | grep "^Mem")
    arcinfo=$(echo "$topinfo" | grep "^ARC")
    swpinfo=$(echo "$topinfo" | perl -ne 'print $1 if
        /^Swap \w+ Total (\w+) Used/')
    xsetroot -name "$swpinfo   $meminfo   $arcinfo [$flags] $life%
        $(date "+%Y-%m-%dT%H:%M:%S")"
    sleep 20
done


В нём отображаются: время, дата, заряд аккумулятора, так называемые флаги, статистика потребления памяти (297M Active, 352M Inact, 1007M Wired, 6165M Free), статистика ARC (ZFS кэш) (560M Total, 286M MFU, 253M MRU, 3326K Anon, 4864K Header, 13M Other) и swap потребление. Для меня было удивительно, что одиночный пробел не может показать разделение секций в statusbar, но двух уже достаточно чтобы чётко выделяться.

На данный момент есть всего два возможных флаговых файла в /tmp/stargrave-flags:

  • FM — fetching mail, показывает что почта должна забираться по POP3 время от времени.
  • WG — web GUI, показывает что по умолчанию у меня будет запускаться GUI броузер для открытия URL-ов.


Флаговые файлы (по задумке) использую для глобальной эфемерной настройки поведения системы. Планируется открывать ссылки в GUI броузере? touch /tmp/stargrave-flags/WG или rm чтобы убрать флаг. Когда я на работе, то выставляю FM флаг, в остальное время не нагружая каналы связи проверкой корреспонденции.

MRA и MDA



Кто и как проверяет флаг FM и скачивает почту? Последняя строка в пользовательском crontab:

----- crontab -u stargrave -l -----
0  12  *  *  *  newsyslog -r -f $HOME/.newsyslog.conf
0  1   *  *  *  find $HOME/secure/vim -type f -atime +1 -delete
0  1   *  *  *  d=$( date -j -v +7d "+\%y-\%m" ) ; [ -e $HOME/mail/mbox ] && {
    umask 077 ;
    for mbox in back XXX sent ; do
        for subdir in cur new tmp ; do
            mkdir -p $HOME/mail/$mbox-$d/$subdir ;
        done ;
    done }
11  */6  *  *  *  [ -e /tmp/stargrave-flags/FM ] && fdm -q -a XXX fetch || :


Ротация логов на данный момент применяется только к журналу maildrop (раньше были и другие), но не устанавливать же эту ротацию в глобальный конфиг newsyslog?

----- ~/.newsyslog.conf -----
/home/stargrave/mail/mailfilter.log stargrave:stargrave 600 1 100 * CYN


В очистке Vim директории тоже нет ничего интересного. В ~/secure/vim хранятся только временные файлы типа tmp, undo и view. А сам ~/secure находится на зашифрованном разделе, поэтому потенциально важные данные не осядут на диске.

Когда-то использовал fetchmail MRA для забора почты по POP3/IMAP4. Теперь советовал бы использовать fdm. Почему решил попробовать последний: fetchmail не умеет в лог писать временной штамп, чтобы было понятно когда и какая попытка скачивания почты была. Плюс fdm умеет брать аутентификационные данные из .netrc, что позволяет отделить пароли от основного конфига. Встроенные возможности фильтрации fdm мне не нужны, но всё равно переход на него стоил того.

Все MRA у меня сохраняли корреспонденцию не напрямую в почтовый ящик, а отправляя на локальный SMTP сервер, через логику которого она проходит дальше, в том числе через MDA. В fdm делаю вот так, чтобы дифференцировать почту приходящую с POP3 почтовых ящиков:

action "lmtp-XXX" smtp server "localhost" to "stargrave+XXX@stargrave.org"


Когда-то использовал procmail MDA, который давно забросили. Жалел что не перешёл раньше на maildrop. Его конфиг чище, проще, понятнее и компактнее. С первого раза свой многосотстрочный .procmailrc перевёл на язык maildrop.

Единственное что maildrop не делает автоматом, в отличии от procmail, так это создание Maildir директорий несуществующих почтовых ящиков. А у меня есть ящики для хранения копий всех писем за каждый месяц. Поэтому в crontab есть команды для создания за неделю до следующего месяца ящиков вида back-21-06, sent-21-06.

maildrop правила в основном раскладывают письма по соответствующим почтовым ящикам. Всё вроде бы понятно и без дополнительных комментариев. Например:

----- ~/.mailfilter -----
logfile $HOME/mail/mailfilter.log
REFORMAIL=/usr/local/bin/reformail
MAILDIR=$HOME/mail
DEFAULT=$MAILDIR/mbox
SPAM=$MAILDIR/spam

if ( /^Delivered-To:.*stargrave.XXX@stargrave.org/ && \
    ! /^From:.* <monit@.*stargrave.org/ && \
    ! /^From:.* Cron Daemon/ )
    cc $MAILDIR/XXX-`date "+%y-%m"`

if ( ! /^To:/ )
    to $SPAM
if ( ! /^Subject:/ )
    to $SPAM
if ( /^To: Recipients/ )
    to $SPAM
if ( /^To: .*(undisclosed|unlisted)-recipients/ )
    to $SPAM
if ( /^Received-SPF:.*fail/ )
    to $SPAM
if ( /^X-Mailer:.*Microsoft Office Outlook/ )
    to $SPAM
if ( /^X-Mailer:.*Microsoft Outlook Express/ )
    to $SPAM

if ( /^From:.*Amazon/ )
    to /dev/null
if ( /^Subject:.*10K LinkedIn/ )
    to /dev/null
if ( /^Subject:.*100K Leads/ )
    to /dev/null
if ( /^From:.*Susan Taylor/ )
    to /dev/null

if ( /^To:.*junk.*@stargrave.org/ )
    xfilter "$REFORMAIL -a'X-Label: JUNK'"

if ( /^(To|Cc):.*debian-russian@lists.debian.org/ )
    to $MAILDIR/debian
if ( /^(To|Cc):.*gnupg-(announce|devel|doc|ru|users)@gnupg.org/ )
    to $MAILDIR/gnupg
if ( /^(To|Cc):.*@suckless.org/ )
    to $MAILDIR/suckless
if ( /^To:.*comment@blog.stargrave.org/ )
{
    cc $MAILDIR/back-`date "+%y-%m"`
    to $MAILDIR/blog-comment
}


Сеть



Вся сеть у меня строится поверх IPv6 протокола. Оно стоит того. IPv4 чисто технически присутствует на компьютерах для доступа в legacy IPv4 Интернет, но самостоятельно с IPv4 протоколом и его адресами толком нигде не работаю и не выставляю уже уйму лет.

Для обеспечения безопасности трафика применяю IPsec: ядерная реализация AES-GCM ESP протокола, strongSwan реализация IKEv2. Если мне необходимо поднять masterslave репликацию между DNS серверами, то говорю своему IKE демону что нужно оборачивать пакеты между двумя DNS серверами:

----- /usr/local/etc/ipsec.conf -----
conn nsd-interconnect
        left=2a04::XXX
        leftid=@dns-master.stargrave.org
        right=2001::YYY
        rightid=@dns-slave.stargrave.org
        type=transport
        auto=route


После чего трафик между этими адресами будет в безопасности. AES-GCM использую потому что это AEAD шифр, зачастую имеющий аппаратное ускорение. Если бы ядро предоставляло ChaCha20-Poly1305, то выбрал бы его. Вопрос времени. А в качестве алгоритма ключевого обмена предпочитаю *25519 семейство: за скорость, простоту и криптографическую надёжность:

----- /usr/local/etc/ipsec.conf -----
conn %default
        keyexchange=ikev2
        ike=aes128gcm16-aesxcbc-x25519!
        esp=aes128gcm16-x25519!
        authby=psk
        compress=no
        mobike=no
        reauth=no
        dpdaction=clear
        keyingtries=%forever

conn ndp-ns
        right=::1
        leftsubnet=fc00::/8[ipv6-icmp/135]
        rightsubnet=fc00::/8[ipv6-icmp/135]
        type=passthrough
        auto=route

conn ndp-na
        right=::1
        leftsubnet=fc00::/8[ipv6-icmp/136]
        rightsubnet=fc00::/8[ipv6-icmp/136]
        type=passthrough
        auto=route


здесь видно что некоторые ICMPv6 пакеты пропускаются as-is, так как они нужны для работы NDP. Почему используется fc00:: site-local сеть? Я бы и хотел использовать link-local адреса, но не выходит из-за strongSwan.

Полноценно удобно использовать IPsec можно, как мне видится, только с IPv6, где есть масса адресов, где буквально для каждой отдельной TCP сессии можно было бы выделять по адресу и специфичные IPsec правила для него.

ОС явно сообщаю что предпочитаю работать с IPv6 адресами, если будет выбор. А также включаю mapping IPv4 пространства в IPv6, позволяя программам слушающим на ::/0 адресе быть автоматически доступными в IPv4 пространстве:

----- /etc/rc.conf -----
ip6addrctl_enable="YES"
ip6addrctl_policy="ipv6_prefer"
ipv6_ipv4mapping="YES"


Уважаю WireGuard и одобряю его протокол шифрования и вообще его подход: ничего лишнего. Если не IPsec, то выбрал бы WireGuard!

Для передачи файлов в домашней локальной сети, просмотре фильмов с NAS-а использую NFS. Но обязательно NFSv4 (vfs.nfsd.server_min_nfsvers=4), а если точнее то v4.1+ (nfsv4,minorversion=1 опция монтирования в fstab). NFSv4 кардинально отличается от NFSv3: для меня, как пользователя, он гораздо более удобен в администрировании (не нужна куча демонов) и прописывании правил firewall-а, где достаточен один TCP порт. Не трогаю /etc/exports, используя zfs set sharenfs настройку.

Кроме NFS активно применяю NNCP — своё творение на замену UUCP. По сути, NFS у меня для мультимедиа в первую очередь, а сама передача файлов идёт чаще всего через NNCP, как по сети, так и через флешки/диски/ленты.

Я видел, что есть возможность работы NFS поверх TLS, но искренне убеждён в ущербности и архаичности этой идеи и подхода. В идеальном мире TLS быть не должно: должен быть IPsec (с IPv6), где одна из сторон может быть «анонимна» (BTNS), используя голые публичные ключи (по аналогии с TLS, где клиент анонимен по умолчанию). Не признаю протоколов где TLS вшит by design (даже если не касаться IPsec, то могут быть физически защищённые и изолированные доверенные сети).

Мои правила firewall-а просто не дают обращаться к NFS серверу не из IPsec-secured IPv6 подсетей. Сам факт нахождения и общения через данную подсеть уже является авторизованным доступом.

В качестве firewall большую часть жизни использую ipfw. Он мне понятнее остальных. ipf слишком простоват и даже для моих нужд не хватает гибкости и возможностей. pf никогда не пробовал, да и вроде бы в FreeBSD сильно более старая версия чем в OpenBSD, плюс вопрос производительности.

Затейливых настроек нет. Самый банальный firewall, запрещающий всё на вход по умолчанию, разрешающий stateful соединения наружу. Стараюсь в нём использовать table для «объединения» адресов в одну сущность (например сервер доступен в Интернете под разными IPv4/IPv6 адресами).

Всегда имею deny all from any to any frag правило, запрещающее хождение IPv4 фрагментов — нормальные системы обязаны использовать path MTU discovery, а не создавать излишнюю нагрузку. Разрешено хождение любого ICMPv6, ESP и SSH трафика. Невалидный ESP всё равно будет отброшен на уровне SA в ядре, а SSH демону доверяю и через него получаю удалённый доступ.

В конце всех правил добавляю deny log all from any to any — если через sysctl явно не включено логирование, то это правило и не создаёт никакого ощутимого overhead-а, зато мне не надо будет париться с правилами firewall-а явно добавляя log для выяснения проблем почему же что-то не работает.

Настройки OpenSSH демона тоже без интересностей. Первым же делом на любой системе отключаю возможность аутентификации по паролям, в крайнем случае используя пароль один раз для первоначального входа. А также, как правило, отключаю возможность входа под root-ом.

Для попадания в root нужно уметь делать su. sudo инструмент не люблю и не использую: сложнейшая overengineered утилита, возможности и гибкость которой не нужны практически никому, а безопасность из-за сложности страдает. У меня только один раз в жизни возникла потребность в отдаче только одного привилегированного действия, а не полноценной возможности входа под root-ом. И задача решалась doas утилитой, портированной из OpenBSD. doas это sudo каким он должен был быть с самого начала.

Клиентский конфигурационный файл OpenSSH по сути содержит только настройки приоритетов алгоритмов:

----- ~/.ssh/config -----
Protocol 2
IdentitiesOnly yes
Compression no

Host *
    HostKeyAlgorithms ssh-ed25519-cert-v01@openssh.com,
                      ssh-ed25519,
                      ssh-rsa-cert-v01@openssh.com,ssh-rsa
    KexAlgorithms curve25519-sha256@libssh.org,
                  diffie-hellman-group-exchange-sha256,
                  diffie-hellman-group-exchange-sha1,
                  diffie-hellman-group14-sha1
    Ciphers chacha20-poly1305@openssh.com,aes128-gcm@openssh.com,aes128-ctr
    MACs umac-128-etm@openssh.com,
         hmac-sha2-512-etm@openssh.com,
         hmac-sha2-256-etm@openssh.com,
         umac-128@openssh.com,
         hmac-sha2-512,
         hmac-sha2-256,
         hmac-sha1
    ControlMaster auto
    ControlPath ~/.ssh/sockets/%r@%h-%p
    ControlPersist 60


Compression у меня отключён на всякий пожарный — чтобы не было попыток сжимать данные при передаче бинарных файлов (scp), которые либо несжимаемы, либо будут лучше и быстрее (не упираясь в CPU) сжаты пропуская через zstd-pipe. Компрессия у меня включается для заведомо текстовой передачи данных — работе в удалённом tmux.

Из асимметричных алгоритмов у меня всегда предпочтение к *25519 или *448 кривым — за простоту, скорость, надёжность/безопасность. К ChaCha20-Poly1305 шифру аналогично. Если он недоступен, то предпочтение AEAD режиму работы AES, аппаратно ускоренного. Поэтому, в общем случае, из MACs ничего использоваться не будет, так как только aes*-ctr режим у меня не AEAD.

SHA2-512 предпочитаю SHA2-256, так как на 64-бит системах он работает быстрее. Почему позволяю использовать HMAC-SHA1, ведь SHA1 is already broken? Потому что в контексте использования HMAC — SHA1 вполне себе безопасен, как и MD5.

Control* опции включают автоматическое использование мультиплексируемых каналов до серверов. Если у меня есть хотя бы одна SSH сессия до машины, то значит и вызовы scp на неё будут отрабатывать существенно быстрее, ведь никакого рукопожатия с дорогостоящей асимметричной криптографией делать не надо.

Для расчёта адресов, масок подсети, PTR адресов люблю использовать sipcalc утилиту.

Локально также использую кэширующий DNS сервер Unbound. Нужно исключительно из-за того, что при включении VPN-а до работы, некоторые адреса внутренних серверов необходимо получить с рабочего DNS сервера. Использовать рабочий DNS для всех запросов не хочу: зачем админу видеть всё это?

----- /usr/local/etc/unbound/unbound.conf -----
server:
  do-daemonize: no
  interface: ::1

forward-zone:
  name: "vpn.arbeit.ru"
  forward-addr: 2001:470::XXX

forward-zone:
  name: "arbeit.ru"
  forward-addr: 10.X.Y.Z

forward-zone:
  name: "."
  forward-addr: 2001:470::XXX


Конфиг говорит о том, что информацию о всех зонах нужно брать с моего основного 2001:470::XXX сервера, кроме arbeit.ru зоны, для которой используется другой. При этом можно и сделать override некоторых имён, чтобы не было проблемы курицы и яйца.

Для авторитарного DNS сервера использую NSD. Никаких BIND! Репликация masterидёт поверх IPsec зашифрованного транспорта. Но NSD не отвечает в Интернет напрямую — перед ним стоит CurveDNS DNSCurve-сервер, отдающий зашифрованные и аутентифицированные по публичному ключу, хранящемуся в WHOIS, ответы. Будучи шифропанком, не признаю DNSSEC.

В качестве NTP сервера/клиента мне нравится chrony: простой и понятный интерфейс, простота конфигурирования, хорошая работа (судя по статьям от крупных компаний, заменяющих ntpd на него). После настройки OpenSSH, почтового relay и DNS я в обязательном порядке настраиваю NTP, ибо крайне раздражителен когда не могу понять и прикинуть последовательность событий на разных машинах из-за сбитого времени.

Jail



Иногда использую Jail-ы. Например Mumble использует Qt и не работает без GUI. Он стоит в отдельном от системы Jail-е, так как ради одной, изредка запускаемой программы, мне не хочется тянуть тяжелейшую зависимость в виде Qt. Можно было бы собрать в chroot-е, но Jail-ом его ещё и изолирую от всего постороннего.

У меня есть базовый образ Jail-а: /jail/skel, который клонирую через ZFS (zfs clone) для создания нового Jail-а. mount_nullfs командой могу примонтировать /usr/ports/distfiles в /jail/XXX/usr/ports/distfiles, давая доступ этому изолированному окружению до кэша уже скачанных исходных кодов.

Так получилось, что всё что в Jail-ах, работает с сетью. И мне сильно удобнее и проще управлять ею если у Jail есть отдельный сетевой интерфейс. То что называется VIMAGE в FreeBSD. А также, чаще всего, мне хочется добавлять Jail в bridge с физическим сетевым интерфейсом, для его доступа к Интернету. Для этого создаётся epair пара интерфейсов, переименовывается, добавляется к bridge. Для удобства написал тривиальный shell-скрипт и теперь конфигурация Jail-а занимает считанные строки:

----- /etc/jail.conf -----
mount.devfs;
allow.raw_sockets;
allow.chflags = 1;
exec.clean;
exec.start = "/bin/sh /etc/rc";
exec.stop = "/bin/sh /etc/rc.shutdown";
path = "/jails/${name}";
host.hostname = "${name}.stargrave.org";

mumble {
    devfs_ruleset = 14;
    vnet = new;
    vnet.interface = epairb_${name};
    exec.prestart += "/jail/net-jail-epair.sh ${name} ;
                        ifconfig bridge0 addm epaira_${name}";
    exec.poststop += "ifconfig epaira_${name} destroy";
}

builder {
    vnet = new;
}


----- /jail/net-jail-epair.sh -----
#!/bin/sh -ex
name=$1
ifconfig epaira_$name && exit || :
epair=$(ifconfig epair create)
ifconfig $epair name epaira_$name
ifconfig ${epair%a}b name epairb_$name
ifconfig epaira_$name up


Для запуска виртуальных машин использую родной bhyve. Сами образы стараюсь размещать на ZVOL устройствах ZFS-а из-за меньшего overhead. Например как-то приходилось проверять работоспособность своей программы на Ubuntu и скрипт её запуска был таким:

bhyve -m 2G -w -H \
    -s 0,hostbridge \
    -s 4,ahci-hd,/dev/zvol/zroot/ubuntu \
    -s 5,virtio-net,tap0 \
    -s 29,fbuf,tcp=127.0.0.1:5900,w=800,h=600,wait \
    -s 30,xhci,tablet \
    -s 31,lpc -l com1,stdio \
    -l bootrom,/usr/local/share/uefi-firmware/BHYVE_UEFI.fd ubuntu
bhyvectl --destroy --vm=ubuntu


UEFI firmware позволяет загрузить UEFI-aware загрузчики, которых всё больше и больше. Вынужден запускать свой ноутбук в UEFI режиме тоже, так как иначе не получу графику с ускорением и XrandR, кроме VESA режима. К сожалению, по IPv6 VNC видео у меня не получилось завести.

Демоны



Почти все демоны у меня запускаются под управлением daemontools. Перезапуск при падениях, посылка сигналов, отслеживание PID-ов, ограничения ресурсов (CPU, память), управление правами доступа — всё автоматом будет из коробки, за счёт простейших программ на Си.

Например lighttpd (web-сервер используемый мною всю жизнь) может уйти в бесконечный цикл, сжигающий CPU — хочется его прибить после этого. Плюс выплёвывает свой журнал в stderr, который я бы не прочь перехватить и записать в журнал. Хочу ещё и конфигурационный файл его проверить перед запуском? А журнал писать под другим пользователем, ещё и ротируя и добавляя временной штамп?

----- /var/service/lighttpd/run -----
#!/bin/sh -e
cfg=/home/lighttpd/etc/lighttpd.conf
cmd=/home/lighttpd/lighttpd/sbin/lighttpd
exec 2>&1
$cmd -f $cfg -tt
exec softlimit -t 300 $cmd -D -f $cfg


----- /var/service/lighttpd/log/run -----
#!/bin/sh -e
exec setuidgid lighttpd multilog t ./main


Из-за того, что inetd не умеет слушать на разных адресах разными службами, то и он заменён на UCSPI-TCP совместимые программы, запускаемые тоже под daemontools. В общем случае, inetd-совместимые программы заработают без изменений и с UCSPI-TCP интерфейсом:

----- /var/service/phlog-ipv6 -----
#!/bin/sh -e
uid=`id -u lighttpd`
gid=`id -g git`
addr=`cat addr`
exec tcpserver -DRH -u $uid -g $gid -l 0 $addr gopher \
  /home/sgblog/sgblog -gopher /home/sgblog/gopher.hjson


Адрес хранится в отдельном файле, чтобы run скрипт можно было сделать символической ссылкой в разных /var/service директориях. tcpserver принимает числовые идентификаторы пользователя и группы — поэтому их приходится узнавать внешними утилитами, существенно упрощая эту программу на Си.

daemontools подход (к нему же относится и runit, s6) считаю единственно правильным из всех что видел. Именно такими должны быть системы инициализации и управления службами. Ведь ничто не мешает тут и распараллеливать запуск и делать зависимости, за счёт минималистичных простых утилит.

MTA



Как и на любой достойной Unix системе, у меня имеется локальный почтовый сервер и работающая email экосистема. Всю жизнь использовал Postfix MTA и никогда не было мысли попробовать что либо другое, разве что кроме OpenSMTPD, который проще, но его возможностей мне не хватает.

На сервере обслуживается множество почтовых доменов, рассылок через mlmmj (лучший движок из всех что встречал, жалею что не переходил на него раньше с GNU Mailman-а), движок комментариев для блога, forwarding писем на внешние почтовые ящики. Он делает задержку приглашения, запрет pipeline, проверку reverse DNS, SPF, OpenDKIM, graylisting и ряд других настроек для борьбы со спамом. Почтовый сервер на моём шлюзе является relay-ем для всех машин в сети.

Настройка Postfix после установки на всех сторонних машинах заключается только в указании где он будет слушать и кто является SMTP relay-ем:

----- /usr/local/etc/postfix/main.cf -----
inet_interfaces = localhost
relayhost = [mail2.stargrave.org]


Единственное что отличается в моей почтовой экосистеме от большинства других: почта между ноутбуком и сервером ходит не по SMTP протоколу, а через NNCP транспорт.

----- /usr/local/etc/postfix/master.cf -----
nncp      unix  -       n       n       -       -       pipe
  flags=Fqhu user=stargrave argv=nncp-exec -quiet $nexthop sendmail $recipient


----- /usr/local/etc/postfix/main.cf -----
default_transport = nncp:gw


----- /usr/local/etc/nncp.hjson -----
neigh: { gw: { ..., exec: { sendmail: ["/usr/local/etc/nncp-sendmail.sh"] } } }


----- /usr/local/etc/nncp-sendmail.sh -----
#!/bin/sh -e
tmp=`mktemp`
trap "rm -f $tmp" HUP PIPE INT QUIT TERM EXIT
cat > $tmp
sendmail -f "`reformail -x Return-Path: < $tmp`" $@ < $tmp


Всё это заставляет отправлять почту через вызов nncp-exec утилиты, которая через store-and-forward или online (если есть сетевая доступность) методы доставки передаст зашифрованное сжатое письмо (ещё и трафик существенно экономя, в отличии от простого SMTP!). Приём почтовой корреспонденции происходит вызовом sendmail команды из nncp-toss, обрабатывающего входящие NNCP пакеты.

nncp-sendmail.sh нужен для добавления корректного Return-Path поля, иначе, из-за особенностей sendmail вызова, эта информация будет потеряна. Эта метаинформация ценна, как минимум, тем, что в ней содержатся знания о номерах сообщений в почтовых рассылках, чтобы потом на них можно было сослаться или скачать (например Return-Path: <dev+bounces-29073-XXX=stargrave.org@suckless.org>).

До NNCP использовал связку из UUCP и SSH (для безопасного соединения между UUCP демонами). А ещё прежде — обычный SMTP, в надежде что у меня не будут возникать достаточно длительные timeout-ы недоступности между компьютерами и почта не будет затёрта. При этом руками вызывал команды форсированной попытки доставки корреспонденции, когда появляется сетевая связанность между ноутбуком и почтовым сервером. UUCP решал проблему timeout-ов. NNCP делает это ещё проще.

Но никогда в жизни не поднимал POP3 или IMAP4 сервера для того, чтобы забирать корреспонденцию — это удел неполноценных не-Unix систем. Только SMTP серверы доставляющие другу другу корреспонденцию в почтовые ящики, пускай и не только через TCP транспорты!

Но для рабочей почты, которая должна идти через рабочие сервера (к сожалению, и забираться через POP3), есть настройки в локальном Postfix чтобы она шла уже не через NNCP транспорт с моим relay-ем:

----- /usr/local/etc/postfix/main.cf -----
sender_dependent_relayhost_maps = hash:/usr/local/etc/postfix/relayhost_maps
sender_dependent_default_transport_maps = hash:/usr/local/etc/postfix/transport_maps
smtp_sasl_auth_enable = yes
smtp_sasl_security_options = noanonymous
smtp_sasl_password_maps = hash:/usr/local/etc/postfix/sasl_passwd
smtp_use_tls = yes


----- /usr/local/etc/postfix/transport_maps -----
stargrave@arbeit.ru smtp


----- /usr/local/etc/postfix/relayhost_maps -----
stargrave@arbeit.ru [mail.arbeit.ru]:587


MUA



Раз уж упомянул и MTA и MRA и MDA, то перейду к MUA. Использую Mutt, хотя прежде был знаком продолжительно с Heirloom mailx и его более продвинутой S-nail версией. Главная придирка к S-nail: отсутствие PGP-MIME поддержки. У S-nail хорошая поддержка threading-а и почтовых рассылок (Mail-Followup-To заголовок), чем не могут похвастаться даже монструозные GUI почтовые клиенты. Mutt остаётся до сих пор непревзойдённым средством работы с почтой, неспроста используемым при разработке Linux.

После MDA почта сохраняется в директориях в Maildir формате. С ними можно работать атомарно и надёжно, в отличии от монофайлов mbox.

----- ~/.mutt/muttrc -----
set folder = "~/mail"
set mbox_type = maildir
set spoolfile = =mbox
set record = =sent-`date "+%y-%m"`
set postponed = =postponed
set certificate_file = ~/.mutt/certificates
set net_inc = 1
set edit_headers = yes
set charset = "utf-8"
set pager_stop = yes
set sort = "threads"
set sort_aux = "last-date-received"
set pager_index_lines = 6
set timeout = 60
set rfc2047_parameters
set reply_to = yes
set fcc_clear = yes
set user_agent = yes
set signature = ~/.signature
set hostname = "stargrave.org"
set realname = "Sergey Matveev"
set query_command="mu cfind --format=mutt-ab '%s'"
set forward_format = "Fwd: %s"
set indent_string = ">"
set attribution = "*** %n [%{%Y-%m-%d %H:%M}]:"
set index_format = "%4C %Z[%1H] %{%m-%d} [%N] %-15.15L (%?l?%4l&%4c?)%?M?[#%02M]?%?Y?[%Y]? %s"
set pipe_decode = yes
set crypt_protected_headers_write = yes
set print_command="cat > /tmp/mail-printed"

macro index <F1> "<next-unread-mailbox><enter>" "Go to new mail"
macro index <F5> ":set mbox_type = mbox\n" "mbox mailbox type"

macro index <F8> "<shell-escape>mu find --clearlinks --format=links --linksdir=~/mail/search " "mu find"
macro index <F9> "<change-folder-readonly>~/mail/search<enter>" "mu find results"

macro pager \cu |urlview\n

macro index \cb <decrypt-copy>
macro pager \cb <decrypt-copy>

macro index \ce <next-entry>
macro index \cy <previous-entry>
macro pager \ce <next-line>
macro pager \cy <previous-line>

alternative_order text/plain text/enriched text/html
auto_view text/html
unignore \
    Mail-Followup-To \
    List-Id \
    Reply-To \
    Mail-Reply-To \
    Organization \
    OpenPGP \
    Return-Path \
    X-Mailer \
    User-Agent \
    Message-Id

fcc-hook .* $record
folder-hook . "set sort = threads; push <collapse-all>"

source ~/.mutt/mailboxes
source ~/.mutt/accounts/rc

# set mime_forward = yes
# set mime_forward_rest = yes


  • set record позволяет исходящую почту автоматически сохранять в почтовых ящиках побитых по месяцам.
  • set edit_headers разрешает редактировать заголовки писем. Нередко бывает нужно что-то подправить, как минимум выставить Reply-To.
  • set sort и set sort_aux задают сортировку писем по умолчанию: по тредам и датам.
  • set rfc2047_parameters к сожалению нужен из-за большого количества не соблюдающих стандарты MUA. Позволяет декодировать не английские имена файлов в MIME частях.
  • set reply_to заставляет использовать адрес из Reply-To при простом (не в рассылку) ответе, а не брать его из From. Это просто корректное поведение по умолчанию.
  • set fcc_clear заставляет хранить исходящие зашифрованные сообщения в открытом виде в почтовом ящике. Так как почта располагается на зашифрованном разделе, то меня это устраивает в плане безопасности. Плюс это позволяет использовать индексаторы, которые бы были слепы к телу зашифрованных сообщений.
  • set query_command указывает команду поиска почтовых адресов. Индексирую почту Mu helper утилитами, у которых есть возможность поиска и вывода в Mutt-дружелюбном формате. Когда нужно найти почтовый адрес, то ввожу его искомую часть, нажимаю Ctrl-T и получаю список предложений.
  • set attribution указывает формат строки идущей перед цитируемым телом сообщения: разделитель, имя и дата. Считаю важным чтобы не было надписей на не английском языке, ибо представляю как мне бы, на своём родном, ответил китаец и я бы даже примерно не знал что там указано: дата или какое-то ругательство? Тоже самое относится и к From полю, которое на кириллице не каждый иностранец знает как прочитать (попробуйте дифференцировать китайские имена).
  • set crypt_protected_headers_write помещает заголовки типа Subject в зашифрованное тело PGP-MIME сообщения. Относительно недавно появившаяся поддержка, не каждый MUA сможет автоматически декодировать и понять эти почтовые заголовки.
  • Нажатие F1 прыгает на ближайшее непрочитанное сообщение, включая сообщения из других почтовых ящиков. Основное что нажимаю для чтения всей прошедшей корреспонденции.
  • Нажатие F8 позволяет мне вбить поисковый запрос для mu утилиты, результат поиска которой будет оформлен в виде символических ссылок в директории (почтового ящика) до найденных писем. А F9 переключит меня в этот почтовый ящик. Нередко использую встроенные возможности поиска/фильтрации писем самого Mutt, но они всё равно не сравнятся с гибкостью и, главное, скоростью проиндексированной базы данных. Полный архив всей корреспонденции прошедшей через мой почтовый сервер (не рабочий) содержит вот-вот почти уже как миллион писем.
  • set pipe_decode декодирует MIME сообщения перед их подачей во внешнюю команду через pipe. Учитывая количество кириллических писем с которыми работаю — это нужно всегда.
  • fcc-hook автоматически сохраняет копии писем в исходящий почтовый ящик.
  • folder-hook… collapse-all включает сортировку по тредам после смены почтового ящики и схлопывает отображение этих тредов.
  • set mime_forward* опции заставляют делать forward сообщений в виде RFC822 MIME приложений. Нередко бывает нужно отправить несколько писем, да ещё и с вложениям, с минимальными потерями заголовков и прочего.
  • macro * \ce и \cy настройки делают bind Ctrl-Y и Ctrl-E символов, которые генерируются колесом прокрутки трэкбола, к перемещению по строкам и письмам пейджера.
  • macro * \cb делает bind Ctrl-B символа к команде создания дешифрованной копии письма. Это использую чтобы иметь дешифрованные копии на зашифрованном разделе файловой системе и иметь возможность индексировать их содержимое.


----- ~/.mutt/mailboxes -----
mailboxes =mbox \
    =arbeit \
    =blog-comment \
    [...]
    =zsh

folder-hook =blog-comment "set sort = date"
folder-hook =monitoring "set sort = date"

set my_month_prev = `date -j -v -1m "+%y-%m"`
set my_month_curr = `date "+%y-%m"`

mailboxes \
    =arbeit-$my_month_prev \
    =arbeit-$my_month_curr \
    =sent-$my_month_prev \
    =sent-$my_month_curr \
    =back-$my_month_prev \
    =back-$my_month_curr

folder-hook =sent-$my_month_prev "set sort = date-sent"
folder-hook =sent-$my_month_curr "set sort = date-sent"


Тут перечисление всех меня интересующих почтовых ящиков и их настроек. В mailboxes ящики можно быстро переключиться y командой. Все архивные ящики у меня делятся на «вся история включительно до прошлого месяцы» и на «текущий месяц». Плюс для исходящих писем, мониторинга мне нужна сортировка по дате.

Далее всякие учётные записи:

----- ~/.mutt/accounts/rc -----
macro index <F2> ":source ~/.mutt/accounts/stargrave.org\n" "Profile: stargrave.org"
macro index <F3> ":source ~/.mutt/accounts/arbeit.ru\n" "Profile: arbeit.ru"
macro index <F4> ":source ~/.mutt/accounts/XXX.net\n" "Profile: XXX.net"

alternates stargrave@stargrave.org junk@stargrave.org admin@cypherpunks.ru [...]

source ~/.mutt/accounts/stargrave.org


----- ~/.mutt/accounts/stargrave.org -----
source ~/.mutt/accounts/clearing
set from = "Sergey Matveev <stargrave@stargrave.org>"
my_hdr OpenPGP: id=AE1A8109E49857EF\; url=http://openpgpkey.stargrave.org/.well-known/...

set pgp_autosign = no
set pgp_sign_as = 0x6B350BA5


----- ~/.mutt/accounts/arbeit.ru -----
source ~/.mutt/accounts/clearing
set from = "Sergey Matveev <stargrave@arbeit.ru>"
set signature = ~/.mutt/accounts/arbeit.signature
set smtp_url = "smtp://localhost/"

alternates stargrave@arbeit.ru
my_hdr OpenPGP: id=...
my_hdr Organization: My lovely work

set pgp_autosign = yes
set pgp_sign_as = 0xXXX

mailboxes imaps://stargrave@mail.arbeit.ru/


----- ~/.mutt/accounts/clearing -----
unmy_hdr Organization OpenPGP
set pgp_autosign = no
unset smtp_url
unset smtp_pass
set signature = ~/.signature


В ~/.mutt/colour находятся цветовые настройки. Насколько помню, как взял какую-то default тему из штатной поставки, то так её и не менял ни разу. Разве что в регулярное выражение подсветки URL добавлял Gopher протокол, ведь, даже у меня, кроме блога есть и phlog:

----- ~/.mutt/colour -----
color body brightgreen default "(http|https|ftp|news|telnet|finger|gopher)://[^ \"\t\r\n]*"


В ~/.mutt/lists находятся знания обо всех почтовых рассылках:

----- ~/.mutt/lists -----
lists goredo-devel@lists.cypherpunks.ru
lists uucp-general@gnu.org
[...]
lists zsh-announce@zsh.org

subscribe goredo-devel@lists.cypherpunks.ru
subscribe uucp-general@gnu.org
[...]
subscribe zsh-workers@zsh.org

lists locals@arbeit.ru
subscribe locals@arbeit.ru


Знание о рассылке (lists) нужно для корректного отображения статуса письма. А знание о том, являюсь ли я подписчиком (subscribe), необходимо для корректного генерирования Mail-Followup-To заголовка при ответе в рассылку.

~/.mutt/gpg.rc содержит полную копию gpg.rc файла с описанием команд вызова GnuPG из дистрибутива Mutt. Но ко всем командам добавлена $my_keyrings переменная:

set my_keyrings = "--keyring ~/.gnupg/arbeit.kbx --keyring ~/keyrings/mein.kbx ..."


обеспечивающая поиск ключей в множестве ключниц.

Для индексации почты, как уже писал, используется mu. Важным открытием для меня было то, что при генерировании базы данных, делается огромное количество fsync вызовов, существенно понижая производительность ФС даже на SSD. Поэтому делаю ежемесячное индексирование в /tmp (tmpfs): mu index --muhome /tmp/mu, а дальше уже перемещаю готовые файлы БД на диск. Почтовые ящики с непрочитанными сообщениями рассылок не индексирую и поэтому создаю .noindex флаговый файл в них.

Mutt не держу постоянно включённым. Как же узнаю о новых письмах? О появлении писем вне почтовых рассылок и рабочих мне напоминает zsh:

----- ~/.zshrc -----
mailpath=(
    ~/mail/mbox"?Neue Nachrichten in =mbox"
    ~/mail/arbeit"?Neue Nachrichten in =arbeit"
)


Да, по умолчанию использую немецкий язык на всей технике (не только компьютерах).

----- ~/.login_conf -----
me:charset=UTF-8:lang=de_DE.UTF-8:


Общее состояние свои почтовых ящиков узнаю вызывая inc команду:

----- ~/bin/inc -----
#!/usr/bin/env zsh
setopt EXTENDED_GLOB
res=()
cd $MAILDIR
for mbox (mbox *~mbox(/)) {
    [[ -e $mbox/.inc ]] || continue
    news=($mbox/new/*(N))
    [[ ${#news} != 0 ]] && res=($res $mbox:${#news})
}
print $res


Она выводит сводку о количестве новых (new директория в Maildir) писем в почтовых ящиках с .inc флаговым файлом:

mbox:1 cryptome:10 fbsd:2 nbsd:1 obsd:2


После просмотра почтового ящика, Mutt перемещает письма в cur директорию Maildir-а и из inc-сводки они пропадают. Состояние почтовых ящиков проверяю часто, поэтому саму команду inc команду руками не ввожу, а просто нажимаю F1 в zsh:

----- ~/.zshrc -----
bindkey -s "^[OP" " inc\n" # F1


Пробел перед командой неспроста. Он говорит, что команда не попадёт в историю. Почему бы не добавить отображение состояния почтовых ящиков в statusbar dwm? Пробовал — отвлекает, постоянно туда бросаю взгляд.

Открытие рабочего почтового ящика и исходящего делается arr и sent алиасами:

----- ~/.zshrc -----
alias arr="mutt -f =arbeit -e 'source ~/.mutt/accounts/arbeit.ru'"
alias sent="mutt -f =sent-\`date '+%y-%m'\`"


К сожалению, сейчас у меня нет примера использования scoring функционала и раскрашивания списка сообщений. На прошлой работе могло приходить под полсотни писем за час от Redmine: многочисленные изменения состояний, комментарии, разные приоритеты и подпроекты. Нужно было шустро и удобно ориентироваться среди крайне важной и совершенно не интересной информации. Для этого MDA пропускал рабочую почту через Perl скрипты, анализирующие метаинформацию, выставляемую Redmine-ом, и добавляющие заголовки по которым Mutt присваивал соответствующий score и цвета для сообщений.

WWW



Последнее что не отметил в Mutt конфиге — строчка с bind-ом вызова pipe сообщения в urlview команду при нажатии Ctrl-U в режиме пейджера. Часто хочется открыть URL указанный в письме. Выделять мышкой? Не вариант — долго. Иметь такую возможность встроенную в эмулятор терминала? Overengineering. urlview находит всё что похоже на URL и показывает меню с выбором найденных URL. При выборе запускается команда:

----- ~/.urlview -----
COMMAND /home/stargrave/bin/www %s &


----- ~/bin/www -----
#!/bin/sh

[ -e /tmp/stargrave-flags/WG ] && [ -n "$1" ] && {
    exec $HOME/local/bin/xombrero -n "$1" 2>/dev/null
}

term() {
    DISPLAY=:0 CMD="tmux attach-session -t www" CMDTITLE="Terminal9" \
        $HOME/bin/dwm-term &
}

if tmux has-session -t www 2> /dev/null ; then
    attached=`tmux list-sessions -F "#{session_attached}" -f "#{==:#{session_name},www}"`
    [ "$attached" != "0" ] || {
        term &
        sleep 0.5
    }
    tmux new-window -t www "lynx $@"
else
    tmux new-session -d -s www "sleep 0.5 ; lynx $@"
    tmux set-option -t www default-command lynx
    term &
fi


Если выставлен глобальный WG флаговый файл, то запускается xombrero GUI броузер. А если он уже запущен, то в нём открывается tab с указанным URL-ом. В противном случае запускается эмулятор терминала, внутри него tmux с www сессией. Эмулятор имеет название Terminal9 и поэтому, как и Xombrero, будет автоматом dwm-ом помещён на 9-ый рабочий стол, где у меня по привычке находятся все броузеры. Если tmux сессия www уже существует, то в ней создаётся новое окно. Новые окна в этом tmux автоматически запускают lynx. В итоге у меня multi-tab броузер получается. Если же мне хочется запустить этот www без urlview, то просто вызываю www команду через dmenu.

Да, большую часть времени, в качестве броузера, использую lynx, ибо не тормозит и удобно. Документацию в HTML формате (например Python), в Lynx очень удобно смотреть и быстро перемещаться по пронумерованным ссылкам. Необходимо включить advanced mode, выставить UTF-8 как предпочтительную кодировку, включить vi-режим клавиш. Чтобы работало колесо прокрутки, делаю bind клавиш:

----- ~/.lynx.cfg -----
KEYMAP:^E:DOWN_TWO
KEYMAP:^Y:UP_TWO


По умолчанию, в Lynx картинки не являются ссылками и их нельзя просмотреть. Это можно включить в меню, но опция не сохраняется перманентно на жёсткий диск. Не знаю почему так решили. Но благо это можно сделать в главном конфиге:

----- ~/.lynx.cfg -----
MAKE_LINKS_FOR_ALL_IMAGES:TRUE


В файле закладок (~/.lynx_bookmarks.html) у меня список ссылок на различную документацию по программам, сайты погоды и подобное. Этот файл и в Xombrero является домашней страницей. Запустив любой из этих двух броузеров я одним переходом по ссылке сразу могу открыть Python, PostgreSQL, whatever документацию, конечно же, хранящуюся на жёстком диске (не переставать же мне работать когда нет доступности к Интернету или сайт проекта уберёт документацию?).

----- ~/.zshenv -----
export WWW_HOME=file:///home/stargrave/.lynx_bookmarks.html
export SSL_CERT_FILE=/usr/local/openssl/cert.pem
export LYNX_CFG=~/.lynx.cfg


Директория ~/doc с документацией, книгами и статьями у меня находится на ZFS dataset с максимальным gzip сжатием (моя версия ZFS не поддерживает Zstandard), ибо сжимается хорошо, и в основном работает только на read-only. Хорошо пожатая документация у меня занимает уже около гигабайта на данный момент. Если документация есть только в online виде, то полностью зеркалирую сайт (часть с документацией) через wget --mirror (об этом ниже). Софт без документации — не может быть использован. Поэтому ничего хорошего не могу сказать про nginx, который не прикладывает документацию в свои tarball и на сайте есть только для самой последней версии online. Это недопустимое наплевательское отношение к пользователю.

Приём cookie у меня разрешён только с моего разрешения. Некоторым сайтам всегда отказываю в них:

----- ~/.lynx.cfg -----
INCLUDE:/home/stargrave/.lynx_cookies.cfg


----- ~/.lynx_cookies.cfg -----
COOKIE_REJECT_DOMAINS:wikipedia.org,en.wikipedia.org,ru.wikipedia.org,github.com,...


В Lynx есть jump файл, позволяющий задавать шаблоны ссылок.

----- ~/.lynx.cfg -----
JUMPFILE:/home/stargrave/.lynx_jumps.html


----- ~/.lynx_jumps.html -----
<html>
<head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8">
</head>
<body>
<dl compact>
<dt>gg<dd><a href="https://www.google.com/search?q=%s">Google</a></dd>
<dt>py<dd><a href="https://pypi.org/project/%s/">PyPI</a></dd>
<dt>rfc<dd><a href="https://tools.ietf.org/html/rfc%s">RFC</a></dd>
<dt>s<dd><a href="https://lite.duckduckgo.com/lite/?q=%s">DuckDuckGo</a></dd>
<dt>we<dd><a href="https://en.wikipedia.org/wiki/%s">WikiEN</a></dd>
<dt>wr<dd><a href="https://ru.wikipedia.org/wiki/%s">WikiRU</a></dd>
</dl>
</body>
</html>


Нажав J вас спросят какой jump мы хотим сделать. Нажимаем s, Enter, далее вводим поисковый запрос, Enter, попадаем на результат поиска в DuckDuckGo. Хочется прочитать RFC 1234? J, rfc, Enter, 1234, Enter. Русскоязычная статья Wikipedia? J, wr, Enter, Хабр, Enter. В Xombrero или Pentadactyl плагине для Firefox аналогичное можно было делать не нажимая Enter, а ставя пробел, но функционал аналогичен.

Хочется чтобы в tmux окне был заголовок более вменяемый чем голый lynx. Хочется title HTML страницы на которой мы сейчас находимся, как это показывается в tab-ах GUI броузеров. Из коробки этого сделать нельзя, но подправил исходный код Lynx чтобы при загрузке страницы он выводил escape последовательность выставляющую имя окна (об этом подробнее ниже). Ещё пришлось подправить его код для форсированной перерисовки окна при изменении размера.

Xombrero броузер мне приглянулся много лет назад. Когда-то и Firefox был достаточно вменяем в плане приватности и безопасности и в нём работали Pentadactyl must-have расширение. Потом Firefox стал ломать свой API и разработчики плагинов махнули рукой. Уже давно Firefox вообще мной не рассматривается. Xombrero имеет удобнейшее vi-like управление, быстрый переход по номерам ссылок (можно сказать он включает в себя основные фичи Pentadactyl), кучу privacy-securing фич, возможность использовать внешний редактор для полей, X.509 certificate pinning, управление HSTS, возможность отключать изображения (по умолчанию их не гружу), отключать JavaScript или разрешать скрипты только с заданных доменов (JavaScript включаю только для локальных рабочих ресурсов, в Интернет с ним не хожу), переключатель CSS для тёмного режима (чтобы белый фон не бил по глазам), быстрый поиск (как jump в Lynx) и чего-то ещё, что не вспомню сразу. Но автор его забросил, WebKit движок очень стар и всё больше сайтов в нём даже отображаться (CSS) не могут хоть сколько-то вменяемо.

Кстати, почему Lynx, а не Links, Links2, ELinks, w3m? Привычка к его типу управлению. Links не умеет Gopher. ELinks — слишком избыточен, мне не нужны CSS и другие его возможности. w3m не нравится управлением, да и он совсем уж слишком прост по возможностям (Lynx на его фоне выглядит распухшим монстром).

Однако, лучшего способа для рендеринга Unicode текстовых таблиц, чем просто их отображение в w3m не находил. Как минимум он корректно всё масштабирует, выравнивает внутри таблиц, учитывает rowspan/colspan, center и border=1. Поэтому w3m у меня в системе стоит, но только для рендеринга таблиц: w3m [-cols XX] XXX.html > dump.

PGP



В Mutt было упомянуто GnuPG и OpenPGP использование. Не смотря на архаичность во многих местах, даже близко нет никакой замены этой экосистеме и этим программам. Разве что только OpenBSD-шный signify использовать для подписи файлов дистрибутивов. Хотя, по хорошему, сами публичные ключи для signify бы подписывать OpenPGP ключами.

Никаких хитрых применений GnuPG у меня нет. В почте используется только PGP-MIME, ибо clearsign PGP легко может быть исковеркан почтовым софтом, особенно когда речь не про ASCII данные (кириллица). А дальше это подпись и шифрование файлов. Во время шифрования не забываю указывать -z 0 чтобы отключить компрессию данных. Если компрессия нужна, то использую внешний zstd вызов.

Я не пользуюсь ключевыми серверами, так как мне не нравится то, что не имею полного контроля над ключом там находящимся. Например я удалил UID, изменил предпочтения алгоритмов — добавляется дополнительный подписанный пакет. А старые, уже неактуальные данные, предпочёл бы не передавать за ненадобностью. Плюс люди могут добавить непойми откуда взявшуюся подпись к моему ключу — мне это не нравится. Ведь уже же был прецедент с «отравлением» публичных ключевых серверов. Публичные ключи публикую через DANE (DNS) и WKD (особые HTTPS URL-ы).

----- ~/.gnupg/gpg.conf -----
charset utf-8
no-auto-key-retrieve
keyserver hkps://hkps.pool.sks-keyservers.net
no-secmem-warning
no-greeting
no-emit-version
default-key AE1A8109E49857EF
keyid-format 0xlong
photo-viewer sxiv %i

trust-model tofu+pgp
auto-key-locate dane wkd hkps://hkps.pool.sks-keyservers.net local

aead-algo OCB
personal-aead-preferences OCB EAX
personal-cipher-preferences TWOFISH AES256 CAMELLIA256 \
    AES192 CAMELLIA192 AES CAMELLIA128 CAST5 BLOWFISH IDEA 3DES
personal-digest-preferences SHA512 SHA256 RIPEMD160
personal-compress-preferences ZLIB ZIP Uncompressed
default-preference-list [всё перечисленное выше]
cert-digest-algo SHA512

group myself=AE1A8109E49857EF


----- ~/.zshrc -----
export GPG_TTY=$(tty)


TOFU (trust on first use) считаю полезной идеей. Хотя теперь, кроме ключей и уровней доверия к ним, нужно в GnuPG ещё и состояние TOFU хранить, сейчас у меня занимающее около трёх мегабайт. Недавно добавленный AEAD режим работы OCB даёт прирост скорости на десятки процентов.

Мой текущий основной ключ использует DSA-3072 ключ и RSA-4096 подключи. Ключи для подписи tarball-ов с исходным кодом, как правило, используют RSA-2048. Сейчас бы не стал так делать и использовал либо ed25519/curve25519, либо ed448/cv448 — они значительно быстрее, надёжнее и компактнее. Впрочем, за безопасность RSA/DSA ключей таких размеров не переживаю и поэтому не бегу сломя голову их перегенерировать. Да и жалко терять кучу подписей на основном ключе, среди которых есть подпись даже самого Ричарда Столлмана, с которым было устроил вечеринку подписи ключей (keysigning party, KSP).

Я разделяю ключи по нескольким ключницам: отдельная для ключей связанных с работой, отдельная для людей участвовавших в KSP, отдельная для ключей подписи различного софта. Плюс отдельные ключницы выкладываемые проектами типа Debian или GNU — ключи в которых никак не трогаю. В ключницах под моим управлением на данный момент 332 ключа. И большая часть email переписки идёт в зашифрованном виде.

В конфиге GnuPG видно как явно указываю команду для просмотра фотографий. А как Lynx понимал что надо запускать для просмотра картинок? А Mutt? Все они смотрят в ~/.mailcap файл, который у меня совсем небольшой:

----- ~/.mailcap -----
text/html; /usr/local/bin/lynx -noreferer -partial \
    -assume_charset=%{charset} -dump %s; copiousoutput; nametemplate=%s.html
application/pdf; /home/stargrave/bin/zat '%s'; test=test -n "$DISPLAY"
image/gif; /usr/local/bin/sxiv '%s'; test=test -n "$DISPLAY"
image/jpeg; /usr/local/bin/sxiv '%s'; test=test -n "$DISPLAY"
image/png; /usr/local/bin/sxiv '%s'; test=test -n "$DISPLAY"
image/webp; /usr/local/bin/sxiv '%s'; test=test -n "$DISPLAY"


За счёт text/html, Mutt может рендерить чёртовы HTML письма. После которых, если не проигнорирую, прошу человека уважать окружающих и не присылать их более. Mutt-у также предоставляю файл с подсказками о MIME типе по расширению файлов:

----- ~/.mime.types -----
application/gzip gz
application/pdf pdf
image/jpeg jpeg
image/jpeg jpg
image/png png
image/webp webp


IRC



Асинхронное общение по почте рассмотрел, web-броузеры тоже. Синхронное общение пробовал за свою жизнь всякое: mICQ для ICQ, MCabber и тьму других XMPP клиентов, PSYC, Tox. И конечно же IRC. На сегодняшний день до сих пор не вижу ничего более достойного чем IRC для синхронного общения. Его возможностей вполне достаточно, простота протокола убийственна. Главное не забывать для каких задач синхронное общение имеет смысл и не хотеть странного.

На одной работе использовался Slack. На другой Mattermost. Везде в них ходил через IRC-мост. Для Mattermost это matterircd. Да и в XMPP ходил последние годы через BitlBee, тоже являющийся IRC-мостом. Сейчас у меня нет XMPP клиента, ибо из-за смартфонов люди переехали в цензурируемые централизованные закрытые экосистемы, где даже приватно/конфиденциально не пообщаться. В IRC (и при использовании мостов) можно хотя бы OTR натянуть поверх. Задачи синхронного общения на работе настолько просты, что даже простые IRC сервера из одного демона подходят и легко поднимаются (мой goircd даже конфигов не имеет).

Использую irssi клиент. Совершенно банальный конфиг:

----- ~/.irssi/config -----
aliases = { wc = "window close"; qmsg = "quote privmsg $0 :$1-"; };
settings = {
  "fe-text" = {
    actlist_sort = "refnum";
    paste_join_multiline = "no";
    paste_detect_time = "0";
    paste_use_bracketed_mode = "yes";
  };
  "fe-common/core" = {
    autolog = "yes";
    beep_msg_level = "MSGS NOTICES INVITES DCC DCCMSGS HILIGHT PUBLIC";
    autolog_path = "~/secure/irclogs/$tag/$0.log";
    beep_when_window_active = "yes";
    hilight_nick_matches_everywhere = "yes";
  };
  core = {
    real_name = "Sergey Matveev";
    user_name = "stargrave";
    nick = "stargrave";
    timestamp_format = "%H:%M:%S";
  };
};
logs = { };
servers = (...);
chatnets = {...};
hilights = ( { text = "stargrave"; nick = "yes"; word = "yes"; } );
keyboard = (
  { key = "^Y"; id = "command"; data = "SCROLLBACK GOTO -2"; },
  { key = "^E"; id = "command"; data = "SCROLLBACK GOTO +2"; },
  { key = "^L"; id = "command"; data = "WINDOW LAST"; },
);


----- ~/.zshenv -----
export IRCNAME="Sergey Matveev"
export IRCNICK="stargrave"


Можно отметить только Ctrl-L комбинацию, перемещающую на предыдущее активное окно, и на Ctrl-Y/E binding-и для прокрутки истории колесом трэкбола. Обязательно включение beep сигнала когда какое-либо окно становится активным (там написали сообщение) (об этом ниже). А qmsg позволяет отправить сообщение, но без его отображения в окне, например для отправки пароля во время логина в matterircd или данных NickServ боту.

Использую свежую версию irssi, в которой из коробки встроен OTR плагин, позволяющий поверх любых текстовых сообщений организовать безопасный, зашифрованный, аутентифицированый end-to-end канал связи, к тому же ещё и возможностью отрицания авторства над сообщениями. OTRv3 использует немного архаичные криптографические примитивы, но для безопасности они достаточны, а производительность не сильно критична, так как это канал для обмена человеческими сообщениями (редкими и небольшими). До сих пор OTRv3 остаётся хорошим вариантом, особенно учитывая его широкую поддержку в софте.

В ~/.irssi/scripts/autorun стоят следующие скрипты:

  • go.pl позволяет /go командой переходить в окна где встречена часть указанного названия.
  • logresume.pl при открытии окна загружает небольшой кусок истории общения, напоминая о чём был разговор в прошлый раз.
  • nickcolor.pl разукрашивает nick-и, позволяя быстрее ориентироваться среди сообщений от разных людей.


На работе для синхронного общения используется Mattermost. Скачивать передаваемые в нём файлы через IRC не выйдет, так как там присылаются только ссылки на API-driven ресурсы. Благо можно было прямо на коленке написать утилиту для скачивания файла по этому URL:

----- ~/work/mmfileget/main.go -----
import "github.com/mattermost/mattermost-server/v5/model"
[...]
Client := model.NewAPIv4Client("https://mm.arbeit.ru")
Client.Login("stargrave", "password")
info, resp := Client.GetFileInfo(fileId)
if info == nil {
    fmt.Fprintln(os.Stderr, resp)
    os.Exit(1)
}
filename := info.Name
if len(os.Args) > 2 {
    filename = os.Args[2]
} else {
    fmt.Fprintln(os.Stderr, "Name:", info.Name)
    fmt.Fprintln(os.Stderr, "Type:", info.MimeType)
    fmt.Fprintln(os.Stderr, "Size:", info.Size)
    fmt.Fprintln(os.Stderr, "Download? Ctrl-D/C")
    os.Stdin.Read(make([]byte, 1))
}
data, _ = Client.GetFile(fileId)
if err = ioutil.WriteFile(filename, data, os.FileMode(0666)); err != nil {
    panic(err)
}


st



Возвращаюсь к процессу запуска X11 сессии. После запуска dwm могу либо открыть новый эмулятор терминала (Alt-Shift-Enter), либо dmenu (Alt-P) в котором вызвать другую программу, что, как правило, либо www для запуска терминала+tmux+Lynx, либо xombrero GUI броузер, либо парольный менеджер passman, либо блокировку экрана mylock.

Уйму лет использую эмулятор терминала st (suckless terminal). В нём есть всё что мне нужно и ничего лишнего. Resize, UTF-8, ANSI последовательности, поддержка разных буферов обмена X11, выделение и вставка текста, выставление urgency свойства окна при ловле bell/alert символов, CSI коды (выставление title окна, общение с буфером обмена), bracketed paste режим, поддержка alternate screen. В его конфигурационном файле вижу ещё какие-то фишки, ни разу не использованные.

Один раз, когда забыл зарядку от ноутбука и поработал за другим терминалом, я даже не подозревал как много мелочей поведения в терминалах. Кто-то подсвечивает выделение символа прямо под курсором, а кто-то нет. Где-то двойной клик выделяет слово, а где-то эти комбинации ведут себя иначе. Кто-то включает перевод строки при выделении мультистрочного текста, а кто-то нет. И эти мелочи очень больно бьют по рукам с непривычки.

Кроме настроек цветов, немного что подправлено в его конфиге:

----- ~/src/suckless/st/config.h -----
static char *font = "Inconsolata LGC:pixelsize=16:antialias=false";
static int borderpx = 0;
static char *shell = "/bin/zsh";
wchar_t *worddelimiters = L" →│";
char *termname = "screen-256color";
static unsigned int mouseshape = XC_left_ptr;

static Shortcut shortcuts[] = {
    /* mask                 keysym          function        argument */
    [...]
    { MODKEY|ShiftMask,     XK_Insert,      clippaste,      {.i =  0} },
};

static Key key[] = {
    /* keysym           mask            string      appkey appcursor */
    [...]
    { XK_Menu,          XK_NO_MOD,      "^B",    0,    0},
    { XK_Super_R,       XK_NO_MOD,      "^A",    0,    0},
    { XK_Scroll_Lock,   XK_NO_MOD,      "^A[",   0,    0},
};


  • Я пробовал всякие шрифты, но Inconsolata LGC бессменный лидер предпочтения. Среди понравившихся шрифтов мне запомнились Iosevka и Spleen. Но первый уж очень непривычен своим узким размером, а второй не знает про кириллицу.
  • В worddelimiters добавил стрелочку и вертикальную чёрточку: оба Unicode символа. Первый используется в Vim для отображения символов табуляции, коих полно в Go коде, а вторым tmux разделяет свои окна. Если бы их в этом списке не было, то двойной клик по слову захватывал бы эти символы тоже.
  • Правильно бы было использовать полноценное корректное описание терминала st, но ещё не дорос до этого и пока просто использую наиболее близкий и подходящий screen-256color.
  • Форма XC_left_ptr курсора мышки сильно заметнее на экране чем «I-beam», с которой мне приходилось подёргать мышку чтобы понять где сейчас находится курсор.
  • В shortcuts добавлен binding на Alt+Shift+Insert, вставляющий текст из «clipboard» X11 буфера обмена. Тогда как штатный Shift+Insert вставляет из primary буфера.
  • Моя небольшая гордость это binding клавиши клавиатуры Menu на посылку Ctrl-B, а правого Super (там где часто рисуется логотип Windows) на посылку Ctrl-A. Искренне ненавижу нажимание Ctrl клавиши, ибо это достаточно медленное действие для меня: мой мизинец работает так себе (поэтому в FPS играх у меня на Ctrl никогда ничего не было, в отличии от преобладающего большинства людей) и нажимать его могу только ребром ладони. Это одна из причин почему, сколько бы я не уважал Emacs, но не перешёл бы на его использование из-за ультра частого использования этой клавиши.

    Ctrl-A используется как prefix клавиша для локального tmux, тогда как Ctrl-B является prefix-ом в нём по умолчанию, а значит и на удалённых tmux-ах из коробки. Tmux возможностями пользуюсь очень часто и поэтому не вариант каждый раз для prefix-а нажимать комбинацию с Ctrl. Super+C — вот у меня и будет открыта новая вкладка в tmux. Menu+C — аналогично, но в удалённом tmux. Это было бы самоубийственное падение КПД если бы мне пришлось, как предполагается по умолчанию, посылать двойной prefix удалённому tmux: Ctrl-B+Ctrl-B+C, пять нажатий, вместо одного!
  • ScrollLock клавиша посылает Ctrl-A+[, что включает режим поиска по буферу в tmux. Почему этот binding находится вне самого конфига tmux? Если не изменяет память, то ScrollLock у меня там просто не получалось указывать. Впрочем, конкретно это нажатие использую редко.


С какой-то версии, st стал корректнее отображать/применять атрибуты к тексту (italic, underline, inverse и подобные). А у меня уже давняя привычка видеть italic текст подсвеченным жёлтым цветом. Пришлось править исходный код терминала чтобы вернуть былое привычное отображение. Жёлтая подсветка курсивного текста хорошо видна на экране. Инвертирование цвета для курсива — не вариант, так как штатно много где есть отдельная подсветка именно инвертированием. Аналогичная настройка для XTerm терминала была бы:

XTerm.vt100.colorITMode: true
XTerm.vt100.colorIT: yellow


В качестве терминала dwm запускает:

----- ~/bin/dwm-term -----
#!/bin/sh
[ -n "$CMD" ] || CMD=tmux
[ -z "$CMDTITLE" ] || CMDTITLE="-t $CMDTITLE"
exec $HOME/bin/st $@ $CMDTITLE -e $CMD 2>/dev/null


Через переменные окружения можно указать и название терминала и запускаемую команду. Что использовалось выше для запуска www скрипта. Видно, что по умолчанию используется tmux, а не просто голый запуск zsh (если нужно запустить «чистый» st+zsh, то через dmenu вызываю st).

tmux



Я не представляю жизни без tmux, который автоматически даёт буфер прокрутки, возможность поиска в нём, множество встроенных буферов обмена, tab-ы (не впиливать же этот функционал в эмулятор терминала!), возможности по скриптованию всего и вся. Конечно же и возможность потерять связь и отключиться — зачастую единственное что большинство людей использует. GNU Screen — огромный монстр (большая кодовая база), ещё и не имеющий такого множества фич.

Кроме того, tmux можно использовать для read-only показа своего терминала или совместной работы (парное программирование). Достаточно создать отдельного пользователя в системе, подсунуть нужные ключи аутентификации и при SSH логине форсированно подключать в read-only режиме к существующему tmux сокету, не забыв про корректные права доступа на него:

----- ~guest/.ssh/config -----
restrict,pty,command="tmux -S /tmp/arbeit.sock attach-session -r" \
    ssh-ed25519 ... colleague@arbeit.ru


От suckless сообщества есть предложения использования abduco и dvtm как ещё более минималистичного варианта. Но, как минимум, там нет нескольких copy-paste буферов обмена. Хотя идею уважаю, всяко лучше GNU Screеn.

А на самом деле начал использовать tmux по совершенно не связанным с вышеописанным причинам: был рабочий проект, в котором нужно запускать кучу демонов, смотреть кучу логов и прочего. Хотелось как-то автоматизировать запуск и создание всей этой кучи окон, заданных размеров, с заранее вбитыми командами активации проектов.

Например, самый часто используемый скрипт запуска сессии с четырьмя окнами: root shell доступ с четырьмя небольшими pane, окно для чтения email, окно для чтения новостей и загруженным newsboat, окно с distributed.net клиентом и когда-то там ещё было окно для музыкального проигрывателя:

----- ~/bin/start-root -----
#!/bin/zsh

paste()
{
    local pane="$1"
    local cmd="$2"
    tmux set-buffer "$cmd"
    tmux paste-buffer -t "$pane"
    tmux delete-buffer
    tmux send-keys -t "$pane" Enter
}

tmux has-session -t root && exit
tmux new-session -d -s root
tmux rename-window -t root:1 su
tmux split-window -t root:1
tmux split-window -h -t root:1
tmux split-window -h -t root:1.0

paste root:1.0 "ssh-add"
paste root:1.1 "su -"
paste root:1.2 "su -"
paste root:1.3 "su -"
tmux new-window -t root:2 -n email
tmux new-window -t root:3 -n rss
paste root:3 "newsboat"
tmux new-window -t root:4 -n dnetc
paste root:4 "cd dnetc"
tmux select-window -t root:1.0

CMD="tmux attach-session -t root" CMDTITLE="Terminal1" ~/bin/dwm-term &


Более сложных примеров на данный момент нет, так как закончил работу над проектами где требовались размашистые и сложные рабочие окружения. Зачастую хотя бы создавал девять окон, внутри которых переходил в рабочие директории. Почему девять? Потому что по ним можно быстро перемещаться prefix+номер комбинацией.

Бывает нечаянно закрою окно терминала (фокус был не там где надо). Открыть терминал и автоматически подключиться к нужной tmux сессии легко: Alt-P, пишу dwm-term-session (имя добивается tab-ом), название сессии (часто это её порядковый номер), Enter.

----- ~/bin/dwm-term-session -----
#!/bin/sh
CMD="tmux attach-session -t $@" $HOME/bin/dwm-term &


Покажу конфиг tmux частями:

----- ~/.tmux.conf -----
set-option -g mode-keys vi
set-option -g base-index 1
set-option -g default-terminal "screen-256color"
set-option -g escape-time 1
set-option -g repeat-time 0
set-option -g history-limit 20000
set-option -g renumber-windows on


  • vi-режим ввода.
  • Начало нумерации окон с единицы. Исключительно потому что ноль находится на клавиатуре в противоположном конце от 1/2/3.
  • escape-time и repeat-time «отключены» исключительно для удобства, когда tmux может сделать повторение команды без повторного ввода prefix. У меня таких use-case-ов нет, а вот зависимость поведения от времени меня раздражает.
  • renumber-windows перенумерует окна когда кто-то закрывается.


----- ~/.tmux.conf -----
set-option -g prefix C-a
unbind-key C-b
bind-key C-a send-prefix
bind-key -n C-PgUp previous-window
bind-key -n C-PgDn next-window
bind-key -n S-PgUp copy-mode -u


  • То самое изменение prefix по умолчанию. Emacs или GNU Screen не использую и поэтому Ctrl-A у меня ни с чем не коррелирует. Зато локальный и удалённый tmux используют разные префиксы.
  • Ctrl-PageUp/PageDown переключают туда-обратно tab-ы. Привычка использовать эту комбинацию у меня появилась со времён какого-нибудь urxvt.
  • Shift-PageUp входит в режим просмотра scrollback истории с переходом на экран выше. Ультра часто используемая команда, когда какой-нибудь вывод убежал за пределы экрана.


----- ~/.tmux.conf -----
bind '"' split-window -c "#{pane_current_path}"
bind % split-window -h -c "#{pane_current_path}"
bind c new-window -c "#{pane_current_path}"


Это всё штатные команды создания pane-ов и tab-ов. Отличаются только добавлением pane_current_path, чтобы новые командные оболочки находились в текущей директории, а не домашней. Это невероятно удобное поведение! Нахожусь в ~/foo/bar/baz директории, понимаю что нужно быстренько рядом выполнить какую-нибудь команду или что-то посмотреть: создаю pane и сразу же автоматом оказываюсь в этой директории.

----- ~/.tmux.conf -----
bind ] paste-buffer -p


Штатная команда вставки из буфера обмена, но с -p опцией, делающей bracketed paste. Удручает как много людей не знает про этот paste. Ужасаюсь когда в Vim отдают команду :set paste, вставляют текст, потом :set nopaste. У кого-то, в лучшем случае, есть переключатель paste режима забитый на функциональной клавише. А чаще происходит так: человек вставляет текст, ругается (потому что он всякими autoindent-ами корёжится, а не вставляется as-is), выходит из режима вставки, делает отмену, переключает paste режим, входит в режим вставки, и т.д… И это тоже не худший случай, ибо если человек перед выходом из режима вставки писал свой штатный текст, то отмена уберёт его в том числе.

Bracketed paste — это когда перед и после вставляемого текста добавляются особые escape-последовательности. Увидев их, программа понимает что сейчас происходит вставка «сырого» текста, а не штатный ввод пользователем. На моей практике редко встречался с программами непонимающими данный «способ» вставки, где человек увидит лишние escape-последовательности. Vim, zsh (подсвечивает многострочный вставленный кусок), irssi его прекрасно понимают, как и все библиотеки использующие GNU Readline или libedit.

----- ~/.tmux.conf -----
set-option -g status-style "bg=black,fg=white"
set-option -g status-left "#S>"
set-option -g status-right "#T"
set-option -g status-justify centre

set-window-option -g automatic-rename on
set-window-option -g window-status-format "#I:#W:#T#F"
set-window-option -g window-status-style "bg=green fg=black"
set-window-option -g window-status-current-format "#I:#W#F"
set-window-option -g window-status-current-style "bg=red"
set-window-option -g window-status-last-style "bg=cyan"


Настройка цветов и форматирование сообщений строки состояния. Как минимум, на tab-ах нужно различать: активное окно, предыдущее окно, окно с alert/urgency. Из коробки это не дифференцируется цветами и поэтому не могу быстро ориентироваться.

Наиболее важная опция тут — automatic-rename. Есть особые CSI escape-последовательности, которые сообщают некий title программы. Опция tmux делает автоматическое переименовывание tab в строке состояния. В tab-е активного окна показывается только название команды (например vim), а его title показывается справа в углу (на данный момент это mysetup.html). А tab неактивного окна содержит и имя программы и title (vim:mysetup.html). Это «разгружает» визуальную нагрузку на tab-ы где может быть открыто куча vim-ов с длинными именами файлов.

----- ~/.tmux.conf -----
bind-key b set-option status


Тут prefix+B используется для скрытия/отображения строки состояния tmux. Нередко бывает нужно убрать её с глаз долой, чтобы не мозолила их, отнимая целую строку пространства. Или если нужно делать снимки экрана. А Alt+b уберёт ещё и строку состояния dwm, оставляя на экране только терминал.

----- ~/.tmux.conf -----
bind-key y {
    capture-pane -J
    save-buffer /tmp/tmux-buffer
    delete-buffer
    split-window 'vim -c "set listchars=" -c "match ExtraWhitespace //" /tmp/tmux-buffer'
}
bind-key Y {
    capture-pane -J -S - -E -
    save-buffer /tmp/tmux-buffer
    delete-buffer
    split-window 'vim -c "set listchars=" -c "match ExtraWhitespace //" /tmp/tmux-buffer'
}


Prefix+y сохраняет содержимое текущего окна во временный файл, открыв новое окно с Vim-ом на этом файле. Частенько бывает нужно отредактировать текущий вывод перед глазами или как-то более удобно с ним разобраться в полноценном редакторе. Prefix+Y отличается только тем, что он захватывает всю историю текущего окна.

----- ~/.tmux.conf -----
bind-key u {
    capture-pane -J
    save-buffer /tmp/tmux-buffer
    delete-buffer
    display-popup -KE -w 100% -R "urlview /tmp/tmux-buffer"
}


Выше показывал что в Mutt, нажав Ctrl-B при просмотре сообщения, получишь urlview вывод для открытия ссылок из письма. Prefix+u в tmux делает аналогичное для всего окна. Причём отображает urlview он в popup-е: относительно новая фича, которая поверх всего текущего рисует ещё одно окно.

----- ~/.tmux.conf -----
bind-key t display-menu \
    music t "display-popup -KE -w 100% -R ~/bin/tmux-menu-music.sh" \
    pass p "display-popup -KE -R ~/bin/tmux-menu-pass-session.sh" \
    dict d "display-popup -KE -h 100% -R ~/bin/tmux-menu-dict.sh" \
    calc c "display-popup -KE -R ~/bin/tmux-menu-calc.sh" \
    cal l "display-popup -R \"cal -3N\"" \
    top o "display-popup -KE -h 100% -R \"top -s 1\""


Без этого меню теперь не могу жить. Это тоже относительно новомодная фишка tmux: показать меню (поверх текущих окон), в котором что-то выбрать и выполнить заданную команду (которая может быть и очередным показом следующего меню). У пунктов меню есть горячие клавиши. Самый часто используемый пункт у меня — показ музыкального проигрывателя. Раз он самый частый, раз показ меню у меня на t клавише, то и пункт меню с музыкой тоже на t, чтобы можно было быстро нажать Super+t+t.

----- ~/bin/tmux-menu-music.sh -----
#!/bin/sh
tmux has-session -t music || {
    tmux new-session -d -s music -c ~/tmp/music
    tmux set-option -t music status off
}
exec tmux attach-session -t music


Музыкальный проигрыватель у меня — это tmux сессия с заранее изменённой директорией. Годами когда-то использовал и cmus и moc и только из-за того, что в них была gapless playback возможность. Но с переходом на mpv проигрыватель, в котором тоже есть gapless режим, более не использую специализированных музыкальных проигрывателей.

----- ~/bin/tmux-menu-pass-session.sh -----
#!/bin/sh
while tmux has-session -t pass ; do echo waiting... ; sleep 1 ; done
tmux new-session -d -s pass rlwrap --history-filename /dev/null ~/bin/tmux-menu-pass.sh
tmux set-option -t pass status off
exec tmux attach-session -t pass


----- ~/bin/tmux-menu-pass.sh -----
#!/bin/sh
while :; do
    echo -n "> "
    read pass
    passman has "$pass" && break
done
exec passman "$pass" >/dev/null


Popup окно для парольного менеджера. Если парольный менеджер был запущен, то ждём пока он не выйдет. Далее запускаем цикл с вводом элемента для которого ищем пароль. Использую самописный shell скрипт passman. Сами пароли хранятся в иерархии директорий и файлов: ДОМЕН/ЛОГИН/passwd на зашифрованном разделе (а можно было бы в tmpfs разворачивать GnuPG зашифрованный архив). Например ~/.passmandb/habr.ru/stargrave+habrahabr@stargrave.org/passwd файл содержит пароль для входа на Хабр. Если запущу passman habr, то будет найден единственный похожий домен и логин и без лишних вопросов мне отобразят habr.ru/stargrave+habrahabr@stargrave.org. Но если сделаю запрос passman free, то мне выведут найденные варианты:

freenode/stargrave
freenet
bugs.freebsd.org/stargrave@stargrave.org


и должен повторить запрос уже указав более точный идентификатор элемента. При успехе, в primary X11 буфер обмена копируется найденный логин, а в clipboard буфер уже сам пароль. При этом скрипт спит десять секунд и форсированно очищает буфер перед выходом — дабы там пароль не держался вечно. Два буфера обмена позволяют Shift+Insert и Alt+Shift+Insert-ом вставить отдельно и логин и пароль. А в GUI приложениях третья кнопка мышки вставит логин, а Ctrl-V пароль. Если мне нужно найти пароль для GUI приложения, то запускаю passman через dmenu. passman gen генерирует новый пароль. Занесение новых элементов делается банальными mkdir+echo. Таким менеджером пароля полностью удовлетворён, а занимает он менее 50 строк POSIX shell кода.

Парольный менеджер используется для всяких удалённых ресурсов. Локально же, для полнодискового шифрования, GnuPG и SSH ключей используются только парольные фразы, длиной в 100±20 символов.

----- ~/bin/tmux-menu-dict.sh -----
#!/bin/sh
word=$(rlwrap -S "> " head -1)
sdcv "$word" | exec less


----- ~/.zshenv -----
export STARDICT_DATA_DIR=$HOME/dic
export SDCV_HISTSIZE=0


Поиск слова в словарях. Использую sdcv утилиту и StarDict словари, свободно скачанные с SourceForge.net. Зачем тут rlwrap? Просто read вызов у меня плохо дружит с кириллицей которую отредактировали (удаление символов попадает в запоминаемую переменную). Поэтому readline wrapper используется как интерактивный редактор, а на его выходе уже полученный чистый отредактированный результат.

----- ~/bin/tmux-menu-calc.sh -----
#!/bin/sh
tmux has-session -t calc || {
    tmux new-session -d -s calc ~/bin/zc
    tmux set-option -t calc status off
}
exec tmux attach-session -t calc


----- ~/bin/zc -----
#!/bin/sh
# natural logarithm: 1000lLx 10lLx /p
# sum of everything in stack: lSxp
pre="5k 10o \
    [1-d2+/d2*Skd*Sy0Ss10K^Sp[d1r/lk*ls+lsrdss-lp*d*1[s_q]s_>_lkly*sk2+lfx] \
    Sf1lfxLsLkLyLpLfs_s_s_s_]sL \
    [0d[+2z>a]salax]sS
"
exec rlwrap \
    --history-filename /tmp/.dc_history \
    --substitute-prompt "> " \
    --prompt-colour=red \
    --wait-before-prompt -1 \
    --pre-given "$pre" dc


А это калькулятор моей мечты. Ещё в школе хотел иметь какой-нибудь МК-52: настоящий стековый с обратной польской нотацией. dc, написанный на PDP-11 ещё до изобретения Unix, как раз является подходящим кандидатом, из коробки присутствующем в любой POSIX системе. Вот только в нём нет никаких интерактивных плюшек — сырой ввод из stdin. Поэтому обрамляю его вызов в rlwrap.

Время от времени мне требуется логарифм, которого штатно нет в dc. Где-то нашёл программу реализующую натуральный логарифм численным методом — загружаю её регистр L. А через формулы, известные её со школы, можно рассчитывать логарифмы и по другому основанию. Плюс добавил программу суммирования всего что находится в стэке в регистр S. Всё это загружаю в калькулятор при запуске.

В итоге получаю и интерактивное удобство, и обратную польскую нотацию, и библиотеку полезных функций, и встроенные средства по разному формату представления и системе счисления входных/выходных чисел. Прежде использовал встроенный в zsh калькулятор zcalc. В нём даже есть RPN режим, но требующий нажатия Enter после каждой операции — это быстро отъедает вертикальное место на экране. Ещё использовал Python, но люто негодовал из-за его низкой скорости запуска, заметной на глаз.

Остальные два элемента меню: показ календаря на три месяца и показ top.

cd



----- ~/.tmux.conf -----
bind-key o display-menu \
    find o "display-popup -KE -w 100% -R \"~/bin/tmux-fzf.zsh find '#{pane_current_path}'\"" \
    buf-files f "display-popup -KE -w 100% -R \"~/bin/tmux-fzf.zsh buf-files '#{pane_current_path}'\"" \
    git-files g "display-popup -KE -w 100% -R \"~/bin/tmux-fzf.zsh git-files '#{pane_current_path}'\"" \
    git-branches b "display-popup -KE -w 100% -R \"~/bin/tmux-fzf.zsh git-branches '#{pane_current_path}'\"" \
    git-commits c "display-popup -KE -w 100% -R \"~/bin/tmux-fzf.zsh git-commits '#{pane_current_path}'\""


Последнее меню tmux, в котором вызывается fuzzy finder fzf на разные объекты из текущей директории.

----- ~/bin/tmux-fzf.zsh -----
#!/usr/bin/env zsh

cd $2
set -e
tmp=`mktemp`
trap "rm -f $tmp" HUP PIPE INT QUIT TERM EXIT

case $1 in
(find)
    find . -mindepth 1 -path "*/.git" -prune -o \
        \( -type f -o -type d -o -type l \) -print |
    cut -c3- | fzf -m --preview="less -N -S {}" |
    while read fn ; do print ${(q)fn} ; done > $tmp
    ;;
(buf-files)
    tmux capture-pane -J
    tmux save-buffer $tmp.capture
    trap "rm -f $tmp.capture" HUP PIPE INT QUIT TERM EXIT
    tmux delete-buffer
    pe < $tmp.capture > $tmp
    ;;
(git-files) git status --short | fzf -m | perl -npe 's/^\s*\S+\s+//' > $tmp ;;
(git-branches) { git branch ; git branch --remote } | fzf > $tmp ;;
(git-commits)
    git --no-pager log --oneline -n 20 | perl -ne "print \"@~\$n \$_\"; \$n++" |
    fzf --reverse | cut -w -f1 > $tmp
    ;;
(*) echo unknown command ; sleep 1 ; exit ;;
esac

[ -s $tmp ] || exit
tmux set-buffer "`perl -npe 's/\n/ /g' $tmp`"
tmux paste-buffer
tmux delete-buffer


fzf начал использовать недавно, прежде на него не обращая внимание. Потому что видел не самый его достойный пример применения: постоянный переход по иерархии директорий чуть ли не при каждом cd. Это точно не повышает эффективность раз за разом ища директории. Но позже дошло, что его же вполне себе эффективно можно использовать для, действительно, fuzzy разрозненных вещей, никак не связанных с файлами. fzf это просто про выбор элементов из списка.

  • find относительно текущей директории ищет файлы, символические ссылки и директории, с возможностью мультивыбора, показывая less-ом предпросмотр файла. Почему не использовать плагины для zsh или tmux поставляемые из коробки с fzf? Во-первых, из коробки там куча hard-code-а на bash — отбивает желание даже пробовать. Во-вторых, zsh плагин отъедает десятки процентов экрана во время показа fzf, а для tmux-а используется отдельный split, с которым, из-за изменений окна, тоже можно получить коверканье информации в окнах. fzf в popup окне лишён этих недостатков.
  • buf-files сохраняет содержимое текущего окна и ищет в его тексте всё что похоже на пути к файлам:

    ----- ~/bin/pe -----
    #!/bin/zsh
    typeset -A seen
    path-extractor | while read fn ; do
        [[ $seen[$fn] -eq 1 ]] && continue
        seen+=($fn 1)
        print $fn
    done | fzf -m
    


    На самом деле, именно для из-за этой задачи я вспомнил про fzf. Мне очень захотелось получить возможность как-то быстро указать файл для команды: грубо говоря, ткнуть мышкой в имя файла на экране (как в Plan 9) и чтобы он подставился в аргументы командной строки.

    Сначала вышел на path-extractor утилиту, потом правил её чтобы она понимала кириллицу, потом пилил свои собственные быстрые и удобные меню для выбора файлов в списке. В итоге пришёл к fzf отлично подходящим для этой задачи. А изначальная идея родилась у меня, увидев PathPicker утилиту.
  • git-files даёт выбор файлов из git status. Сейчас это самая часто используемая команда этого меню. Набираем git add (на самом деле, набираю Ga), дальше жмём Super+o+g, выбираем интересующие нас файлы, Enter, видим что все выбранные добавлены через пробел с экранированием имён к уже набранному git add. Commit и profit!
  • git-branches даёт выбрать git ветки, в том числе других remote-ов.
  • git-commits показывает список коммитов, но с использованием @~XXX нотации, а не только хэша. Относительная нотация особенно удобна когда после интерактивного rebase меняются хэши коммитов.


----- ~/.zshrc -----
export FZF_DEFAULT_OPTS="--color=16 --info=inline"
cf() {
    local dir=$(find -L ${1:-.} -mindepth 1 -path "*/\.git" -prune -o -type d -print |
        fzf --height 40% --reverse --preview="tree -CN {}")
    [[ -z $dir ]] || { print -s cd $dir ; cd $dir }
}


cf команда набирается так же быстро как и cd, но она запускает find+fzf. Вариант с tmux popup-ом ищет файлы только от текущей директории, чем может быть не везде удобен. Интересно тут использование print -s который добавляет в историю zsh команду перехода на конечную директорию, а не только cf XXX.

Раз это zsh код, то почему бы не использовать встроенные возможности globbing-а, экономя на вызове внешней команды? Засада в том, что встроенные возможности не отображают результат поиска в real-time, тогда как find всё печатает на лету и fzf обновляет на живую, порой существенно экономя время когда нужно перейти куда-то неглубоко в иерархии директорий.

Глобально для fzf включаю только 16 цветов, как и для Vim. Не люблю все эти полутональные бледные блёклые унылые 256-цветные схемы. Скорее всего, это связано с плохим зрением. Люблю когда цвета сильно яркие и контрастные. Если где-то что-то подсвечено ярко-красным — то издалека увижу. А бледные 256+ цвета бывают на грани «зелёный vs салатовый». info=inline настройка просто экономит одну строку на экране, помещая информацию о поиске в строку ввода.

Одна из главнейших фич zshautopushd. Много лет использовал bash и перепробовал все виды хаков чтобы пытаться эмулировать autopushd, но точно запомнилось, что ничего не вышло: где-то да всё равно вылазили проблемы (конкретику забыл за давностью). Ну не вводить же мне длинный pushd вместо cd? Смысл autopushd в том, что каждый cd является pushd командой, запоминающей директорию в стэке. popd команда переходит на директорию из вершины стэка. cd - позволяет ходить только между двумя директориями туда-обратно. pushd+popd (autopushd) это life-changing привычка, без которой удобство страшно деградирует.

Но я настолько ленив, что даже popd отказываюсь набирать! Вместо этого нажимаю F4 и даже факт вызова popd не будет писаться в строке приглашения.

----- ~/.zshrc -----
popdquiet() { popd ; zle reset-prompt }
zle -N popdquiet
bindkey "^[OS" popdquiet # F4


Кроме того, отказываюсь набирать cd ..! Знаю что некоторые люди делают алиасы с различным количеством «точек» (cd....) чтобы переходить на n-уровней выше. Я же просто нажимаю F2, чтобы сделать один переход наверх и показать где нахожусь сейчас. Это в любом случае эффективнее и быстрее набирается чем cd....., в котором ещё и легко ошибиться в количестве уровней:

~ |0-% cd ~pyg/pygost/asn1schemas
autoenv: /home/stargrave/work/pygost/.autoenv.zsh
pygost/asn1schemas 1|0-% /home/stargrave/work/pygost/pygost        # F2!
~pyg/pygost 1|0-% /home/stargrave/work/pygost                      # F2!
~pyg 1|0-% autoenv: /home/stargrave/work/pygost/.autoenv_leave.zsh # F2!
/home/stargrave/work
~/work |0-% /home/stargrave                                        # F2!
~ |0-%


----- ~/.zshrc -----
cddotdot() { cd .. ; pwd ; zle reset-prompt }
zle -N cddotdot
bindkey "^[OQ" cddotdot # F2


Но часто мы не помним или не знаем где находится тот или иной файл или директория. Конечно можно использовать find — но долго писать его длинные конструкции. Можно использовать recursive globbing возможности типа ls **/*whatever*. Но тоже много лишнего набирать. Сделал вот такую примитивную функцию:

----- ~/.zshrc -----
f() { find . -name "*$1*" -print }


И теперь для поиска хоть чего-нибудь с «foo» в названии по всей иерархии мне достаточно набрать f foo. print -C 1 **$1* не используется, как уже писал выше, только потому что он не отображает прогресс поиска в реальном времени.

Достаточно ли быстра подобная навигация и все эти F2/F4 с fzf? Для часто используемых директорий — нет. Вот есть у меня например проект XXX, живущий в ~/work/arbeit.ru/XXX/src. И если с ним работаю ежедневно, то в лучшем случае смогу найти cd переход в истории zsh. Можно ли сделать некие алиасы для быстрого перехода?

----- ~/.zshrc -----
while read w ; do
    w=(${(s/=/)w})
    hash -d ${w[1]}=${~${w[2]}}
done < ~/.zhashd


----- ~/.zhashd -----
pyg=~/work/pygost
pyd=~/work/pyderasn
XXX=~/work/arbeit.ru/XXX/src


Теперь в XXX проект могу перейти набрав cd ~XXX. Более того, в приглашении командной строки будет писаться именно ~XXX, а не настоящий путь до .../XXX/src. Можно спокойно ссылаться и на директории ниже этого пути: vi ~XXX/tests/*bar.py. Цикл в .zshrc сделан исключительно чтобы не писать hash -d XXX=/path/to/XXX на каждой строчке ~/.zhashd.

Очень давно использовал z для быстрого перехода по директориям, основанный на частоте их посещения. Но из-за неуверенности куда всё же точно попаду, отбросил эту затею.

git



Раз уже начал упоминать git в меню tmux, то добью рассмотрение его алиасов и настроек:

----- ~/.zshrc -----
alias Ga="git add"
alias Gb="git branch"
alias Gc="git checkout"
alias Gd="git diff"
alias Gdc="git diff --cached"
alias Gs="git show --show-signature"
alias Gm="git diff --name-only --diff-filter=M"
alias Gam="git commit --amend"
alias Gl="git log --oneline --graph --decorate=short"
alias Gld="git log --format=format:'%ai %Cgreen%h%Creset %s'"
bindkey -s "^[OR" " git status --short\n" # F3
alias gg="git grep "


----- ~/.gitconfig | git config -l -----
alias.wt=worktree
alias.unchanged=update-index --assume-unchanged


  • Различные алиасы на git команды это наверное одно из самых частых что люди себе добавляют. У меня ничего интересного. Стараюсь действительно часто используемые вещи добавлять как алиасы именно в командный интерпретатор, а не как алиас команды самого git, для которого пришлось бы писать длиннющий git префикс.
  • Просмотр состояния репозитория — архичастая задача. Писать даже гипотетический алиас Gt не собираюсь. Одно нажатие F3 мне покажет сводку.
  • Поиск текста — ещё более частая операция. А git grep делает это существенно более быстрее чем grep. Используется так часто, что и gg алиас сделал.


----- ~/.git-ignore -----
*.gch
*.o
*.pyc
*.swp
.autoenv.zsh
.autoenv_leave.zsh
.redo
.tags
start.sh


----- ~/.gitconfig | git config -l -----
core.excludesfile=/home/stargrave/.git-ignore


Это часто встречаемые файлы которые хочется всегда игнорировать в репозиториях.

----- ~/.gitconfig | git config -l -----
color.diff=auto
color.status=auto
color.branch=auto
color.interactive=auto


Включаем цвета во всех командах. Никогда не мешало.

----- ~/.gitconfig | git config -l -----
advice.pushupdaterejected=false
advice.detachedhead=false
help.autocorrect=never


Отключить режим «новичка» и не делать мне излишних подсказок когда в чём-то ошибаюсь. Автокоррекция мне тоже вредна, так как исправленный вариант не попадёт в историю комманд.

----- ~/.gitconfig | git config -l -----
core.quotepath=false
core.whitespace=blank-at-eol, space-before-tab, tab-in-indent, blank-at-eof
format.useautobase=true
grep.linenumber=true
init.defaultbranch=master
log.date=iso8601
notes.displayref=*
push.default=current
diff.tool=vimdiff
merge.tool=vimdiff


  • quotepath отключает экранирование имён файлов в выводе статуса. Из вывода этой команды всё равно имена файлов не копирую в аргументы.
  • whitespace настраивает какие whitespace-ы учитывать в diff.
  • useautobase включает вставку hint-а о родительском коммите в патчи создаваемые format-patch командой. Не знаю точно, но говорят, что это помогает при merge.
  • linenumber включает показ номера строки в выводе git grep.
  • defaultbranch заставляет не следовать идиотским веяниям Запада.
  • iso8601 является единственно правильным форматом отображения времени.
  • displayref показывает заметки (notes) из всех namespace-ов. Многие люди возможно никогда в жизни не встречались с notes-ами, но у меня есть репозитории с ними.
  • vimdiff используется для всех операций связанных с merge.


----- ~/.gitconfig | git config -l -----
pull.ff=only


Важная защита от внезапного merge после git pull. Всё должно быть под чётким контролем разработчика, никаких автоматических merge, которые в неумелых руках превращают историю репозитория в нечитаемое месиво.

----- ~/.gitconfig | git config -l -----
rerere.enabled=true


rerere позволяет запоминать как человек разрешал конфликты. Очень полезная штука, которая помогает сократить время при повторных интерактивных rebase-ах. Хотя были и случаи когда разрешал конфликты не корректно и автоматическое запоминание rerere повторяло эти варианты.

----- ~/.gitconfig | git config -l -----
diff.algorithm=histogram
diff.colormoved=default
diff.colormovedws=allow-indentation-change


histogram алгоритм иногда действительно ощутимо лучшие diff-ы генерирует — его включение стоит того. А colormoved функционал позволяет разными цветами подсвечивать перемещённые области кода. Штатно перемещённые области никак не подсвечиваются: вот отсюда удалили какой-то кусок, а сюда какой-то кусок добавили — и далеко не всегда понятно это было действительно удаление или просто перемещение. С данными настройками даже изменение indentation не смутит git от понимания что это перемещённые блоки текста.

----- ~/.gitconfig | git config -l -----
core.pager=diff-highlight | $PAGER


В дистрибутиве git есть contrib/diff-highlight фильтр для diff вывода. Штатно git никак не показывает что именно в строке было изменено: вот её старая, а вот новая версии. Поди быстро найди глазами что именно поменялось внутри слова в середине предложения! С этим фильтром инверсным цветом подсвечиваются изменения в строке.

Я нередко использую git bundle для создания пачек коммитов, которые можно переносить через store-and-forward зашифрованные методы передачи NNCP. Часто проще перекинуть bundle чем обеспечивать полноценную файловую или сетевую доступность репозитория, или думать о пользователях и правах доступа при упаковке .git в tar.

Многие проекты хостятся на GitHub, использовать который нет возможности:

  • Он не работает без скачивания и выполнения несвободного ПО (в виде потенциально компрометирующих компьютер JavaScript программ). Когда то, напомню, работал.
  • Его terms of service выглядят как несовместимые с copyleft лицензиями типа GNU GPL. А я ярый поклонник copyleft.
  • Тот кто как-либо (даже будучи туристом) связан с Крымом, Ираном или Сирией — на раз два, в любой момент может быть забанен, как показала практика. Отношусь к этой группе людей.


Поэтому патчи отправляю штатным Linux способом разработки: git format-patch и git send-email. Так как у меня полноценная почтовая система, то отсылка работает автоматом из коробки.

zsh



Возвращаюсь к zsh и, собственно, почему именно этот командный интерпретатор? У меня есть многолетний опыт использования tcsh, bash и zsh, а также немного какой-то версии ksh. bash для меня лидер по монструозности и медленности. Имеет огромную кодовую базу, при этом значительно уступая по функционалу zsh. Нет ни одной рациональной причины выбирать bash.

Портируемые скрипты следует писать либо на POSIX shell, либо не писать вообще, а использовать полноценный язык программирования. В 99% случаев когда вижу скрипт с #!/bin/bash, то в нём или уже вполне себе POSIX compatible код, либо требуется замена двойных квадратных скобок на одинарные и будет POSIX. Это говорит о безграмотности авторов подобных творений, которые не понимают даже что и для чего пишут. Да, shell в целом поэтому и не любят, потому что никто не хочет его, хотя бы в минимальной степени, изучать чтобы писать грамотно. Отсюда у меня эмпирическое правило: #!/bin/bash — значит будет плохой неграмотный скрипт. Безусловно, всегда и везде бывают исключения. Но не даром в Debian и AltLinux устраивали джихад против башизмов.

Если нужно исполнять POSIX shell скрипты, то bash избыточен и, как правило, не шустр. Если нужно иметь удобный интерактивный shell, то, опять же, смысла в bash нет никакого. Единственная причина его популярности: он является частью GNU операционной системы.

С zsh есть большая отталкивающая проблема: 99.99% статей на тему «why zsh?», «zsh vs bash» будут говорить о штуках типа oh-my-zsh и богатстве встроенных completer-ов. И не считаю всё это преимуществом, а скорее больше вредом. Во-первых, все эти многочисленные completer могут очень и очень тормозить, выполняя сторонние команды на каждый чих дополнения. Во-вторых, видя со стороны как люди их используют, замечаю, что они или совершенно забывают как пользоваться базовыми командами или разучиваются читать документацию к программам, ориентируясь только на предложения completer-а. В-третьих, люди перестают понимать что происходит в дебрях всех этих completer-ов и как они могли бы получить аналогичный результат дополнения без них: как получить список файлов на удалённом сервере, или найти процесс по имени или man по части имени.

zsh имеет убер-настраиваемую мощнейшую систему completion! К ней как таковой нет никаких претензий. Но редко можно встретить достойные примеры её применения, которые действительно бы повышали КПД без тормозов. Чаще всего все они вуалируют незнание ОС и инструментария. «Скрывают» новый функционал обновлённых программ, так как completer не синхронизирован с фичами программы которую он дополняет. В общем, делают медвежью услугу. На многих видео в YouTube видно как это регулярно тормозит.

zsh это производительность, богатый язык программирования, масса интерактивных плюшек для удобства, богатейшие возможности substitution, history/filename/process/whatever expansion, pattern matching, globbing и много чего другого. Рассказывать об этом тут не имеет смысла, так как лучше начать читать User's guide to Z-Shell чтобы начать понимать какие-нибудь print ${array[®${(l.${#${(O@)array//?/X}[1]}..?.)}]} выражения. А также zsh tips and tricks, zsh-lovers, ZZapper's tips. Все эти возможности чрезвычайно экономят время, как минимум, за счёт коротких выражений. И помнить, что если где-то зажимается курсор для перемещения по введённой строке, то вы делаете что-то точно не так. А программирование в zsh гораздо более надёжно чем в POSIX shell, как минимум из-за наличия полноценных массивов и гибкости управления раскрытия переменных и выражений.

У меня в системе bash принципиально не стоит. Но, как минимум, скрипты для virtualenv-wrapper Python-а написаны на нём — чёрт уж с ними, ибо zsh имеет отличную совместимость с bash кодом, без проблем их запуская. Если бы по какой-то причине не было возможности установить zsh, то мой бы выбор пал на одну из версий ksh, в котором тоже масса интерактивных удобств и Bourne/POSIX-like язык, в отличии от совершенно чуждого (но интерактивно удобного) tcsh. На серверах zsh не ставлю, оставляя голый /bin/sh — всё же там времени проводится сильно меньше, можно и потерпеть неудобства.

К FISH проявляю уважение из-за того, что он продемонстрировал какие можно бы было сделать удобства интерактивного использования. Рассматривать этот shell всерьёз для работы не могу: беглым взглядом вижу что в нём скуднее функционал для substitution, expansion и globbing. Могу ошибаться, ибо не использовал, а только рассматривал документацию, но его несовместимость с POSIX всё же останавливает, как и tcsh.

Судя по всему, в zsh нет трёх фич из коробки, предоставляемых FISH-ем:

  • Синтаксическая подсветка вводимых команд. Служит точно так же как и подсветка кода языков программирования — помочь быстрее ориентироваться в множестве написанных слов и символов. И подтверждаю на практике — это существенно помогает! Благо что из-за гибкости zsh всё это возможно сделать в виде подключаемого скрипта:

    ----- ~/.zshrc -----
    ZSH_HIGHLIGHT_HIGHLIGHTERS=(main brackets)
    . ~/work/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh
    typeset -A ZSH_HIGHLIGHT_STYLES
    ZSH_HIGHLIGHT_STYLES[assign]="fg=yellow"
    ZSH_HIGHLIGHT_STYLES[commandseparator]="fg=red"
    ZSH_HIGHLIGHT_STYLES[single-hyphen-option]="fg=green,bold"
    ZSH_HIGHLIGHT_STYLES[double-hyphen-option]="fg=green"
    ZSH_HIGHLIGHT_STYLES[globbing]="fg=magenta"
    ZSH_HIGHLIGHT_STYLES[global-alias]="fg=yellow,bold"
    ZSH_HIGHLIGHT_STYLES[history-expansion]="fg=magenta"
    ZSH_HIGHLIGHT_STYLES[redirection]="fg=red"
    ZSH_HIGHLIGHT_STYLES[path]="fg=white,underline"
    ZSH_HIGHLIGHT_STYLES[path_pathseparator]="fg=white,bold,underline"
    


    Все строчки, кроме одной, просто задают цвета для разных элементов, не полностью удовлетворяющих меня из коробки. Ценнейший плагин!
  • Autosuggestions функция: когда автоматически предлагается другим цветом дополнение уже сейчас введённой команды. Если shell настроен так, что при нажатии Up он предложит из истории команду начинающуюся на уже введённом тексте, то с autosuggestions, отдельным цветом сразу же будет показано что будет взято из истории, ещё не нажимая Up. По сути, это экономия ровно одного-двух нажатий: мы могли бы нажать Up и увидеть что будет предложено из истории, Down чтобы «отменить» предложение, вернувшись к недописанному оригиналу. Но эта экономия одного нажатия, показывающая в том ли мы «движемся» по истории направлении, стоит того! Чудовищно повышает производительность, меньшим количеством ложных срабатываний элементов из истории.

    ----- ~/.zshrc -----
    . ~/work/zsh-autosuggestions/zsh-autosuggestions.zsh
    ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE="fg=cyan"
    

  • Substring поиск с подсветкой. Об этом напишу в zsh history разделе.


zsh completion



Последнее чем zsh мне помогает в навигации и дополнения путей к файлам — его система completion-а, в которой используется мой собственный простой completer:

----- ~/.zshrc -----
zstyle ":completion:*:functions" ignored-patterns "_*"
zstyle ":completion:*" matcher-list "" 'm:{a-z\-}={A-Z\_}' 'r:|?=** m:{a-z\-}={A-Z\_}'
_mycomp () {
    [[ ${words[1]} != man ]] || { _man && return 0 }
    [[ $CURRENT -eq 1 ]] && _command_names || _files && return 0
    # MAGIC_EQUAL_SUBST {{{
    [[ $PREFIX = *\=* ]] || return 1
    compstate[parameter]=${PREFIX%%\=*}
    compset -P 1 "*="
    _value
    # }}}
}
zstyle ":completion:*" completer _mycomp _parameters
autoload -U compinit ; compinit -d /tmp/.zcompdump
zstyle ":completion:*:default" list-colors ""
autoload -U complist


На эту часть конфига потратил больше всего времени, тонув в документации. Включение completion системы со всеми completer-ами меня не устраивает: оно тормозит, пытаясь дополнять всё что увидит. Если где-то начинаю видеть задержки — негодую.

Во-первых, мне хочется чтобы пути до файлов дополнялись tab-ом, как это по умолчанию происходит в большинстве interactive shell-ах. С отключённым completion это поведение работает. Во-вторых, мне хочется чтобы было fuzzy-like дополнение, когда могу набирать только некоторые части имён файлов — это уже требует включённую completion систему. Кроме тормозов, она ещё и частенько просто мешает жить: например хочу прочитать .info файл и набираю info ПУТЬ, который не будет дополнятся нажатием tab-а, так как completer для info форсированно ищет файлы в INFOPATH. Включив только completer имён файлов — не будут дополнятся имена команд. В-третьих, хочется дополнять имена переменных окружения. В-четвёртых, будет проблема в том, что после каких-нибудь аргументов из серии --prefix= не будет производится дополнение путей, если они написаны слитно к этому знаку равно. Для штатных completer-ов есть MAGIC_EQUAL_SUBST опция отвечающая за это, но я не использую штатный набор completer-ов! Все эти хотелки уместились в небольшом куске кода выше.

  • ignored-patterns отключает все completer-ы, чтобы они даже не загружались, экономя время.
  • matcher-list настройка заставляет делать fuzzy-like matching имён файлов. Ультраповышающая производительность человека настройка — чуть ли не только ради неё стоит переходить на zsh.

    Суть проста: набрав s/2/r2s/auth/init, нажав tab, вся эта портянка недо-путей раскроется в src/rik2utils/rik2s/request_auther/__init__.py. /n/s/m/omegaParac/death раскроется в /net/storage/music/Deathspell_Omega-2010-Paracletus/04.Dearth.wv. Это кардинально отличается от просто добивания tab-ом, который не учитывает все возможные допустимые пертурбации указанных путей! В src директории находится rik2s и rik2c (сервер и клиент), поэтому любое добивание tab-ом пути rik2 всё равно остановится на конечном символе s или c, потребующим явного ввода от пользователя. А zsh matcher увидит что только в rik2s директории есть хоть что-то с auth именем, к тому же ещё и содержащим *init*. Кроме того, в некоторых shell-ах после tab-а пришлось бы ещё и дописывать слэш, чтобы продолжить поход вглубь указанной директории.
  • _mycomp это самописный completer:

    • Если первым аргументом идёт man, то включается _man completer — вот автоматизированный поиск man-ов мне нравится иметь: например man git-, tab, покажет все известные man-ы и, косвенно, команды git-а. При этом, дополнение путей до man страницы вне MANPATH всё равно будет работать.
    • Если наша текущая позиция completion-а в начале, то есть мы вводим
      команду, то включается completer имён команд.
    • В противном случае, используется completer имён файлов.
    • А дальше эмуляция MAGIC_EQUAL_SUBST поведения, при котором, если мы находимся в слове со знаком равно, то будет производится дополнение путей.

  • completer настройка подключает _mycomp и _parameters, дополняющий имена переменных.
  • compinit инициализирует completion систему с сохранением инициализированного состояния в tmpfs. Не люблю когда на диске находятся промежуточные (автоматически сгенерированные) файлы, тем более быстро создающиеся один раз за всё время работы.
  • complist и list-colors настройки включают цветное отображение completion результатов в виде списка. Применение меню ни разу не видел полезного: выбор элемента в интерактивном меню делается кучей нажатий клавиш перемещения, которых, как правило, больше чем если бы продолжил дополнять путь и нажимать tab снова.


zsh history



Возвращаюсь к substring поиску с подсветкой как в FISH. Например хочу найти команду vpxenc, с, неизвестно где находящейся, foo=bar опцией. Но совершенно не помню в каком месте полной командной строки она находится (зачастую у меня делаются вызовы ДА_ЕЩЁ=ПЕРЕМЕННАЯ_ОКРУЖЕНИЯ ffmpeg… | vpxenc ...). В FISH, если вводить vpxenc foo=bar, будет работать поиск по этим введённым частям подстроки, да ещё и подсвечивая из истории места где были найдены vpxenc и foo=bar.

Для zsh сделали zsh-history-substring-search плагин. Всё бы хорошо, но меня он не устраивает, как, возможно на самом деле, и не устроил бы FISH: чаще всего мне нужен не substring поиск. Я бы хотел иметь возможность указывать какой тип поиска хочу включить. И у меня не получилось с этим плагином сделать комбинации клавиш которые бы включали то один, то другой варианты. Чаще всего, когда набираю foo и Up, то действительно хочу чтобы мне из истории показывали элементы начинающиеся на foo, а не элементы где этот foo будет в любом месте.

Плюс просматриваю глазами все эти плагины и оцениваю годно ли они написаны, или являются просто каким-то наколеночным месивом лишь бы хоть как-то выполняя задачу. И zsh-history-substring-search уж больно массивно выглядит (к качеству претензий нет).

Я в итоге написал свой «поиск», как я его вижу, полностью меня удовлетворяющий:

----- ~/.zshrc -----
autoload -U history-search-end
zle -N history-beginning-search-backward-end history-search-end
zle -N history-beginning-search-forward-end history-search-end
bindkey "^[[A" history-beginning-search-backward-end
bindkey "^[[B" history-beginning-search-forward-end

beginning-history-incremental-pattern-search-backward() {
    zle history-incremental-pattern-search-backward ${BUFFER:gs/ /*/}
}
zle -N beginning-history-incremental-pattern-search-backward
bindkey "^[[1;2A" beginning-history-incremental-pattern-search-backward
bindkey -M isearch "^[[A" history-incremental-pattern-search-backward
bindkey -M isearch "^[[B" history-incremental-pattern-search-forward


С десяток строчек, но это всё что мне надо! В начале настраиваются клавиши Up/Down для поиска по истории элементов начинающихся с уже введённой последовательности — это и так популярная настройка, вроде бы аналогично работающая в bash. А вот дальше, при нажатии Shift-Up, говорю чтобы включился инкрементальный поиск по шаблону, в котором теми же Up/Down буду перемещаться по элементам истории. При этом, пробелы между уже введёнными словами (например foo bar) заменятся на звёздочки, сформировав foo*bar шаблон поиска.

Набрав u suck, Shift-Up два раза, предложат mutt -f =suckless открытие почтового ящика suckless рассылки. Но без синтаксической подсветки найденных слов. Тратить ли время на плагин или допиливать свои десять строчек ради подсветки относительно редко выполняемого действия? Для меня овчинка выделки не стоит.

----- ~/.zshenv -----
HISTFILE=~/secure/.history
HISTSIZE=10240
SAVEHIST=10240


----- ~/.zshrc -----
setopt APPEND_HISTORY SHARE_HISTORY INC_APPEND_HISTORY HIST_IGNORE_ALL_DUPS
setopt HIST_IGNORE_SPACE
HISTORY_IGNORE="(yt* *|t *|t|sdcv *|mmfileget *|arr)"


  • Сохраняется история в файл зашифрованного раздела — не хочу чтобы кто-то мог посмотреть чем я занимался, если диск окажется в чужих руках.
  • Размер истории в 10k элементов вроде бы достаточен на практике. И не слишком маленький и не слишком большой.
  • APPEND_HISTORY, INC_APPEND_HISTORY добавляют инкрементально историю во время работы. А HIST_IGNORE_ALL_DUPS убирает дубликаты.
  • SHARE_HISTORY говорит об общей для всех instance-ов zsh истории. Это вроде бы самая самая первая фишка zsh о котором упоминают люди при сравнении с bash. Она действительно life-changing! Вы не привязаны к одному окну, одному instance-у shell.
  • HIST_IGNORE_SPACE не добавляет в историю команды начинающиеся с пробела. Каждый день бывают вызовы которые вы точно не хотели бы выставлять на показ или просто ими мусорить в autosuggestion-ах.
  • Но можно и глобально игнорировать сохранение команд в историю по шаблону указанному в HISTORY_IGNORE: все вызовы словаря, youtube-dl, открытие рабочей почты и скачивания файлов из Mattermost (ссылки до которых точно никогда не будут актуальны после).


ZLE



----- ~/.zshrc -----
bindkey -v
export KEYTIMEOUT=1
bindkey "^[[1~" beginning-of-line # Home
bindkey "^[[4~" end-of-line # End

autoload -U edit-command-line
zle -N edit-command-line
bindkey -M vicmd v edit-command-line


Как известно, в мире есть только два настоящих достойных редактора: Vi(m) и Emacs. Я бы не был столь категоричен и ещё бы не забыл про стандартный текстовый редактор ed и Acme редактор из Plan 9. ed может быть актуален при редактировании файлов не помещающихся в оперативной памяти. Принципы работы в Emacs и Vi существенно отличаются и многие программы поддерживают оба режима ввода. Даже POSIX стандарт обязывает в shell иметь переключаемый режим ввода: set -o vi включает модальный vi-режим ввода.

bindkey -v включает vi-режим в zsh. Далее в конфиге уменьшается timeout при нажатии Escape для смены модального режима ввода. Делается binding на Home и End клавиши (нажимать Ctrl-A (в Emacs режиме) или Escape+^ для перехода в начало строки? Когда есть полноценная клавиатура с предназначенными для этого клавишами?). Включается возможность редактирования всей командной строки внешним редактором, нажимая v в командном режиме vi. Полноценный редактор же всё равно не сравнится с ZLE редактором zsh.

Включение vi-режима для всех использующих GNU Readline программ:

----- ~/.inputrc -----
"\e[A": history-search-backward
"\e[B": history-search-forward
"^B": backward-word
"^F": forward-word

set editing-mode vi
set show-mode-in-prompt on


Ещё добавляются binding-и клавиш Up/Down поиска по истории. А также показывается текущий режим (командный, ввода) в строке приглашения.

Для программ использующих libedit (весь LLVM/Clang инструментарий):

----- ~/.editrc -----
bind -v


zsh prompt



Как у меня выглядит строка приглашения?

~pyg/src 0|0-% sleep 3 ; false
~pyg/src 3|1+%


----- ~/.zshrc -----
function zle-line-init zle-keymap-select {
    mode_vi=${${KEYMAP/vicmd/+}/(main|viins)/-}
    [[ $timer ]] && timer_show=$(( $SECONDS - $timer ))
    prompt="%2~ "
    prompt+="%U${timer_show}%u|"
    prompt+="%B%?%b"
    prompt+="${mode_vi}"
    prompt+="%B%F{magenta}%#%f%b "
    PS1=$prompt
    zle reset-prompt
}
zle -N zle-line-init
zle -N zle-keymap-select

preexec() { timer=$SECONDS }
precmd() { printf "\a\033]2;\033\\" }


  • В начале показывается пара последних элементов пути до директории где нахожусь, с учётом алиасов на директории. В моём примере это PyGOST проект (~pyg) и его src поддиректория. От остальной части приглашения отделена пробелом, чтобы можно было выделить мышкой двойным кликом.
  • Далее идёт подчёркнутый показ количества секунд прошедших с момента выполнения предыдущей команды. Для этого используется hook preexec, сохраняющий текущего значения секунд в переменную перед выполнением команды. Крайне полезная штука, так как очень часто хочется узнать сколько выполнялась команда, но конечно же ты забываешь ставить time перед её вызовом.
  • Далее показывается жирным цветом (все эти подчёркивания и цвета только для того чтобы быстро можно было находить элементы на экране) код возврата последней команды. Ещё более важная для показа штука, ведь многие не выводят никаких сообщений в случае ошибки. Можно набрать echo $?, но достаточно ввести любую другую команду чтобы значение этой переменной было потеряно.
  • Далее символом - или + показывается текущий vi-режим: вставки или командный. В моём примере в конце был нажат Escape и переход в командный vi-режим.
  • И фиолетовым цветом показывается разделитель между приглашением и самой строкой ввода. Фиолетовый цвет хорошо виден на экране, позволяя быстро глазами находить места ввода командного интерпретатора.


precmd — архиважнейший hook, просто выводящий строчку в терминал перед выводом приглашения командной строки. Первый символ в ней это alert/bell, который выставит active свойство у tmux окна и urgency у X11 окна эмулятора терминала. Например ввёл команду сжатия здорового файла: не буду же сидеть и лицезреть прогресс? Переключаюсь в другой эмулятор терминала или другой tmux tab. Но когда сжатие завершится, то, перед показом приглашения, alert/bell заставит tmux и dwm мне показать urgency. Внезапно инвертированное значение цвета в панели тэгов dwm, или цвета tab tmux замечу сразу же, тем самым оповещаясь о завершении моей команды.

Есть минималистичная herbe утилита для отображения на экране GUI оповещений. Однако я совершенно не смог придумать где бы она могла быть удобнее. Древнейшая штатная urgency+bell возможность эмуляторов терминалов и оконных менеджеров, выставляющая флажок привлекающий внимание — достаточно для всех моих задач. Когда-то оповещения выводили приходящие сообщения в XMPP — но сторонние люди могут увидеть что-то нежелательное.

На удалённых серверах уже привык дописывать к долгоживущим командам что-то типа echo done | mailx -s whatever-compression stargrave@stargrave.org для отправки почтового сообщения о завершении.

Оставшаяся часть строки вывода precmd является escape-последовательностью очищающей title окна. Некоторые программы забывают о его очистке при выходе. Они могут не штатно выйти или упасть.

Я часто видел у людей многострочные строки приглашения: совершенно не понимаю столь безумно нерационального расходования вертикального пространства, быстро отъедаемого немногочисленными командами. Кроме того, убеждён что в строке приглашения должна быть информация имеющая «историческую» ценность: в какой директории выполнил ту или иную команду, какой был код возврата, в какой ветке происходило и т.д… А отображение заряда батареи или уровня сигнала WiFi актуально только «здесь и сейчас»: этому место только в statusbar-ах. Показ часов мог бы быть полезен чтобы понимать когда была запущена/завершена команда. Но они ощутимо отнимают пространство.

Я пробовал отображать текущую ветку git-а в приглашении, где-нибудь справа на экране. zsh даже имеет штатную поддержку подобных hook-ов для произвольной системы контроля версий. Но чтение чего-то с файловой системы (пускай даже через кэши) — рано или поздно даст о себе знать задержками при нагруженной системы. Лично я и так знаю где нахожусь, а если нет, то Gb команда покажет.

zsh misc



Оставшиеся zsh разносторонние настройки:

----- ~/.zshrc -----
setopt GLOB_STAR_SHORT GLOB_DOTS EXTENDED_GLOB
setopt NO_NOMATCH
setopt AUTO_PUSHD PUSHD_IGNORE_DUPS
setopt PIPE_FAIL

setopt RM_STAR_SILENT
export LISTMAX=9999


  • EXTENDED_GLOB нужен де-факто всегда для мощного globbing. GLOB_STAR_SHORT позволяет писать ** вместо **/*. GLOB_DOTS автоматом включает файлы с точками в начале в поиск.
  • NO_NOMATCH явно говорит, чтобы при отсутствии результатов globbing-а, не показывалась ошибка, а введённое выражение использовалось как есть. Я некорректно в этой статье использую слово globbing, так как matching относится не только к globbing, но и, например, раскрытию путей до исполняемых файлов: echo =mutt покажет /home/stargrave/local/bin/mutt.

    Так вот NOMATCH при выполнении echo =foo скажет zsh: foo not found, а NO_NOMATCH покажет =foo. Так как почтовые ящики для Mutt тоже указываются через =ЯЩИК, то с NO_NOMATCH оно не будет требовать экранирования. mutt -f =arbeit успешно отработает, передавая =arbeit как аргумент Mutt-у.
  • Про необходимость autopushd уже писал.
  • RM_STAR_SILENT опция говорит что я самоуверенный человек и не побоюсь выполнять rm со звёздочкой. Надо учиться быть аккуратным везде.
  • LISTMAX выставляет границу срабатывания completion предложений. Когда нахожусь в директории с сотнями тысяч файлов, нечаянно нажав tab, не хочу ждать ни CPU, форматирующего вывод для меня, ни сети, по которой будет передаваться приличное количество имён файлов.


Совсем немного алиасов:

----- ~/.zshrc -----
alias l="ls -AF "
alias ll="ls -AFl "
alias vi="vim"
alias m="less "
alias -g M="| less"
alias -g W="| wc -l | sed 's/ //g'"
alias ssh="TERM=xterm ssh"
alias sshnm="ssh -S none"
ssht() { ssh -C -t $1 tmux attach -t0 }


  • l и ll вариации встречаются сплошь и рядом, даже иногда сразу присутствуя в дистрибутивах ОС. А настройки цветов у меня заданы переменными окружения:

    ----- ~/.zshenv -----
    export CLICOLOR=1
    export LSCOLORS=BxGxcxdxCxegDxabagacad
    

  • Одной из удобнейших фич zsh являются алиасы, используемые в конвейере. Вместо whatever | less набираю whatever M. А для, довольно частого, подсчёта количества строк добавляю W в конце. Почему у меня используется буква «m» для всего что касается less? Привычка, ибо изначально использовал more.
  • TERM=xterm ssh уже много лет не подводящий нигде алиас. Даже screen256-color есть не на всех системах, поэтому получить ошибки о незнакомом терминале от удалённой системы можно на раз два. xterm присутствует везде и работает is good enough при администрировании серверов.
  • По умолчанию, мой OpenSSH использует долгоживущие сессии. Но это не всегда хочется, поэтому sshnm (no master) вынесен в алиас.
  • Так как на серверах всегда используется tmux, и наверняка, кроме первоначального входа, хочется сразу же подключаться к tmux сессии, то для этого сделан удобнейший ssht алиас. А раз речь про априори текстовые данные, то в нём ещё и форсированно включается сжатие данных.
  • Так как начинал работать с vi, то привык набирать эту команду: vim у меня никогда с клавиатуры не вводится.


less



Как уже упомянуто выше, для просмотра текстовых файлов использую less:

----- ~/.zshenv -----
export PAGER=less
export LESSHISTFILE=/tmp/.lesshst
export LESSKEY=/tmp/.lesskey
export LESS=RWXij2
export LESS=${LESS}"Ps?f%f .?m(%i/%m) .%lt-%lb?L/%L. [%bB?B/%B.]?B %pB\%.?x N\:%x.%t"


----- ~/.lesskey -----
#command
^N next-file
^P prev-file


  • ~/.lesskey делает возможным работу колеса прокрутки трэкбола.
  • R опция включают отображение ANSI последовательностей. Много команд делают цветной вывод.
  • j2 опция показывает две строчки сверху/снизу от искомого слова, чтобы получше видеть и понимать контекст где находится искомая строка.
  • i опция делает поиск игнорирующим регистр, если в нет заглавных букв. Обожаю именно такое поведение поиска где бы то ни было.
  • Важнейшая для удобства W опция — она подсвечивает italic-ом строку которая ещё не была мною прочитана, когда проматываю экран. Это ценно если прокрутка осуществилась, например, на половину: в этом случае часть строк сместится, но не исчезнет полностью и мне пришлось бы глазами искать и понимать что я видел, а что нет.
  • Последняя строка настраивает формат строки статуса: в ней хочу видеть, как и в Vim-е, байтовые и строковые позиции, их общее количество и процентаж от того где нахожусь. Фактически хочу всегда видеть вывод = less команды, только без лишних длинных слов типа «bytes» и «lines».


Продолжение статьи.
Теги:
Хабы:
Всего голосов 17: ↑16 и ↓1+18
Комментарии14

Публикации

Истории

Ближайшие события

2 – 18 декабря
Yandex DataLens Festival 2024
МоскваОнлайн
11 – 13 декабря
Международная конференция по AI/ML «AI Journey»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань