Ми}{@лbI4

Блог хеллоуворлдщика

Window switcher в i3

15.12.2023 i3, linux, python, bash

В i3 не так важна функция window switcher, т.к. эти окна и так все известны для того, кто работает за компом. К тому же информации об этих окнах перед глазами создает только дополнительный информационный шум. Но иногда данный функционал полезен, когда используется scratchpad и хочется быстро проверить, что все окна точно закрыты.

В i3 получить список открытых окон можно через сторонние пакеты, например wmctrl, или через команду i3 i3-msg. В первом случае для достижения результата нам потребуется только Bash, а во втором случае удобно будет сделать через Python и Bash.

Нам также потребуется dmenu или другое динамическое меню. Кстати, в rofi window switcher доступен из коробки и не нужно ничего придумывать. Поэтому, если вам не принципиально использование dmenu, то просто замените его на rofi.

Сначала я реализовал максимально простой вариант через wmctrl и Bash. Выглядело это следующим образом:

#!/usr/bin/env bash

set -euo pipefail

switch_window() {
    local target=""
    read -r target
    if [ -z "${target}" ]; then
        return 1
    fi

    wmctrl -a "$target"

    return 0
}

windows=$(wmctrl -xl | tr -s '[:blank:]' | cut -d ' ' -f 3-3,5- | sed 's/^[a-zA-Z0-9-]*\.//' | sort -V)
if [ -z "${windows}" ]; then
    exit 0
fi

echo "$windows" | \
    dmenu -i -p "Window:" | \
    tr -s '[:blank:]' | \
    cut -d ' ' -f 2- | \
    switch_window

exit 0

У данного решения имелась проблема, связанная с уникальностью title окон. Если будет открыто несколько окон с одинаковыми title, то переключение будет выполняться только на самое первое из списка. Это не проблема wmctrl, а моего решения, т.к. для меня на тот момент стояла задача просто сделать список отрытых окон. Позже я исправил данную проблему, когда заменил wmctrl на простую обертку над i3-msg, написанную на Python. О ней далее.

В i3 через команду i3-msg мы можем получить информацию об открытых окнах в формате JSON. В Bash для работы с JSON можно воспользоваться пакетом jq, но по мне лучше сразу взять что-то подходящее как Python. Да и к тому же эти данные имеют древовидную структуру, что превращает реализацию через Bash в огромную проблему. Поэтому, используя Python, реализовываем следующую вспомогательную команду - window-tree.py:

#!/usr/bin/env python3

import json
import os
import subprocess
import sys
from json import JSONDecodeError
from subprocess import CalledProcessError


def find_windows(tree: dict) -> dict[int, str]:
    windows: [int, str] = {}
    if 'nodes' not in tree:
        return {}
    for node in tree['nodes']:
        if (
            'window_type' in node
            and 'id' in node
            and 'name' in node
            and node['window_type'] == 'normal'
        ):
            windows[int(node['id'])] = str(node['name'])
        if 'nodes' in node:
            windows.update(find_windows(node))
    return windows


try:
    windows_tree = json.loads(
        subprocess.run(['i3-msg', '-t', 'get_tree'], stdout=subprocess.PIPE).stdout
    )
except (JSONDecodeError, CalledProcessError) as e:
    print(e, file=sys.stderr)
    sys.exit(os.EX_DATAERR)

idx = 0
for _id, title in sorted(find_windows(windows_tree).items(), key = lambda item: item[1]):
    idx += 1
    print('{0:d} {1:d}: {2}'.format(_id, idx, title))

sys.exit(os.EX_OK)

Данная команда возвращает построчный список открытых окон с их идентификаторами. Почти как wmctrl, но проще и только то, что нам нужно. Осталось завернуть её для работы с dmenu и пользоваться:

#!/usr/bin/env bash

set -euo pipefail

switch_window() {
    local window_title=""
    read -r window_title
    if [ -z "${window_title}" ]; then
        return 1
    fi

    local window_id=""
    window_id=$(u-window-tree | grep "${window_title}" | head -n 1 | cut -d ' ' -f 1)
    if [ -z "${window_id}" ]; then
        return 1
    fi

    i3-msg "[con_id=${window_id}] focus"

    return 0
}

windows=$(u-window-tree | cut -d ' ' -f 2- | sed 's/^[a-zA-Z0-9-]*\.//')
if [ -z "${windows}" ]; then
    exit 0
fi

echo "${windows}" | dmenu -i -p "Window:" | switch_window

exit 0

Полезные ссылки