Так совпало, что меня заинтересовали две вещи: язык программирования Haskell и фреймовые (или тайловые) оконные менеджеры под Linux. И тут я обнаружил замечательный тайловый менеджер, написанный на Haskell. Я не смог пройти мимо и установил себе xmonad.
Эта статья будет не совсем обычная для этого блога. Здесь практически не будет программирования. Я просто покажу Вам как настроить окружение на основе оконного менеджера xmonad и панелей xmobar.

Приведу сразу же скриншоты своего окружения. На первом, моё рабочее пространство без открытых окон.

А на втором запущено несколько оконных приложений

Сразу после установки xmonad, Вы, скорее всего, не сможете даже запустить какое-либо приложение. Вам придётся сходить в терминал и прочесть man xmonad, где описаны все дефолтные сочетания клавиш. Дабы облегчить Вам задачу, сразу же приведу основные, без которых работать в среде невозможно:

  • Mod+p: открыть dmenu для запуска приложений;
  • Mod+Shift+Enter: открыть терминал;
  • Mod+Shift+c: закрыть текущее окно;
  • Mod+j: перевести фокус на следующее окно;
  • Mod+k: перевести фокус на предыдущее окно;
  • Mod+[1-9]: перейти на рабочее пространство [1-9];
  • Mod+Shift+[1-9]: перекинуть текущее окно на рабочее пространство [1-9];
  • Mod+q: перезагрузить xmonad;
  • Mod+Shift+q: выйти из xmonad.

Mod по умолчанию — клавиша Alt.
Никаких баров с xmonad не поставляется, но этот оконный менеджер хорошо интегрируется с xmobar и dzen2.

Базовая настройка xmonad

Для начала настроим оконный менеджер, а затем перейдём к барам, трею и дополнительным скриптам.
Xmonad настраивается путём описания настроек на языке Haskell. Если Вы не знакомы с этим замечательным языком, я советую Вам исправить эту ситуацию как можно скорее. Без базового понимания Haskell, Вы сможете только скопировать готовый конфиг и кастомизировать его по аналогии с уже написанным кодом. В то время как зная Haskel, Вам откроются безграничные просторы для фантазии.
Настройки xmonad должны находиться в файле ~/.xmonad/xmonad.hs.
Минимальный файл конфигурации должен содержать функцию main, в которой происходит вызов функции xmonad с передачей структуры XConfig, содержащей конфигурацию.

import XMonad

main = do xmonad $ defaultConfig

XConfig проще всего получить из функции defaultConfig и на основе настроек по умолчанию создать свои собственные.
Первое, что мне захотелось сделать — это изменить клавишу-модификатор  с Alt на Super, так как клавиша Alt используется многими другими приложениями. Сделать это очень легко, достаточно назначить полю modMask значение типа KeyMask, в нашем случае это mod4Mask.

import XMonad

main = do
xmonad $ defaultConfig {
modMask = mod4Mask }

Настройка xmobar и trayer

Теперь, когда мы знаем, как настраивать xmonad, было бы не плохо обзавестись тулбаром, который покажет время, загрузку CPU, а самое главное, информацию о том, где (в контексте xmonad) мы сейчас находимся. Кроме того, было бы здорово убрать почтовый клиент, jabber и прочие фоновые задачи в трей.
Xmobar — это бар, который написан на haskell специально для xmonad, но может работать и без него. Настройки xmobar можно передавать через параметры напрямую или через конфигурационный файл. Я предпочитаю второй способ.
Для отображения трея будем использовать программу trayer.
Основная идея заключается в том, что я хочу, чтобы у меня справа отображались часы и раскладка клавиатуры, слева индикаторы загрузки процессора и памяти и состояние xmonad. Между правой и левой панелью я хочу поместить трей. Для реализации этой идеи мне понадобится запустить два экземпляра xmobar.
Мой монитор имеете ширину 1920 пикселов, поэтому я буду исходить из неё.
Конфигурационный файл xmobar имеет синтаксис конструктора с именованными полями языка haskell. Все его поля очень подробно описаны в man xmobar, поэтому я лишь кратко опишу свои файлы.
Итак, я создал каталог ~/local/conf и положил туда два файла xmobar_left и xmobar_right. Вот содержимое файла xmobar_right.

Config { font = "xft:DejaVu Sans Mono:size=10:bold:antialias=true",
bgColor = "#000000",
fgColor = "#BBBBBB",
position = Static { xpos = 1690 , ypos = 0, width = 230, height = 20 },
commands = [
Run Date "%a %b %_d %Y %H:%M:%S" "date" 10,
Run Kbd [("us", "US"), ("ru", "RU")]
],
template = "<fc=green>%kbd%</fc> %date%"
}

Назначение полей font, bgColor и fgColor не требует объяснений. Содержимое поля position было установлено опытным путём. Эксперименты показали, что для отображения даты и раскладки клавиатуры потребуется ровно 230 пикселов. Так как трей я хочу сделать шириной в 250 пикселов, то начальная позиция правой панели будет в точке 1690 по оси X. Высота бара будет состовлять 20 пикселов.
Самые важные поля в конфиге — это commands и template. Поле commands представляет собой список команд, которые xmobar должен запустить. Список поддерживаемых команд можно найти в man xmobar. Команды имеют параметры. Команда Date принимает три аргумента: формат даты, переменная, которая будет использоваться в шаблоне для доступа к значению команды и частоту обновления в десятых долях секунды. Команда Kdb принемает список двуместных кортежей, описывающий доступные раскладки клавиатуры. Первый элемент кортежа — это наименование раскладки, а второй — отображаемое имя. В шаблоне к значению этой команды можно обратиться по предопределённому имени kdb.
В параметре template записывается шаблон выводимого текста. Переменные из параметра commands нужно использовать, обернув их в символы %. Кроме переменных можно использовать простой текст и разметку, указывающую цвет символов.
Далее приведу конфиг для левой панели.

Config { font = "xft:DejaVu Sans Mono:size=10:bold:antialias=true",
bgColor = "#000000",
fgColor = "#BBBBBB",
position = Static { xpos = 0 , ypos = 0, width = 1530, height = 20 },
commands = [ Run Cpu ["-S", "True", "-t", "CPU: <total>", "-L","5","-H","40","--normal","#FFFF00","--high","#FF0000"] 10,
Run Memory ["-S", "True", "-t","RAM: <usedratio> (<used>MiB of <total>MiB)"] 10,
Run XMonadLog
],
template = "%cpu% %memory% %XMonadLog%"
}

Здесь используются команды Cpu, Memory и XMonadLog. Команде Cpu передаётся два параметра. Первый параметр — это набор опций для отображения. Шаблон передаётся через опцию -t (опция и её значение указываются в подряд следующих элементах списка). Доступны следующие переменные: total, bar, user, nice, system, idle, iowait, значение которых известно любому линуксойду. Опция -S в True просит xmobar показывать знак единицы измерения величины (в данном случае %). Опции -L и -H задают нижний и верхний предел для раскрашивания выводимого значения соответственно цветами в оциях —normal и —high. До значения -L цвет текста будет по умолчанию, между -L и -H цвет будет браться из опции —normal, для значений выше и равных -H будет использоваться цвет —high. Вторым параметром команде Cpu передаётся частота обновления в десятых долях секунды.
Команда Memory аналогична команде Cpu, только отображает загрузку памяти. Для этой команды доступны переменные total, free, buffer, cache, rest, used, usedratio, usedbar и freebar.
Самая интересная команда, которая тут используется — это команда XMonadLog. Она не принимает никаких аргументов, а для её использования нам придётся кое-что написать в конфигурационном файле xmonad.
Запуск trayer`а достаточно тривиален.

trayer --edge top --align right --margin 230  --widthtype pixel --width 200 --heighttype pixel --height 20 --tint 0x0 --alpha 0 --transparent true

Думаю, что тут всё понятно и я не буду останавливаться на этой команде.
Теперь помещаем в файл ~/.xsession необходимые команды для запуска xmonad, xmobar`ов и trayer`а.

xmobar ~/local/conf/xmobar_left &
xmobar ~/local/conf/xmobar_right &
trayer --edge top --align right --margin 230 --widthtype pixel --width 200 --heighttype pixel --height 20 --tint 0x0 --alpha 0 --transparent true &
exec xmonad

Полная настройка xmonad

Если Вы перезапустите xmonad, то увидите, что окна перекрывают Ваш бар. Структура данных XConfig содержит несколько полей, которым можно передать функцию обработки некоторых событий. Для того, чтобы бары не перекрывались, следует определить функцию layoutHook и обернуть лейауты в конструктор AvoidStruts при помощи функции avoidStruts. Мы можем использовать лейауты по умолчанию, но лучше определим свой собственный список. Это позволит добавить собственные лейауты и настроить дефолтные.

import XMonad
import XMonad.Hooks.ManageDocks
import XMonad.Layout.NoBorders
import XMonad.Layout.Tabbed
import XMonad.Layout.StackTile

myLayouts = ( avoidStruts $ smartBorders $
Tall 1 (3/100) (1/2) |||
StackTile 1 (3/100) (1/2) |||
simpleTabbed ) |||
noBorders Full

main = do
xmonad $ defaultConfig {
modMask = mod4Mask,
layoutHook = myLayouts }

Новый код выделен полужирным шрифтом. Итак, мы создаём 4 лейаута:

  1. Tall имеет 2 столбца. В левом отображаются основные окна, а в правом вспомогательные. Конструктор принимает три аргумента. Первый — количество окон в основном (левом) столбце. Второй — процент, на который изменяется размер столбцов при увеличении и уменьшении. Последний — отношение ширины столбцов по умолчанию;
  2. StackTile. Все окна располагаются в вертикальном стеке. Параметры означают то же самое, что и в конструкторе Tall;
  3. simpleTabbed — функция, которая создаёт лейаут, в котором все окна максимально развёрнуты, а сверху экрана появляются вкладки с их заголовками;
  4. Full. Все окна максимально развёрнуты.

Лейауты объединяются оператором |||. Объединённые лейауты передаём функции smartBorders, которая убирает рамку окна, если оно одно. Далее вызывается функция avoidStruts для предотвращения перекрытия баров. Полноэкранный лейаут должен перекрывать бары, поэтому он не должен попадать под действие avoidStruts. А так как окно там всегда одно, то вместо smartBorders, мы применяем к этому режиму функцию noBorders для предотвращения отрисовки рамки. Документацию и список доступных лейаутов можно найти здесь.

Ещё одна беда заключается в том, что панель trayer принимает фокус и присутствует лишь на одном workspace`е. Чтобы решить эту проблему, нужно добавить в обработчик manageHook функцию manageDocks.

import XMonad
import XMonad.Hooks.ManageDocks
import XMonad.Layout.NoBorders
import XMonad.Layout.Tabbed
import XMonad.Layout.StackTile

myLayouts = ( avoidStruts $ smartBorders $
Tall 1 (3/100) (1/2) |||
StackTile 1 (3/100) (1/2) |||
simpleTabbed ) |||
noBorders Full

main = do
xmonad $ defaultConfig {
modMask = mod4Mask,
layoutHook = myLayouts,
manageHook = manageHook defaultConfig <+> manageDocks }

Хуки, как видно, объединяются оператором <+>. Кроме управления барами, в обработчике manageHook можно управлять поведением различных окон. Все доступные операции приведены в документации здесь. Я покажу, на примере своего конфига, как делать определённые окна плавающими.

myManageHook = composeAll [
className =? "Gtk-recordmydesktop" --> doFloat,
className =? "Xmessage" --> doFloat,
className =? "Gxmessage" --> doFloat,
className =? "Galculator" --> doFloat,
className =? "Gksu" --> doFloat ]
manageHook = manageHook defaultConfig <+> manageDocks <+> myManageHook

Здесь я получаю класс окна при помощи функции className и сравниваю его с образцом оператором =?. Если образец и класс совпадают, то окно передаётся функции doFload оператором —>, которая делает окно плавающим. Класс окна можно получить командой

$ xprop | grep CLASS

Кроме класса окна можно использовать имя приложения или заголовок окна. Все доступные операции описаны в документации.

В обработчике manageHook можно ловить окна и перенаправлять их на другой workspace по его ID функцией doShift. Идентификаторы (они же имена) передаются в XConfig в параметре workspaces, который является списком строк.

myWorkspaces = [ "1", "2", "3", "4", "5", "6", "7", "8", "9" ]
main = do
xmonad $ defaultConfig {
modMask = mod4Mask,
workspaces = myWorkspaces,
layoutHook = myLayouts,
manageHook = manageHook defaultConfig <+> manageDocks <+> myManageHook }

Конечно же Вы можете назвать ваши рабочие пространства как хотите. Эти имена будут отображаться в нашем xmobar.

Вот и пришло время настроить тот самый лог xmonad, который мы использовали в левом xmobar. Как сказано в man xmobar, мы должны указать в параметре logHook такое выражение

logHook = dynamicLogString defaultPP >>= xmonadPropLog

И это будет работать. Но вывод, предоставленный функцией defaultPP отображается одним цветом и немного неудобно (для меня) отформатирован. Функция dynamicLogString принимает экземпляр типа PP (pretty-printing) и отдаёт строку, отформатированную согласно этому параметру. Создать экземпляр PP лучше всего функцией xmobarPP.

myLog = dynamicLogString xmobarPP {
ppCurrent = xmobarColor "green" "" . wrap "[" "]",
ppTitle = xmobarColor "lightblue" "" . wrap "[" "]",
ppHidden = xmobarColor "yellow" "",
ppHiddenNoWindows = xmobarColor "darkgray" "",
ppLayout = xmobarColor "orange" "" }
logHook = myLog >>= xmonadPropLog,

Для работы этого кода необходимо подключить модуль XMonad.Hooks.DynamicLog.
Итак, лог выглядит следующим образом:

<workspace`ы> : <имя лейаута> : <заголовок текущего окна>

Разделителем по умолчанию между воркспейсами служит пробел, а между воркспейсами, именем лейаута и именем окна — двоеточие, окружённое пробелами. Разделители меня устраивают. В структуре PP мы меняем следующие параметры:

  • ppCurrent: имя текущего workspace`а. Его я отображаю зелёным цветом при помощи функции xmobarColor и оборачиваю в символы [ и ] стандартной функцией haskell wrap;
  • ppTile: заголовок текущего окна;
  • ppHidden: имена воркспейсов, на которых есть окна, но которые не являются текущими;
  • ppHiddenNoWindows: не текущие воркспейсы, на которых нет окон (по умолчанию не отображаются вообще);
  • ppLayout: имя лейаута.

Раз уж мы начали заниматься красивосятми, то, мне кажется, нужно поменять и рамку окон xmonad. Сделать это можно передачей параметров borderWidth, normalBorderColor и focusedBorderColor в XConfig.

main = do
xmonad $ defaultConfig {
modMask = mod4Mask,
borderWidth = 3,
normalBorderColor = "gray",
focusedBorderColor = "red",

workspaces = myWorkspaces,
layoutHook = myLayouts,
logHook = myLog >>= xmonadPropLog,
manageHook = manageHook defaultConfig <+> manageDocks <+> myManageHook }

Я использую клавиатуру с кучей мультимедийных клавиш и хотел бы использовать некоторые из них в повседневной работе. Xmonad позволяет очень легко привязать обработчики к любым клавишам. Привязки клавиш  передаются в XConfig параметром keys.

myKeys x = M.union (keys defaultConfig x) (keysToAdd x) 
where
keysToAdd = c -> mkKeymap c $ [
("<XF86HomePage>", spawn "x-www-browser"),
("<XF86AudioRaiseVolume>", spawn "~/local/bin/pactrl.sh inc"),
("<XF86AudioLowerVolume>", spawn "~/local/bin/pactrl.sh dec"),
("<XF86AudioMute>", spawn "~/local/bin/pactrl.sh mute"),
("<XF86AudioPlay>", spawn "xterm -e cmus"),
("<XF86Mail>", spawn "icedove"),
("<XF86Search>", spawn "pcmanfm"),
("<XF86Calculator>", spawn "galculator"),
("<XF86Launch5>", spawn "emacs"),
("<XF86Launch6>", spawn "gthumb"),
("<XF86Favorites>", spawn "gksu halt"),
("M-<XF86Favorites>", spawn "gksu reboot"),
("<Print>", spawn "~/local/bin/printscreen.sh"),
("M1-<Tab>", windows W.focusDown),
("M1-<F4>", kill) ]
main = do
xmonad $ defaultConfig {
modMask = mod4Mask,
borderWidth = 3,
normalBorderColor = "gray",
focusedBorderColor = "red",
workspaces = myWorkspaces,
layoutHook = myLayouts,
logHook = myLog >>= xmonadPropLog,
manageHook = manageHook defaultConfig <+> manageDocks <+> myManageHook,
keys = myKeys }

Для работы кода требуется импортировать следующие модули:

import XMonad.Util.EZConfig
import XMonad.Operations
import qualified XMonad.StackSet as W
import qualified Data.Map as M

Привязки клавиш представляют собой карту кортежей из модификатора, клавиши и обработчика. Существует несколько способов создать такую карту, но я предпочитаю использовать функцию mkKeymap. Функция mkKeymap принимает список кортежей состоящий из строкового обозначения клавиатурного сочетания в стиле emacs и обработчика. Получить имена клавиш можно с помощью программы xev или подглядеть их в минибуфере emacs.
Функция spawn запускает внешнее приложение.
Для управления окнами используются два модуля: XMonad.Operations и XMonad.StackSet. Первый предоставляет API высокого уровня для работы с окнами, а второй позволяет работать непосредственно со стеком окон и экранами.
Функция focusDown принимает стек окон, перемещает фокус на следующее окно и возвращает новый стек. Но обработчики клавиатурных сочетаний должны возвращать тип X (). Здесь нам поможет функция windows, которая принимает функцию, принимающую и возвращающую стек окон, и возвращает нужный нам X ().
Чтобы закрыть текущее окно, достаточно применить функцию kill. Она сразу же возвращает необходимое значение, поэтому оборачивать её ни во что не нужно.
Получив карту клавиатурных сочетаний, её нужно объединить с картой по умолчанию. Для этого используется функция union из модуля Data.Map.

Xmobar, по умолчанию, предоставляет одно сочетание клавиш для вызова терминала — Mod+Shift+Enter. Изменить терминал можно через параметр terminal.

Полный конфигурационный файл ~/.xmonad/xmonad.hs

import XMonad
import XMonad.Hooks.ManageDocks
import XMonad.Layout.NoBorders
import XMonad.Layout.Tabbed
import XMonad.Layout.StackTile
import XMonad.Hooks.DynamicLog
import XMonad.Util.EZConfig
import XMonad.Operations
import qualified XMonad.StackSet as W
import qualified Data.Map as M

myWorkspaces = [ "1", "2", "3", "4", "5", "6", "7", "8", "9" ]

myLayouts = ( avoidStruts $ smartBorders $
Tall 1 (3/100) (1/2) |||
StackTile 1 (3/100) (1/2) |||
simpleTabbed ) |||
noBorders Full

myManageHook = composeAll [
className =? "Gtk-recordmydesktop" --> doFloat,
className =? "Xmessage" --> doFloat,
className =? "Gxmessage" --> doFloat,
className =? "Galculator" --> doFloat,
className =? "Gksu" --> doFloat ]

myLog = dynamicLogString xmobarPP {
ppCurrent = xmobarColor "green" "" . wrap "[" "]",
ppTitle = xmobarColor "lightblue" "" . wrap "[" "]",
ppHidden = xmobarColor "yellow" "",
ppHiddenNoWindows = xmobarColor "darkgray" "",
ppLayout = xmobarColor "orange" "" }

myKeys x = M.union (keys defaultConfig x) (keysToAdd x)
where
keysToAdd = c -> mkKeymap c $ [
("<XF86HomePage>", spawn "x-www-browser"),
("<XF86AudioRaiseVolume>", spawn "~/local/bin/pactrl.sh inc"),
("<XF86AudioLowerVolume>", spawn "~/local/bin/pactrl.sh dec"),
("<XF86AudioMute>", spawn "~/local/bin/pactrl.sh mute"),
("<XF86AudioPlay>", spawn "xterm -e cmus"),
("<XF86Mail>", spawn "icedove"),
("<XF86Search>", spawn "pcmanfm"),
("<XF86Calculator>", spawn "galculator"),
("<XF86Launch5>", spawn "emacs"),
("<XF86Launch6>", spawn "gthumb"),
("<XF86Favorites>", spawn "gksu halt"),
("M-<XF86Favorites>", spawn "gksu reboot"),
("<Print>", spawn "~/local/bin/printscreen.sh"),
("M1-<Tab>", windows W.focusDown),
("M1-<F4>", kill) ]

main = do
xmonad $ defaultConfig {
modMask = mod4Mask,
terminal = "xterm",
borderWidth = 3,
normalBorderColor = "gray",
focusedBorderColor = "red",
workspaces = myWorkspaces,
layoutHook = myLayouts,
logHook = myLog >>= xmonadPropLog,
manageHook = manageHook defaultConfig <+> manageDocks <+> myManageHook,
keys = myKeys }

Немного скриптов

Как Вы могли заметить, я привязал к клавишам два скрипта: один регулирует громкость PulseAudio, другой снимает скриншоты. Приведу их тут с кратким описанием для полноты картины.

#!/bin/bash

VOLUME_FILE=/tmp/pa_volume
MUTE_FILE=/tmp/pa_mute

function doMute() {
if [ -f $MUTE_FILE ];
then
pactl set-sink-mute 0 0
rm -rf $MUTE_FILE
else
pactl set-sink-mute 0 1
touch $MUTE_FILE
fi
}

function doChangeVolume() {
if [ ! -f $VOLUME_FILE ];
then
echo 100 > $VOLUME_FILE
fi
VOLUME=`cat $VOLUME_FILE`
NEW_VOLUME=$((VOLUME+$1))
if [ $NEW_VOLUME -lt 0 ];
then
exit 0
fi
pactl set-sink-volume 0 $NEW_VOLUME%
echo $NEW_VOLUME > $VOLUME_FILE
}

case $1 in
inc)
doChangeVolume 2
;;
dec)
doChangeVolume -2
;;
mute)
doMute
;;
esac
exit 0

Скрипт использует утилиту pactl для изменения громкости. Можно было бы использовать её напрямую в xmonad.hs, но существуют три проблемы:

  1. Из-за ошибки в программе, невозможно передать ей отрицательное значение для изменения громкости; она воспринимает его как неизвестный параметр;
  2. После того, как громкость достигла нуля, pactl продолжает её уменьшать, после чего придётся увеличивать её несколько раз, чтобы достичь нуля;
  3. Программа не предоставляет возможности узнать, в каком состоянии находится mute.
Примечание: по прошествии времени после публикации статьи, все проблемы с pactl решены.

  • Первая решается отменой восприятия "-2%" как параметра с помощью команды pactl set-sink-volume 0 — -2%;
  • Вторая проблема решена в последней версии pulseaudio;
  • Последняя проблема не актуальна, так как появилась возможность попросить переключение состояния mute командой pactl set-sink-mute 0 toggle.

Скрипт записывает текущую громкость (в процентах) в файл и при поступлении команды либо увеличивает её, либо уменьшает. Для индикации состояния mute используется создание и удаление файла с предопределённым именем.

Для снятия скриншота используется конвеер

xwd -root | convert - <filename>

Скрипт нужен лишь для того, чтобы определить имя файла и показать его имя в диалоге с предложением открыть в просмоторщике.

#!/bin/bash

FILEPATH=~/
FILENAME=snapshot
FILEEXT=.png
FULLFILENAME=$FILEPATH$FILENAME$FILEEXT

if [ -f /usr/bin/gxmessage ];
then
XMESSAGE=gxmessage
else
XMESSAGE=xmessage
fi

for((I=1; ;I++)); do
if [ ! -f $FULLFILENAME ]; then
break
fi
FULLFILENAME=$FILEPATH$FILENAME$I$FILEEXT
done

xwd -root | convert - $FULLFILENAME
$XMESSAGE "Snapshot has been saved to $FULLFILENAME" -center -default Ok -buttons Ok:0,Open:1

if [ $? -eq 1 ];
then
gthumb $FULLFILENAME
fi

На скриншоте Вы могли заметить, что у меня на фоне корневого окна установлены обои. Делается это при помощи утилиты feh в два этапа. Первый — это непосредственно установка изображения

$ feh --bg-scale <имя файла>

А второй — восстановление изображения после перезапуска X`ов. Для этого нужно поместить такую команду в файл ~/.xsession

eval $(cat ~/.fehbg)