Linux
October 19

Пишем виджеты для оконных менеджеров. Гайд на eww

eww (Elkowars Wacky Widgets) - это автономная система виджетов, созданная на Rust, которая позволяет реализовать собственные виджеты в любом оконном менеджере.

К большому сожалению информации про создание виджетов с помощью eww (Elkowars Wacky Widgets) в сети очень мало, причем я имею ввиду не только рунет. Даже на зарубежных источниках сложно отыскать достойный гайд по этой теме. Да, есть официальная документация, но она не так уж и хороша, как хотелось бы. В ней мало примеров и информация предоставлена в сжатом виде. Я, увидев такую ситуацию подумал, а почему бы не написать свой собственный гайд? И вот, ты сейчас его читаешь. Здесь я постараюсь максимально подробно и понятно рассказать тебе, как создавать свои собственные виджеты с помощью eww. Более того, я постарался сюда добавить как можно больше примеров кода, чтобы было нагляднее.

Предварительно, тебе нужно хорошо знать Linux (чуть больше, чем обычный пользователь), хоть немного знать CSS и желательно иметь небольшой опыт в программировании. Опыт в программировании необязателен, но с ним будет легче влиться в тему.
Также учти, что вся предоставленная информация будет актуальна для пользователей Arch Linux. Если ты используешь Debian, то скорее всего, некоторые команды придется адаптировать под твой дистрибутив.


Установка:

Предварительно, мы должны установить rustc и cargo.
В официальной документации нам настоятельно рекомендуют установить все это с помощью Rustup:

Что-ж, давай так и сделаем:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Еще нам необходимо установить некоторые пакеты:

  • gtk3 (libgdk-3, libgtk-3)
  • gtk-layer-shell (только на Wayland)
  • pango (libpango)
  • gdk-pixbuf2 (libgdk_pixbuf-2)
  • libdbusmenu-gtk3
  • cairo (libcairo, libcairo-gobject)
  • glib2 (libgio, libglib-2, libgobject-2)
  • gcc-libs (libgcc)
  • glibc
    Сделать это можно следующей командой:
    sudo pacman -S gtk3 gtk-layer-shell pango gdk-pixbuf2 libdbusmenu-gtk3 cairo glib2 gcc-libs glibc

Теперь устанавливаем сам eww:
git clone https://github.com/elkowar/eww
cd eww
cargo build --release --no-default-features --features x11
ПРИМЕЧАНИЕ: Если ты юзаешь Wayland, то используй эту команду:
cargo build --release --no-default-features --features=wayland

Наконец, мы можем запустить eww:
cd target/release
chmod +x ./eww
./eww daemon

Если на этапе запуска ты столкнулся с ошибкой Configuration directory /home/freemore/.config/eww does not exist, то просто переходи к следующему шагу. Там я расскажу о том, какие нужно создать файлы конфигурации, после чего ты сможешь запустить eww.


Конфигурация eww:

Eww использует собственный язык yuck. Он предназначен для создания структуры, геометрии, положения, поведения и содержимого виджетов. Yuck основан на S-выражениях, которые тебе могут быть известны из lisp-подобных языков и именно поэтому в нем очень чрезмерно много используются круглые скобки и к синтаксису привыкнуть будет трудновато. Ты можешь использовать yuck.vim для подсветки синтаксиса в редакторе Vim или yuck-vscode для подсветки синтаксиса в редакторе VSCode, но его нет в официальном магазине расширений.

Стили определяются с помощью CSS или SCSS, но eww поддерживает не весь CSS, т.к. использует собственный CSS-движок GTK. В основном, не поддерживаются некоторые функции для создания анимаций, а также большинство свойств CSS, которые связаны с макетом (например, flexbox, float, width или height).

Все файлы конфигурации должны храниться в $XDG_CONFIG_HOME/eww (чаще всего это ~/.config/eww). Создай эту директорию и еще 2 файла в ней:
eww.yuck
eww.css или eww.scss, в зависимости от того, что ты предпочитаешь.


Первое окно:

Для начала мы создаем окно. Окно - это пространство, в котором будет помещен виджет (да-да, окно - это не виджет). В одном окне может быть помещен один виджет, а сам виджет может помещать в себе еще один виджет. Кажется, что сложно, но на самом деле все просто. Покажу на практике. Пишем код в eww.yuck:

(defwindow example

	:monitor 0
	:geometry (geometry :x "0%"
						:y "20px"
						:width "500px"
						:height "500px"
						:anchor "top center")
	:stacking "fg"
	:windowtype "dock"
	:wm-ignore false

(example_widget))

В этом примере мы создаем окно с именем example со следующими характеристиками:

  • monitor 0 - указываем, на каком мониторе расположить окно. Это может быть число или название монитора. Также есть возможность использовать строку primary или строку, содержащую JSON-массив, но я не представляю для чего это, поэтому пропустим + может не сработать, особенно на Wayland.
  • geometry - геометрия окна. Помимо размеров можно указать и другие характеристики, например положение окна.
  • stacking "fg" - где в стеке должно появиться окно. fg - поверх окон или bg - за окнами. Например, если ты хочешь сделать что-то похожее на Conky, то bg будет предпочтительным.
  • windowtype "dock" - тип окна. Это будет учитываться оконным менеджером, чтобы определить, как следует обращаться с этим окном. normal, dock, toolbar, dialog или desktop.
  • wm-ignore false - Должен ли оконный менеджер игнорировать это окно. Это полезно для виджетов в стиле информационной панели, которым вообще не нужно взаимодействовать с другими окнами. Или true или false.
  • (example_widget) - указываем название виджета (который мы создадим далее)

Подробнее о характеристиках defwindow см. здесь.


Первый виджет:

Теперь создаем сам виджет, который будет отображаться в ранее созданном окне:

(defwidget powermenu_layout []
  (label :text "Hello, World!"))

Здесь мы создаем виджет с названием example_widget. В него мы помещаем label, который отображает текст. По сути, label тоже является виджетом (ранее я рассказывал, что в одном виджете может быть расположен еще один виджет). В нашем случае, текстом является "Hello, World!".

У eww есть несколько готовых виджетов, которые мы можем использовать для создания своих собственных и label - это один из них.

В итоге у нас получился вот такой код:

(defwindow example

	:monitor 0
	:geometry (geometry :x "0%"
						:y "20px"
						:width "500px"
						:height "500px"
						:anchor "top center")
	:stacking "fg"
	:windowtype "dock"
	:wm-ignore false

(example_widget))

(defwidget example_widget []
  (label :text "Hello, World!"))

Попробуем запустить:
./eww open example

Если на этом этапе у тебя открылось окно, в котором отображается текст "Hello World!", то все правильно и мы можем идти дальше. Самый простой способ закрыть это окно - xkill или pkill eww.

На самом деле, код выше можно сократить, т.к. label не обязательно явно указывать. Достаточно указать текст в самом окне:

(defwindow example

	:monitor 0
	:geometry (geometry :x "0%"
						:y "20px"
						:width "500px"
						:height "500px"
						:anchor "top center")
	:stacking "fg"
	:windowtype "dock"
	:wm-ignore false

"Hello, World!")


О виджетах:

Хотелось бы немного заострить внимание на виджетах. Во-первых, они принимают атрибуты, которые можно передавать при вызове в окне. Вот пример:

(defwindow example
  :monitor 0
  :geometry (geometry
              :width "0%"
              :height "20px"
              :anchor "top center")
  :stacking "fg"
  :windowtype "dock"
  :wm-ignore false
  
(example_widget :text "Say hello!"
                :name "Tim"))

(defwidget example_widget [?text name]
  (box :orientation "horizontal"
       :halign "center"
    text
    (button :onclick "notify-send 'Hello' 'Hello, ${name}'"
    "Greet")))

Сначала обрати внимание на виджет. Там присутствует [?text name], который означает, что у виджета есть 2 атрибута. Первый - ?text, который указывает, что этот атрибут является необязательным и может быть пропущен. В таком случае его значением будет пустая строка "". А вот атрибут name должен быть обязательно указан. В окне мы вызываем виджет и передаём значения атрибутов:

В виджете example_widget мы вызываем виджет box, который позволяет делать вложенные виджеты. Именно с помощью box мы и можем в одном виджете располагать несколько виджетов, т.к. box определяет как их отображать, выравнивать и т.д. В box мы установили ссылку на атрибут text и кнопку, которая при нажатии будет отправлять уведомление (для этого должен быть установлен демон уведомлений, например swaync). Обрати внимание, что при вертикальной ориентации, виджеты располагаются сверху вниз, а при горизонтальной, слева направо.

узнать больше о виджетах и их свойствах можно из документации.


Интерактивные и динамические виджеты:

Теперь поговорим про интерактивные и динамические виджеты. Давай представим, что мы хотим создать виджет, который должен отображать текущее состояние батареи. Если этот виджет будет открыт, то он покажет состояние батареи, но вот если батарея разрядится - информация в виджете не обновится, пока мы его не перезапустим. Это интерактивный виджет. Динамический же будет периодически обновлять информацию и поэтому его не придется перезапускать для получения текущего состояния батареи. Как такой виджет сделать? Очень просто! Для этого нужно добавить опрос переменной. Вот пример с отображением времени:

(defpoll time :interval "5s"
  `date +'{"hour":"%H","min":"%M"}'`)


(defwidget example_widget []
  (label :text time))

defpoll в этом случае выполняет команду `date +'{"hour":"%H","min":"%M"}'`)` с интервалом 5 секунд, а виджет label отображает результат этой команды.
Вот еще некоторые способы вывести информацию на виджет:

(label :text {time.min}) ;; отображает минуты - т.е. полезно для JSON
(label :text "${time.hour} :: ${time.min}") ;; отображает час :: минуты

Больше о типах переменных см. в документации.

Также существуют магические переменные, которые предназначены для того, чтобы не делать их самостоятельно. Причем информация в таких переменных обновляется автоматически, то есть использовать defpoll уже не нужно. Например, можно получить информацию о батарее с помощью EWW_BATTERY. Чтобы использовать такую переменную, достаточно просто ее указать в коде:

(defwidget example_widget []
  (label :text EWW_DISK))

Вывод будет примерно такой:
{ <name>: { capacity, status } }

Посмотреть магические переменные можно здесь.


Пишем powermenu:

Оригинальный исходный код, а также некоторая информация, которую ты можешь прочитать далее, были взяты отсюда:
EWW Powermenu - dead airspace

Для начала нам нужно определить размеры окна. Я хочу, чтобы виджет был на весь экран и отображался поверх других окон на втором мониторе:

(defwindow powermenu
  :monitor 1
  :stacking "fg"
  :windowtype "normal"
  :wm-ignore true
  :geometry (geometry :width "100%" :height "100%")
  (powermenu_layout))

Теперь я хочу получить данные о батарее и вывести их на экран. Для этого будем использовать переменную EWW_BATTERY. Узнать ключевое значение интерфейса батареи можно в /sys/class/power_supply. У меня - BAT1:

Мы воспользуемся EWW_BATTERY.BAT1.capacity и EWW_BATTERY.BAT1.status. Также добавим получение данных о времени:

(defpoll time :interval "5s"
  :initial `date +'{"hour":"%H","min":"%M"}'`
  `date +'{"hour":"%H","min":"%M"}'`)

Далее добавим виджет с кнопками. Нам нужны кнопки: выключение, перезагрузка, блокировка экрана.

(defwidget _buttons [shutdown shutdown_icon reboot
                    reboot_icon lock lock_icon]
  (box :class "btns-box" :spacing 5
       :vexpand true :hexpand true
       :valign "end" :halign "end"
       :space-evenly false
    (button :onclick shutdown shutdown_icon)
    (button :onclick reboot reboot_icon)
    (button :onclick lock lock_icon)))

Что делает этот код?
В нем мы указываем, что хотим использовать виджет с названием _buttons, в котором присутствуют обязательные параметры: shutdown, shutdown_icon, reboot, reboot_icon, lock и lock_icon. Далее мы создаем вложеннный виджет box, в котором указываем следующие обязательные параметры:
class - CSS-класс, который мы пропишем потом в файле eww.scss;
spacing - расстояние между элементами.

Нам нужно отобразить информацию о батарее. Текущая мощность батареи классифицируется на восемь уровней, а именно one, two, three, four, five, six, seven и charge:

(defwidget _battery [battery status one two three
                    four five six seven charge]
  (box :class "bat-box" :space-evenly false :spacing 8
    (label :text {status == 'Charging' ? charge :
      battery < 15 ? seven :
        battery < 30 ? six :
          battery < 45 ? five :
            battery < 60 ? four :
              battery < 75 ? three :
                battery < 95 ? two : one})))

Теперь объединяем все виджеты в powermenu_layout:

(defwidget powermenu_layout []
  (box :class "layout-box" :space-evenly false :orientation "vertical"
       :style "background-image: url('./wallpaper')"
    (box :valign "start" :space-evenly false :spacing 25
      (_battery :status {EWW_BATTERY.BAT0.status}
                :battery {EWW_BATTERY.BAT0.capacity}
                :charge "" :one "" :two "" :three "" :four ""
                :five "" :six "" :seven "")
      (button :onclick "cd ~/eww/target/release && ./eww close powermenu" :class "close-btn" ""))
    (box :space-evenly false :hexpand true :vexpand true
      (box :spacing 15 :class "tm-box" :space-evenly false
            :valign "end" :halign "start"
        (label :text "${time.hour}:${time.min}"))
      (_buttons :shutdown "poweroff" :reboot "reboot"
                :lock "loginctl kill-session self"
                :shutdown_icon "" :reboot_icon ""
                :lock_icon "󰌾"))))

И создаем стиль для нашего powermenu. Код, представленный далее я уже не буду объяснять, т.к. CSS - это отдельная тема, но могу смело заявить, что в CSS нет ничего сложного и ты можешь освоить его базу для написания простых стилей за 1 день.
В данном примере мы используем SCSS, что является более сложным, но при этом и более функциональным решением:

$surface-darkgrey: #1d2021;
$surface-fg: #fbf1c7;
$surface-lightgrey: #282828;
$surface-grey: #3c3836;
$surface-red: #fb4934;

* { all: unset; }

.layout-box {
  font-family: Phosphor, Koulen;
  background-repeat: no-repeat;
  background-size: contain;
  padding: 5em;
  color: rgba($surface-fg, 0.8);
}

.net-box,
.bat-box,
.tm-box {
  label {
    font-size: 2em;
  }
}

.close-btn {
  font-size: 2em;
  &:hover {
    color: $surface-red;
  }
}

.btns-box {
  font-size: 2.5em;

  button {
    padding: 0.4em;
    border-radius: 0.1em;
    background-color: rgba($surface-darkgrey, 0.3);

    &:hover {
      transition: 200ms linear background-color, border-radius;
      background-color: rgba($surface-lightgrey, 0.6);
    }

    &:first-child {
      color: rgba($surface-red, 0.8);
    }
  }
}

.sep {
  font-size: 1.5em;
  padding-top: 0.15em;
  padding-left: 0.2em;
  padding-right: 0.2em;
  color: $surface-lightgrey;
  }

.sundial-lbl {
  font-size: 1.5em;
  font-weight: bold;
  border-radius: 0.2em;
  padding: 0.4em;
  padding-bottom: 0.5em;
  font-family: "Fantasque Sans Mono";
}

В итоге получилось так:

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


Мой Telegram

Мой GitHub

Поддержать автора донатом