vincentbernat.i3wm-configur.../bin/i3-companion

208 lines
6 KiB
Text
Raw Normal View History

#!/usr/bin/python3
"""Personal i3 companion
This script will listen to i3 events and react accordingly.
"""
import argparse
import logging
import logging.handlers
import os
import sys
import re
import time
from i3ipc import Connection, Event
logger = logging.getLogger(os.path.splitext(os.path.basename(sys.argv[0]))[0])
class CustomFormatter(argparse.RawDescriptionHelpFormatter,
argparse.ArgumentDefaultsHelpFormatter):
pass
def parse_args(args=sys.argv[1:]):
"""Parse arguments."""
parser = argparse.ArgumentParser(
description=sys.modules[__name__].__doc__,
formatter_class=CustomFormatter)
g = parser.add_mutually_exclusive_group()
g.add_argument("--debug", "-d", action="store_true",
default=False,
help="enable debugging")
g.add_argument("--silent", "-s", action="store_true",
default=False,
help="don't log")
return parser.parse_args(args)
def setup_logging(options):
"""Configure logging."""
root = logging.getLogger("")
root.setLevel(logging.WARNING)
logger.setLevel(options.debug and logging.DEBUG or logging.INFO)
if not options.silent:
ch = logging.StreamHandler()
ch.setFormatter(logging.Formatter(
"%(levelname)s[%(name)s] %(message)s"))
root.addHandler(ch)
# See https://fontawesome.com/v5.15/icons
application_icons = {
"chromium": "",
"discord": "",
"emacs": "",
"firefox": "",
"gimp": "",
"google-chrome": "",
"inkscape": "",
"signal": "",
"snes9x-gtk": "",
"spotify": "",
"steam": "",
"vbeterm": "",
"zathura": "",
2021-07-05 10:29:45 +02:00
"zoom": "",
"NOMATCH": ""
}
application_icons_alone = {
application_icons[k] for k in {
"vbeterm"
}
}
def application_icon(window):
"""Get application icon for a window."""
for attr in ('name',
'window_instance',
'window_class'):
name = getattr(window, attr, None)
if name is None:
continue
for k, v in application_icons.items():
if re.match(k, name, re.IGNORECASE):
logger.debug(f"in {attr}, found '{name}', matching {k}")
return v
return application_icons["NOMATCH"]
def workspace_rename(i3, event):
"""Rename workspaces using icons to match what's inside it."""
workspaces = i3.get_tree().workspaces()
commands = []
for workspace in workspaces:
icons = set()
for window in workspace.leaves():
icon = application_icon(window)
if icon is not None:
icons.add(icon)
if any([i not in application_icons_alone for i in icons]):
icons -= application_icons_alone
new_name = f"{workspace.num}:{'|'.join(icons)}".rstrip(":")
if workspace.name != new_name:
logger.debug(f"rename workspace {workspace.num}")
command = f'rename workspace "{workspace.name}" to "{new_name}"'
commands.append(command)
i3.command(';'.join(commands))
def new_workspace(i3, event):
"""Create a new workspace and optionally move a window to it."""
if event.payload not in {"new-workspace", "move-to-new-workspace"}:
return
# Get the currently focused window
if event.payload == "move-to-new-workspace":
current = i3.get_tree().find_focused()
if not current:
return
# Create a new workspace
workspace_nums = {w.num for w in i3.get_workspaces()}
max_num = max(workspace_nums)
available = (set(range(1, max_num + 2)) - workspace_nums).pop()
logger.info(f'create new workspace number {available}')
i3.command(f'workspace number "{available}"')
# Move the window to this workspace
if event.payload == "move-to-new-workspace":
current.command(f'move container to workspace number "{available}"')
def quake_console(i3, event):
"""Spawn a quake console or toggle an existing one."""
if type(event.payload) is not str or \
not event.payload.startswith("quake-console:"):
return
try:
_, term_exec, term_name, height = event.payload.split(":")
height = float(height)
except Exception as exc:
logger.warn(f"unable to parse payload {event.payload}: {exc}")
return
term = i3.get_tree().find_instanced(term_name)
if not term:
i3.command(f'exec {term_exec} --name {term_name}')
tries = 5
while not term and tries:
term = i3.get_tree().find_instanced(term_name)
time.sleep(0.2)
tries -= 1
if not term:
raise RuntimeError("unable to spawn terminal")
term = term[0]
workspace = [ws for ws in i3.get_workspaces() if ws.focused][0]
ws_x = workspace.rect.x
ws_y = workspace.rect.y
ws_width = workspace.rect.width
ws_height = workspace.rect.height
width = ws_width
height = int(ws_height * height)
posx = ws_x
posy = ws_y
command = (f'[instance={term_name}] '
'border none,'
f'resize set {width} px {height} px,'
'scratchpad show,'
f'move absolute position {posx}px {posy}px')
logger.debug(f"QuakeConsole: {command}")
i3.command(command)
if __name__ == "__main__":
options = parse_args()
setup_logging(options)
try:
i3 = Connection()
# Rename workspace depending on what is inside
for event in {Event.WINDOW_MOVE,
Event.WINDOW_NEW,
Event.WINDOW_CLOSE}:
i3.on(event, workspace_rename)
# Create a new workspace or move to a new workspace
i3.on(Event.TICK, new_workspace)
# Create/display a quake console
i3.on(Event.TICK, quake_console)
# Log output event
i3.on(Event.OUTPUT,
lambda _, event: logger.info(f"Output event: {event.ipc_data}"))
i3.main()
except Exception as e:
logger.exception("%s", e)
sys.exit(1)
sys.exit(0)