В 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