Библиотека — Домашний десктоп на CachyOS: развёртывание и конфигурация [X]
Шифр
hyprland-desktop
Разделы
linux tooling
Сверено
1 сентября — в этот день утверждения статьи сверялись с первоисточниками, а не правился текст
Объём
18 704 знака · 17 разделов · чтения около 20 минут

Домашний десктоп на CachyOS: развёртывание и конфигурация


Конкретная сборка, а не руководство. Заметка описывает одну машину и устаревает вместе с ней; технология как таковая — в статьях из раздела «Связанные заметки».

Обзор

Заметка описывает конкретную машину, а не технологию 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 и не делает ничего — поэтому откат в скриптах и завязан на код возврата.


Связанные заметки

Из библиотеки института уносят только знания. Читателей здесь не считают: посещение не регистрируется, отметок в издании не остаётся, счётчиков нет ни у одного издания фонда.

Издание пополняется и пересверяется. Текст доступен на условиях CC BY-SA 4.0 — унесённое можно передавать дальше на тех же условиях.