Конкретная сборка, а не руководство. Заметка описывает одну машину и устаревает вместе с ней; технология как таковая — в статьях из раздела «Связанные заметки».
Обзор
Заметка описывает конкретную машину, а не технологию Hyprland вообще — тот же жанр, что медиасервер. Здесь зафиксировано, из чего собрано домашнее рабочее окружение, почему принято то или иное решение и что понадобится, если поднимать его заново.
Все факты ниже проверены на живой системе: команды выполнялись, поведение наблюдалось. Там, где обнаружилось расхождение с ожиданиями (а его оказалось немало — см. раздел про ловушки), записан результат эксперимента, а не то, как «должно быть».
Ежедневная работа — клавиши, панели, скретчпады — вынесена в отдельную шпаргалку: Hyprland: горячие клавиши и скретчпады.
Железо и сессия
Машина построена вокруг одного ультраширокого монитора, и это определяет почти все решения по раскладке окон.
| Компонент | Значение |
|---|---|
| Дистрибутив | CachyOS, ядро 7.2 |
| Композитор | Hyprland 0.56.2 |
| Монитор | DP-1, Xiaomi Mi Monitor, 3440×1440@144 Гц |
| Видео | NVIDIA RTX 4070 Ti, драйвер 610 |
| Вход в сессию | SDDM → uwsm start -e -D Hyprland |
| Терминал | kitty + tmux |
| Раскладки | us, ru — переключение по Super+Space на уровне XKB |
Сессия поднимается через uwsm, поэтому все долгоживущие процессы запускаются как uwsm app -- <команда>: каждый попадает в собственный systemd-скоуп, корректно гасится при выходе и получает свой OOM-скоуп. Проверить, что автозапуск отработал:
systemctl --user list-units --type=scope --no-legend | grep -i hyprland
VRR на этом мониторе включён в режиме «только фуллскрин» (vrr = 2). Режим «всегда» на NVIDIA даёт мерцание на десктопе и в браузере, а сам монитор адаптивную синхронизацию по EDID не рекламирует — hyprctl monitors показывает vrr: false даже при включённой опции.
Состав окружения
| Роль | Программа |
|---|---|
| Панель | waybar |
| Уведомления | mako |
| Обои | awww |
| Блокировка экрана | hyprlock |
| Простой и автоблокировка | hypridle |
| Лаунчер приложений | hyprlauncher |
| Поиск по окнам и меню | fuzzel |
| История буфера обмена | cliphist |
| Индикатор громкости и яркости | swayosd |
| Скриншоты | grim + slurp, разметка в satty |
| Пипетка цвета | hyprpicker |
| Авторизация GUI | polkit-kde-agent |
Отдельно стоит отметить агент авторизации: без запущенного polkit-kde-authentication-agent-1 любое GUI, запрашивающее пароль root, молча не получает авторизацию. Диагностировать это неприятно, потому что ошибки нигде не видно — приложение просто не делает то, о чём его просят.
Конфигурация на Lua
Hyprland поддерживает два формата конфига: исторический hyprlang и Lua. Здесь используется Lua, и это принципиально меняет несколько вещей. Проверить, какой провайдер активен:
hyprctl systeminfo | grep configProvider
Lua даёт то, чего hyprlang не умеет: в конфиге доступны запросы к состоянию композитора (hl.get_windows, hl.get_active_workspace), таймеры, подписка на события и биндинги на произвольные Lua-функции, а не только на диспетчеры. Скретчпады ниже построены именно на этом — логика «если приложение не запущено, запусти; иначе покажи или спрячь» живёт прямо в конфиге, без внешних скриптов и разбора JSON.
Плата за это — несовместимость привычных команд, см. раздел про ловушки.
Проверка после любой правки:
hyprctl reload && hyprctl configerrors
Пустой вывод configerrors означает, что конфиг принят целиком. Ошибка в одном ключе не мешает загрузиться остальному, поэтому проверять надо явно — молчание в терминале ничего не гарантирует.
Раскладка окон под ультраширокий монитор
При ширине 3440 пикселей тайлинг «пополам» даёт две колонки по 1700 — это комфортно, а вот третье окно уже делает колонки бесполезно узкими. Отсюда два решения.
Первое — dwindle с force_split = 2, то есть новое окно всегда появляется справа или снизу. Без этого направление сплита зависит от положения курсора и пропорций текущего окна, и предсказать результат невозможно.
Второе — группы. Несколько окон схлопываются в один слот с полоской вкладок сверху, и три терминала занимают одну колонку вместо трёх узких. На широком экране это оказывается полезнее, чем кажется на обычном 16:9.
Отступы сведены к gaps_in = 8 и gaps_out = 12. Прежние 5 и 20 на таком мониторе съедали заметную полосу по краям при том, что внутренние границы почти не читались.
Скретчпады как плавающие панели
Telegram, Spotify и Claude — приложения жанра «выглянул, ответил, спрятал». Держать их на нумерованных воркспейсах расточительно, а сваливать в один общий special-воркспейс неудобно: он показывает всех сразу, тайлингом.
Решение — отдельный special-воркспейс на приложение, и окна в них плавающие с фиксированной геометрией:
hl.window_rule({
name = "scratchpad-claude",
match = { class = "^com.anthropic.Claude$" },
workspace = "special:ai",
float = true,
size = "1400 1330",
move = "monitor_w-1420 50",
})
Панель прижата к правому краю и занимает 1400 пикселей из 3440 — под ней остаётся видно и читаемо то, чем ты занимался. Это и есть главный выигрыш: панель не подменяет экран, а ложится поверх него.
Клавиша работает тумблером за счёт Lua-функции в биндинге:
local function scratchpad(ws, class, cmd)
return function()
if #hl.get_windows({ class = class }) == 0 then
hl.exec_cmd("uwsm app -- " .. cmd)
else
hl.dispatch(hl.dsp.workspace.toggle_special(ws))
end
end
end
Если приложение не запущено, правило окна само положит его в нужный воркспейс и покажет; если запущено — клавиша просто переключает видимость.
Лаунчер приложений
Super+D открывает hyprlauncher. Тонкость, которая экономит время: собственный
конфиг лаунчера отвечает только за поведение — размер окна, режимы поиска, кэш.
Всё оформление живёт в ~/.config/hypr/hyprtoolkit.conf, и этот файл читают все
приложения экосистемы, построенные на hyprtoolkit. Ключей там немного:
background = rgba(1d2021e6)
accent = rgba(a9b665ff)
accent_secondary = rgba(89b482ff)
bright_text = rgba(d4be98ff)
font_family = JetBrainsMono Nerd Font
rounding_small = 8
icon_theme = Adwaita
Лаунчер — это layer-surface на уровне overlay с namespace hyprlauncher, а не
обычное окно, поэтому размытие фона ему даётся правилом слоя, как панели:
hl.layer_rule({
name = "blur-launcher",
match = { namespace = "^hyprlauncher$" },
blur = true,
xray = false,
ignore_alpha = 0.1,
})
xray = false здесь существеннее, чем кажется: с включённым xray размывались бы
только обои, и лаунчер поверх окна с текстом читался бы плохо.
Режимы поиска переключаются префиксом первого символа: = — калькулятор, . —
поиск символа юникода, ' — просмотр шрифтов, без префикса — приложения. Список
пуст, пока не введён первый символ; это поведение hyprlauncher, а не поломка.
Утечка дескриптора сокета
Отдельная ловушка, на которую легко потратить вечер. hyprlauncher слушает
управляющий сокет /run/user/$UID/.hyprlauncher.sock и не ставит на него
FD_CLOEXEC. Приложение, запущенное из лаунчера напрямую, наследует дескриптор —
и после выхода лаунчера сокет остаётся живым внутри этого приложения. Следующий
запуск лаунчера находит слушающий сокет, считает, что демон уже работает, и
уходит туда клиентом: процесс стартует, окно не появляется, в логах пусто.
Диагностика — посмотреть, кто на самом деле владеет сокетом:
ss -lxp | grep hyprlauncher
Если в владельцах не hyprlauncher, а произвольное приложение, дескриптор течёт. Разовое лечение — удалить файл сокета. Системное — запускать приложения через uwsm:
finders {
desktop_launch_prefix = "uwsm app -- "
}
uwsm поднимает процесс через systemd, а не форком лаунчера, поэтому наследовать дескриптор некому. Побочная выгода — приложения из лаунчера попадают в собственные systemd-скоупы, как и всё остальное в сессии.
Часы и календарь в панели
Наведение на часы показывает три месяца сразу — предыдущий, текущий и следующий, с номерами недель у каждого и залитым сегодняшним днём. Прокрутка над часами двигает это трёхмесячное окно, правый клик возвращает к текущему месяцу.
Штатный модуль clock так не умеет: у него есть режимы «месяц» и «год», между
которыми нет ничего, а соседние месяцы доступны только прокруткой — по одному и
вслепую. Поэтому вместо него стоит custom/clock с генератором на Python,
который печатает JSON с полями text и tooltip.
Замена оказалась выгодна не только видом. Календарь строится модулем calendar
из стандартной библиотеки, а он локале-независим: английские названия и
понедельник первым получаются сами, без возни с локалями — а возня предстояла
серьёзная, см. ниже.
Смещение месяца хранится в $XDG_RUNTIME_DIR/waybar-cal-offset. Обработчик
прокрутки пишет туда новое значение и будит модуль сигналом:
printf '%s\n' "$new" > "$file"
pkill -RTMIN+8 waybar
Число в SIGRTMIN+8 должно совпадать с "signal": 8 в конфиге модуля.
Ширина тултипа: GTK молча переносит строки
Тултип GTK переносит строку примерно на 73–74 символах. Для календаря это катастрофа: сетка рассыпается, и понять причину по виду невозможно — выглядит как случайно съехавшая вёрстка.
Порог удобно замерить линейкой: вывести в тултип несколько строк известной длины и посмотреть, какая переносится. На проверяемой системе 72 символа влезали, 75 переносились. Отсюда и раскладка: три блока по 23 символа плюс два зазора по два пробела — ровно 73.
Локаль дат: две независимые системы
Ловушка штатного модуля clock, для нынешней конфигурации уже неактуальная, но
живучая и стоящая описания.
Waybar рендерит даты в строке tooltip-format всегда английской локалью. Ни
опция locale у модуля, ни LC_ALL при запуске процесса на это не влияют —
проверено экспериментально. Локаль управляет только сеткой календаря, которую
модуль строит отдельно. Если системная LC_TIME не английская, а locale не
задана, в одном окне окажутся два языка.
Имя локали при этом должно совпадать с выводом locale -a в точности:
en_US.utf8, а не en_US.UTF-8. При несовпадении опция молча игнорируется, без
сообщения в логе.
Из той же локали берётся первый день недели — собственной опции у waybar
нет. en_US ставит первым воскресенье (first_weekday=1), понедельник даёт
en_GB или любая европейская. Проверяется через LC_ALL=xx locale first_weekday.
Если нужной локали в системе нет, генерировать её глобально не обязательно —
localedef умеет собирать в произвольный каталог без root, а программе
указывается LOCPATH. Важная тонкость: LOCPATH заменяет путь поиска, а не
дополняет его — с ним программа перестаёт видеть системные локали и падает на
C, о чём GTK честно пишет в лог. Поэтому переменную задают одному приложению,
никогда не всей сессии.
Всё это, впрочем, обходится стороной, если календарь строить самому: cal -m
или питоновский calendar дают понедельник первым независимо от локали.
Простой, блокировка, деградация
hypridle настроен тремя ступенями: через 5 минут приглушается подсветка, через 10 минут срабатывает loginctl lock-session, через 15 гаснет монитор. Замок в панели waybar отключает всё это целиком на время видео или долгой сборки.
Первая ступень на этой машине холостая, и это стоит знать заранее. brightnessctl управляет яркостью через /sys/class/backlight, а на десктопе с внешним монитором такого устройства нет — каталог пуст. Не найдя backlight, утилита берёт первое устройство класса leds, и brightnessctl -s set 10% уезжает в светодиод kana на клавиатуре. Ошибки нет, код возврата нулевой, яркость монитора не меняется. Единственный способ управлять ей программно — DDC/CI через ddcutil, то есть посылка команд по I²C самому монитору.
У hyprlock выставлен grace = 2: две секунды форы, в течение которых случайное движение мыши разблокирует экран без ввода пароля. Раскладка выведена в угол, а CapsLock и NumLock подсвечивают рамку поля ввода — это снимает самый частый вопрос «почему пароль не подходит».
Отдельный принцип, заложенный во все вспомогательные скрипты: ни одна клавиша не молчит. Там, где возможен откат, он работает без внешней программы; там, где нет — приходит уведомление с готовой командой установки. Пример — обёртка над swayosd:
osd() {
command -v swayosd-client >/dev/null 2>&1 || return 1
swayosd-client "$@" >/dev/null 2>&1
}
case "$action" in
vol-up) osd --output-volume raise || wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 5%+ ;;
esac
Проверка идёт по коду возврата, а не по наличию бинарника, и это не педантизм: swayosd умеет быть установленным и при этом нерабочим (см. ниже).
Монитор не выходит из энергосбережения
Связка NVIDIA + DisplayPort даёт неприятный отказ: после долгого dpms off панель роняет линк, и обратного dpms on не хватает. Композитор при этом полностью жив — окна на месте, клавиши с locked = true отрабатывают, музыка переключается вслепую, — но картинки нет. Внешне неотличимо от зависшей машины, отчего рефлекторно хочется нажать reset.
Помогает не включение как таковое, а полный modeset. Диспетчер force_renderer_reload переинициализирует рендерер и все выходы, чем и заставляет драйвер заново провести link training:
#!/bin/sh
# Три ступени по нарастанию силы: узнать изнутри, помогла ли предыдущая,
# нельзя, поэтому выполняются все подряд.
hyprctl dispatch 'hl.dsp.dpms("on")'
sleep 1
hyprctl dispatch 'hl.dsp.force_renderer_reload()'
sleep 1
hyprctl eval 'hl.monitor({
output = "DP-1", mode = "3440x1440@144",
position = "0x0", scale = 1, vrr = 2,
}) return "ok"'
Скрипт повешен на SUPER + CTRL + W обязательно с locked = true — к моменту, когда он понадобится, сессия уже заблокирована hyprlock, и без этого флага бинд просто не сработает.
Последний рубеж, если не помогло и это, — переключение на текстовую консоль и обратно (Ctrl+Alt+F2, затем Ctrl+Alt+F1). Тогда modeset делает ядро, в обход композитора целиком.
Единая палитра
Тема — Gruvbox Material Dark, вариант hard. Цвета продублированы в шести файлах и должны меняться синхронно: таблица c в начале hyprland.lua, блок @define-color в waybar/style.css, hex-значения в mako/config, hyprlock.conf, hyprtoolkit.conf (все приложения экосистемы) и satty/config.toml. Терминал живёт своей жизнью в kitty/current-theme.conf.
Отдельная ловушка — GTK. Тёмная тема в GTK3 задаётся через gtk-application-prefer-dark-theme=1 в settings.ini, а в GTK4 этот ключ не работает: libadwaita его игнорирует и пишет предупреждение в каждый запуск приложения. Там управляет gsettings:
gsettings set org.gnome.desktop.interface color-scheme 'prefer-dark'
Без этого GTK-диалоги — файловые пикеры, pavucontrol — открываются светлыми посреди тёмного окружения.
Развёртывание с нуля
Пакеты сверх базовой установки CachyOS с Hyprland:
sudo pacman -S hypridle cliphist swayosd satty fuzzel hyprpicker brightnessctl
swayosd требует ещё системной службы — именно она владеет именем org.erikreider.swayosd на системной шине D-Bus:
sudo systemctl enable --now swayosd-libinput-backend.service
Порядок важен. Если swayosd-server был запущен раньше этой службы, он её не подхватит, и клиент продолжит получать ServiceUnknown даже после включения — сервер нужно перезапустить. При обычном входе в сессию порядок выстраивается сам.
Тема курсора задаётся явно, иначе XWayland-приложения получают дефолтный X-курсор и он «прыгает» между контекстами: XCURSOR_THEME и HYPRCURSOR_THEME в конфиге, gsettings для GTK и ~/.icons/default/index.theme для legacy-X11.
Ловушки Lua-конфига
Все четыре проверены экспериментально на Hyprland 0.56.2. Объединяет их то, что ни одна не даёт ошибки — конфиг загружается «чисто», а поведение молча отличается от ожидаемого.
hyprctl dispatch принимает Lua-выражение, а не классический синтаксис. Команда hyprctl dispatch dpms off падает с ошибкой парсинга; работает hyprctl dispatch 'hl.dsp.dpms("off")'. Это ломает всё, что вызывает hyprctl снаружи конфига: скрипты, hypridle, чужие сниппеты из интернета.
hyprctl keyword не работает вовсе. Это продолжение той же истории, но выхода в виде «написать по-другому» здесь нет — команда отвечает keyword can't work with non-legacy parsers. Use eval. и не делает ничего. То есть массовый рецепт «перенастроить монитор на лету» в привычном виде на Lua-провайдере недоступен:
hyprctl keyword monitor "DP-1, 3440x1440@144, 0x0, 1" # не сработает
hyprctl eval 'hl.monitor({
output = "DP-1", mode = "3440x1440@144",
position = "0x0", scale = 1, vrr = 2,
}) return "ok"'
Заодно стоит помнить, что hyprctl eval возвращает только ok, а не значение выражения. Для интроспекции живого конфига — обойти hl в цикле и записать результат в файл через io.open.
Поле class матчится по-разному в двух местах. В hl.window_rule{ match = { class = ... } } это регулярное выражение, а в hl.get_windows{ class = ... } — точное совпадение с учётом регистра. Регулярка во втором случае вернёт пустой список без единого предупреждения, и тумблер скретчпада начнёт запускать новую копию приложения при каждом нажатии.
Правило move не понимает проценты. Запись move = "100%-820 50" игнорируется, и окно просто центрируется. Работают формы monitor_w-820 50 и абсолютные пиксели. Из-за этого типовое правило для Firefox Picture-in-Picture, которое кочует по конфигам в интернете, на Lua-провайдере не срабатывает.
К ним примыкает пятая, уже не про Lua: swayosd-client может быть установлен и не работать. Он обращается за именем org.erikreider.swayosd на системную шину, а поднимает это имя отдельная служба, выключенная по умолчанию. Пока она выключена, клиент возвращает 1 и не делает ничего — поэтому откат в скриптах и завязан на код возврата.
Связанные заметки
- Hyprland: горячие клавиши и скретчпады — ежедневная шпаргалка по этой же машине
- Kitty terminal guide — терминал, на котором построена работа в этом окружении
- Linux — systemd: init, сервисы, таймеры — скоупы и пользовательские юниты, через которые uwsm запускает приложения
- Linux — пакетные менеджеры — pacman, которым ставятся компоненты окружения
- Fish Shell — справка — оболочка по умолчанию на этой машине
Из библиотеки института уносят только знания. Читателей здесь не считают: посещение не регистрируется, отметок в издании не остаётся, счётчиков нет ни у одного издания фонда.
Издание пополняется и пересверяется. Текст доступен на условиях CC BY-SA 4.0 — унесённое можно передавать дальше на тех же условиях.