---
tags: [linux, tooling]
verified: 2026-09-01
---

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

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

## Обзор

Заметка описывает конкретную машину, а не технологию Hyprland вообще — тот же жанр, что [медиасервер](../../media-server-guide.md). Здесь зафиксировано, из чего собрано домашнее рабочее окружение, почему принято то или иное решение и что понадобится, если поднимать его заново.

Все факты ниже проверены на живой системе: команды выполнялись, поведение наблюдалось. Там, где обнаружилось расхождение с ожиданиями (а его оказалось немало — см. раздел про ловушки), записан результат эксперимента, а не то, как «должно быть».

Ежедневная работа — клавиши, панели, скретчпады — вынесена в отдельную шпаргалку: [Hyprland: горячие клавиши и скретчпады](./hyprland-keybindings.md).

## Железо и сессия

Машина построена вокруг одного ультраширокого монитора, и это определяет почти все решения по раскладке окон.

| Компонент | Значение |
|---|---|
| Дистрибутив | 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-скоуп. Проверить, что автозапуск отработал:

```bash
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, и это принципиально меняет несколько вещей. Проверить, какой провайдер активен:

```bash
hyprctl systeminfo | grep configProvider
```

Lua даёт то, чего hyprlang не умеет: в конфиге доступны запросы к состоянию композитора (`hl.get_windows`, `hl.get_active_workspace`), таймеры, подписка на события и биндинги на произвольные Lua-функции, а не только на диспетчеры. Скретчпады ниже построены именно на этом — логика «если приложение не запущено, запусти; иначе покажи или спрячь» живёт прямо в конфиге, без внешних скриптов и разбора JSON.

Плата за это — несовместимость привычных команд, см. раздел про ловушки.

Проверка после любой правки:

```bash
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-воркспейс на приложение, и окна в них плавающие с фиксированной геометрией:

```lua
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-функции в биндинге:

```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. Ключей там немного:

```conf
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`, а не
обычное окно, поэтому размытие фона ему даётся правилом слоя, как панели:

```lua
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`. Приложение, запущенное из лаунчера напрямую, наследует дескриптор —
и после выхода лаунчера сокет остаётся живым внутри этого приложения. Следующий
запуск лаунчера находит слушающий сокет, считает, что демон уже работает, и
уходит туда клиентом: процесс стартует, окно не появляется, в логах пусто.

Диагностика — посмотреть, кто на самом деле владеет сокетом:

```bash
ss -lxp | grep hyprlauncher
```

Если в владельцах не hyprlauncher, а произвольное приложение, дескриптор течёт.
Разовое лечение — удалить файл сокета. Системное — запускать приложения через
uwsm:

```conf
finders {
  desktop_launch_prefix = "uwsm app -- "
}
```

uwsm поднимает процесс через systemd, а не форком лаунчера, поэтому наследовать
дескриптор некому. Побочная выгода — приложения из лаунчера попадают в
собственные systemd-скоупы, как и всё остальное в сессии.

## Часы и календарь в панели

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

Штатный модуль `clock` так не умеет: у него есть режимы «месяц» и «год», между
которыми нет ничего, а соседние месяцы доступны только прокруткой — по одному и
вслепую. Поэтому вместо него стоит `custom/clock` с генератором на Python,
который печатает JSON с полями `text` и `tooltip`.

Замена оказалась выгодна не только видом. Календарь строится модулем `calendar`
из стандартной библиотеки, а он локале-независим: английские названия и
понедельник первым получаются сами, без возни с локалями — а возня предстояла
серьёзная, см. ниже.

Смещение месяца хранится в `$XDG_RUNTIME_DIR/waybar-cal-offset`. Обработчик
прокрутки пишет туда новое значение и будит модуль сигналом:

```bash
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:

```bash
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:

```bash
#!/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`:

```bash
gsettings set org.gnome.desktop.interface color-scheme 'prefer-dark'
```

Без этого GTK-диалоги — файловые пикеры, pavucontrol — открываются светлыми посреди тёмного окружения.

## Развёртывание с нуля

Пакеты сверх базовой установки CachyOS с Hyprland:

```bash
sudo pacman -S hypridle cliphist swayosd satty fuzzel hyprpicker brightnessctl
```

swayosd требует ещё системной службы — именно она владеет именем `org.erikreider.swayosd` на системной шине D-Bus:

```bash
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-провайдере недоступен:

```bash
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: горячие клавиши и скретчпады](./hyprland-keybindings.md) — ежедневная шпаргалка по этой же машине
- [Kitty terminal guide](../../kitty-guide.md) — терминал, на котором построена работа в этом окружении
- [Linux — systemd: init, сервисы, таймеры](../../linux-systemd.md) — скоупы и пользовательские юниты, через которые uwsm запускает приложения
- [Linux — пакетные менеджеры](../../linux-package-managers.md) — pacman, которым ставятся компоненты окружения
- [Fish Shell — справка](../../fish-reference.md) — оболочка по умолчанию на этой машине
