Оконный менеджер bspwm

by hugeping on 2022-05-29 12:27:46

Эксперименты с оконными менеджерами

Я люблю экспериментировать с рабочей средой в Linux. Редакторы, оконные менеджеры, способы создания документации и т.д. Думаю, меня гнали по этому пути любопытство и жажда эксперимента (в том числе и над собой). Помню как в своём первом Linux (KSI Linux) мне решительно не понравились gnome 2 и kde 2. Тогда же я обнаружил прекрасный WindowMaker и процесс был запущен.

Я перепробовал массу оконных менеджеров. В разные периоды меня кидало от минималистических wm до полновесных рабочих столов. Но фундаментально стиль работы не менялся. "Вынос мозга" случился после ion3. Очень достойный тайловый менеджер (написанный харизматичным автором) дал новые ощущения и я подсел! Стоит ли говорить, что на ion3 я не остановился? Тайловые менеджеры тогда набирали популярность и я пробовал всё, что выглядело интересным: dwm, wmii, musca, xmonad, ratpoison, i3wm... Обычные wm я тоже пробовал, но после выхода gnome 3 интерес к экспериментам угас, так как gnome3 в качестве обычного десктопа мне очень понравился.

Золотой набор

После выхода gnome3 я постепенно успокоился и у меня сформировался свой "золотой" набор.

- Для работы: i3wm (+dmenu).

- Для дома (компьютер, который использую не только я): gnome3.

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

Интересно что wmii был создан с оглядкой на acme! Только вот про Plan 9 и acme я узнал гораздо позже.

Кстати, раз уж зашёл разговор про Plan 9... В rio ("оконном менеджере" Plan 9) при абсолютной аскетичности сохраняется высокая практичность окружения. Я пытался сделать подобие rio на основе fvwm2 (с частичным успехом), но оставил эту попытку. Слишком уж разный "путь" у Linux и Plan 9. Например, в rio ты заранее создаёшь окна в которых запускаются программы, но программа не создаёт окон сама! Интересно, что небезызвестный Drew DeVault делал эмуляцию такого поведения в своём "клоне" wio: https://drewdevault.com/2019/05/01/Announcing-wio.html [1] Но всё это выглядит как подделка, если честно. Так что я оставил Plan 9 "плановое", а Linux - "линуксовое".

Новое знакомство: bspwm

Многие годы я использовал i3wm и gnome3 и до сих пор считаю это лучшим "набором", который могу рекомендовать всем. Но время от времени я продолжал экспериментировать. Например, познакомился с cwm. В "наборе" своё место занял tmux. И вот, на днях, решил посмотреть на bspwm. Мне этот оконный менеджер настолько понравился, что я решил написать эту заметку. Говорю сразу -- достойный wm! Для любопытных программистов. :)

К сути

Обычно упоминают что bspwm работает с окнами как с бинарным деревом. Это первая строчка в man bspwm и, честно говоря, с чисто практической точки зрения для меня это мало что значит. Мне же хочется сделать упор на "практике". А с практической точки зрения "суть такова"(c):

- bspwm конфигурируется и управляется только одним способом: утилитой bspc;

- чтобы управлять bspwm с клавиатуры используется внешний "демон" горячих клавиш (обычно sxhkd) из которого вызывается bspc;

- bspwm не поддерживает никаких панелей и прочих "свистелок". Но с помощью bspc вы можете слушать нужные вам события и делать что хотите;

- bspwm выглядит сбалансированным и отполированным как и i3wm. Многие вещи сделаны "интуитивно-верно".

То-есть, мы видим вполне себе тот самый Unix-way да ещё и в качественном исполнении. Вообще, когда я начинал играться с bspwm меня пугала перспектива писать портянки на shell, как это часто бывает. Но... Обо всём по порядку...

Конфиг bspwm

Конфигурация bspwm это просто shell скрипт в котором в основном присутствуют вызовы bspc. Я приведу фрагменты своего конфига для иллюстрации. Интересно, что файл получается простым, потому что в нём не заданы горячие клавиши. Только конфигурация самого wm!

#! /bin/sh

pgrep -x sxhkd > /dev/null || sxhkd & # запуск демона горячих клавищ
pgrep -x panel > /dev/null || panel & # запуск панельки (об этом - ниже)

setxkbmap -layout "us,ru" -variant "winkeys" -option "grp:caps_toggle,compose:ralt,grp_led:scroll" # раскладка

xsetroot -cursor_name left_ptr # курсор вместо символа X

bspc monitor -d 1 2 3 4 5 6 7 8 9 0 # рабочие столы

bspc config removal_adjustment false # при удалении окна не ребалансить
bspc config swallow_first_click true # первый клик не идёт в приложение

bspc rule -a librewolf desktop='^4' # пример правила
bspc rule -a Xdialog state=floating # ещё пример правила

bspc config pointer_modifier mod4 # ресайзим и таскаем окна мышкой
bspc config pointer_action1 move
bspc config pointer_action2 resize_side
bspc config pointer_action2 resize_corner
bspc config focused_border_color '#ff0000' # рамка активного окна поярче

На самом деле это практически весь конфиг, кроме каких-то локальных нюансов.

Конфиг sxhkd

Теперь, sxhkd. На самом деле вам не нужно будет писать этот файл с нуля, можно взять типовой из share/doc/bspwm/examples и начать использовать его. В качестве примера, приведу фрагменты своей конфигурации:

XF86AudioLowerVolume
        amixer -q sset Master 10%-

XF86AudioRaiseVolume
        amixer -q sset Master 10%+

# terminal emulator
super + Return
        st

# focus the node in the given direction
super + {_,shift + }{h,j,k,l}
        bspc node -{f,s} {west,south,north,east}

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

Панелька

В примерах bspwm есть панелька на основе shell скрипта и lemonbar. Я не люблю портянки на shell (хотя и умею их писать и понимать) поэтому я изучил как она работает и написал свою...

lemonbar рисует саму панель, но содержимое панели приходит в виде stdin. И вот наша задача предоставить информацию для lemonbar в виде текста оформленного определённым образом.

Что за информация? Например: информация о номерах десктопов, активном десктопе, режиме окна и так далее. Эту информацию нам может предоставить bspc. В режиме bspc subscribe report мы получаем события этого оконного менеджера. Но кроме десктопов нам нужны ещё: часы, батарея, раскладка. Ну и так далее, по вкусу.

Панелька из примеров делает fifo и направляет в эту fifo вывод различных утилит, которые запущены в режиме монитора (выводят строчку в stdout при изменении информации). Например, xtitle -s. Далее, скрипт на sh читает из fifo общий поток, парсит его и даёт на вход lemonbar. Я подумал, что это полотно легко переписать на go (go-рутиты идеально здесь подходят). Я приведу фрагмент того, что у меня получилось:

func read(fname string) string {
// читает файл и возвращает строку
// ...
}

// запускает процесс и отправляет его вывод в канал
func cmd_reader(out chan<- string, prog string, args ...string) {
	cmd := exec.Command(prog, args...)
	pipe, _ := cmd.StdoutPipe()
	reader := bufio.NewReader(pipe)
	cmd.Start()
	for {
		output, _, err := reader.ReadLine()
		if err != nil || err == io.EOF {
			break
		}
		out <- string(output)
	}
}
// парсим информацию о десктопе
func bsp_parse(item string) string {
	c := item[0:1]
	var U, F, B string;
	U = "#144b6c"
	nam := item[1:]
	switch c {
	case "f": // free desktop
		F = "#737171"
		B = "#333232"
		// далее F= B= в каждом case (FoOuU) пропущено для краткости
	case "F": // active free desktop
	case "o": // occupied desktop
	case "O": // focused occupied
	case "u": // urgent
	case "U": // focused urgent
	case "L","T","G":
		F = "#ffffff"
		B = "#333232"
		return fmt.Sprintf("%%{F%s}%%{B%s} %s %%{B-}%%{F-}", F, B, nam)
	default:
		return ""
	}
	return fmt.Sprintf("%%{F%s}%%{B%s}%%{U%s}%%{+u}%%{A:bspc desktop -f %s:} %s %%{A}%%{B-}%%{F-}%%{-u}",
		F, B, U, nam, nam)
}

func main() {
	bspc_in := make(chan string)
	xtitle_in := make(chan string)
	mail_in := make(chan string)
	xkb_in := make(chan string)
	go cmd_reader(bspc_in, "bspc", "subscribe", "report") // десктопы
	go cmd_reader(xtitle_in, "xtitle", "-s") // заголовок окна
	go cmd_reader(mail_in, "checkmail", "-s") // новая почта
	go cmd_reader(xkb_in, "xkbmon") // раскладка
	var bspc, bat, clock, mail, xtitle, xkb string;
	for {
		select {
		case bspc = <-bspc_in:
		case xtitle = <-xtitle_in:
		case mail = <-mail_in:
		case xkb = <-xkb_in:
		case <-time.After(time.Second * 30):
		}
		if bspc == "" {
			continue
		}
		bat = read("/sys/class/power_supply/BAT1/status")
		bat += ":" + read("/sys/class/power_supply/BAT1/capacity")
		curt := time.Now()
		clock = curt.Format("02-01-2006 Mon 15:04")
		bsp := strings.Split(bspc, ":")
		desk := ""
		for _, item := range bsp {
			desk += bsp_parse(item)
		}
		fmt.Printf("%%{l}%s%%{c}%s%%{r}%s %s%% %%{F#000000}%%{B#ffffff}%s%%{B-}%%{F-}[%s]\n", desk, xtitle, mail, bat, clock, xkb)
	}
}

Программа совсем простая, написанная под конкретную ситуацию как скрипт. Конечно, можно было взять готовую панель. Можно было взять вместо lemonbar что-то другое. Но мне лично проще, когда я понимаю происходящее полностью и могу это контролировать. Да, монитор раскладки я написал на C. Тоже небольшая программка.

Особенности использования

В целом, bspwm из коробки вполне себе годен, но мне не хватало некоторых вещей. На этих нюансах остановлюсь подробнее.

Если в запущенном wm просто начать запускать терминал по super + Return, то заполняться пространство будет примерно так (что-то вроде спирали Фибоначчи):

+----------+----------+
|          |          |
|          |          |
|          |          |
+----+-----|          |
|    |     |          |
|    +--+--+          |
|    |  +--+          |
+----+--+--+----------+

При этом, если закрыть какое-то из окон, то оставшиеся окна автоматически "сбалансируются". Это напоминает поведение динамических wm (которое мне не нравится). К счастью, в bspwm есть настройка: bspc config removal_adjustment false.

Кроме того, bspwm позволяет предварительно выбрать следующую позицию для разделения окна. Эта фича висит на хоткеях но я сделал для себя отдельно запуск терминала под текущим терминалом:

super + shift + Return # запуск "вертикально"
        bspc node -p south; \
        st

Таким образом, я могу быстро создавать терминалы в одном столбце:

+----------+----------+
|          |          |
+----------+          |
|          |          |
+----------+     2    |
|    1     |          |
+----------+          |
|          |          |
+----------+----------+

Далее, выбрав определенный терминал хоткеями или мышкой можно максимизировать его на всё пространство (режим монокля, по умолчанию super + m) или поменять его (1) с самым большим окном (2) примерно так, как это сделано в dwm. (Для этого используется хоткей super + g).

# swap the current node and the biggest window
super + g
        bspc node -s biggest.local
# в дефолтном примере было: bspc node -s biggest.window
# в таком режиме большое окно выбиралось со всех десктопов
# что было неудобно
# заменил на .local

Мне ещё не хватало возможн��сти растянуть терминал вертикально:

+----------+----------+
+----------+          |
+----------+     2    |
|          |          |
|    1     |          |
|          |          |
|          |          |
+----------+          |
+----------+----------+

Я смог добиться такого поведения, правда, небольшим хаком:

super + v
	bspc node north#north#north#north#north#north -z top 0 -2000; \
	bspc node north#north#north#north#north -z top 0 -2000; \
	bspc node north#north#north#north -z top 0 -2000; \
	bspc node north#north#north -z top 0 -2000; \
	bspc node north#north -z top 0 -2000; \
	bspc node north -z top 0 -2000; \
	bspc node -z top 0 -2000; \
	bspc node -z bottom 0 2000

Дело в том, что окно не может быть расширено, если над ним есть несколько максимально суженных окон. Возможно, это баг bspwm. Возможно, есть более элегантное решение, но текущее тоже работает!

Всё эти хитрости помогли мне заменить табы и стек окон в i3wm.

Ещё одна штука, которая мне нравилась ещё по Plan9 -- возможность именовать окна по ситуации. В случае bspwm, правда, именуем не окна, а рабочие столы. Я написал скрипт, который вызывает Xdialog (Xdialog --stdout --under-mouse --inputbox "Window name" 0 0) и просит имя для текущего десктопа. Потом делает: bspc desktop focused --rename имя. Повесил на хоткей и всё -- можно именовать!

Ещё один пример гибкости простых решений. Скрипт который делает все окна на 9м десктопе "плавающими". За основу был взят пример с Arch wiki, но немного доработан (отслеживается не только создание, но и перемещение node):

#!/bin/bash

# change the desktop number here
FLOATING_DESKTOP_ID=$(bspc query -D -d '^9')

bspc subscribe node_add node_transfer | while read -a msg ; do
    if [ "${msg[0]}" = "node_transfer" ]; then
        desk_id=${msg[5]}
        wid=${msg[3]}
    else
        desk_id=${msg[2]}
        wid=${msg[4]}
    fi
    [ "$FLOATING_DESKTOP_ID" = "$desk_id" ] && bspc node "$wid" -t floating
done

Хакерская штучка

bswpm создаёт впечатление добротной и отполированной хакерской "штучки". Например, по умолчанию super + tab работает именно так как нужно! Переключаясь между последними двумя рабочими столами. Работа с окнами просто реактивная. Изменение размера окон мышкой работает тоже отлично. Также мышкой можно перемещать тайловые окна, меняя их местами. Много мелочей, которые незаметны, когда они работают "правильно". По стабильности на данный момент тоже нареканий нет. Кстати, параллельно с bspwm я также посмотрел herbstluftwm (никак не могу выучить название этого wm!). Но ощущения "отполированности" с этим wm у меня не возникло, хотя тоже -- неплохой тайловый менеджер и подход к управлению/конфигурированию очень похож.

Вместо заключения

Функционально i3wm и bspwm близки. Но i3wm предоставляет большинство функций "из коробки". С другой стороны, в bspwm благодаря простоте устройства многие вещи выглядят менее "захламлёнными" и "раздражающими". То что есть -- работает предсказуемо и отлично. Это для меня уже привычная характеристика для простых инструментов.

И если i3wm я могу рекомендовать всем без исключения программистам, то bspwm уже скорее для любопытных минималистов. Но, как мне кажется, любопытство -- одно из наших основных (программистских) качеств. Ведь правда? :) Ну а на моём рабочем ноутбуке bspwm уже заменил i3wm.

https://drewdevault.com/2019/05/01/Announcing-wio.html [1]