2021-07-04 19:08:48 +02:00
|
|
|
#!/usr/bin/python3
|
|
|
|
|
|
|
|
"""Personal i3 companion
|
|
|
|
|
2021-07-04 23:50:48 +02:00
|
|
|
This script will listen to i3 events and react accordingly.
|
2021-07-04 19:08:48 +02:00
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
import logging
|
|
|
|
import logging.handlers
|
|
|
|
import os
|
|
|
|
import sys
|
|
|
|
import re
|
|
|
|
|
2021-07-04 23:50:48 +02:00
|
|
|
from i3ipc import Connection, Event
|
|
|
|
|
2021-07-04 19:08:48 +02:00
|
|
|
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": "",
|
2021-07-05 10:38:41 +02:00
|
|
|
"discord": "",
|
2021-07-04 23:50:48 +02:00
|
|
|
"emacs": "",
|
2021-07-04 19:08:48 +02:00
|
|
|
"firefox": "",
|
2021-07-05 16:12:21 +02:00
|
|
|
"gimp": "",
|
2021-07-04 19:08:48 +02:00
|
|
|
"google-chrome": "",
|
2021-07-05 16:12:21 +02:00
|
|
|
"inkscape": "",
|
2021-07-05 10:38:41 +02:00
|
|
|
"signal": "",
|
2021-07-04 23:50:48 +02:00
|
|
|
"snes9x-gtk": "",
|
2021-07-04 19:08:48 +02:00
|
|
|
"spotify": "",
|
2021-07-04 23:50:48 +02:00
|
|
|
"steam": "",
|
2021-07-04 19:08:48 +02:00
|
|
|
"vbeterm": "",
|
2021-07-05 16:12:21 +02:00
|
|
|
"zathura": "",
|
2021-07-05 10:29:45 +02:00
|
|
|
"zoom": "",
|
2021-07-04 19:08:48 +02:00
|
|
|
"NOMATCH": ""
|
|
|
|
}
|
2021-07-04 23:50:48 +02:00
|
|
|
application_icons_alone = {
|
|
|
|
application_icons[k] for k in {
|
|
|
|
"vbeterm"
|
|
|
|
}
|
|
|
|
}
|
2021-07-04 19:08:48 +02:00
|
|
|
|
|
|
|
|
|
|
|
def application_icon(window):
|
|
|
|
"""Get application icon for a window."""
|
2021-07-04 23:50:48 +02:00
|
|
|
for attr in ('name',
|
|
|
|
'window_instance',
|
|
|
|
'window_class'):
|
2021-07-04 19:08:48 +02:00
|
|
|
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)
|
2021-07-04 23:50:48 +02:00
|
|
|
if any([i not in application_icons_alone for i in icons]):
|
|
|
|
icons -= application_icons_alone
|
2021-07-04 19:08:48 +02:00
|
|
|
new_name = f"{workspace.num}:{'|'.join(icons)}"
|
|
|
|
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))
|
|
|
|
|
|
|
|
|
2021-07-05 19:07:41 +02:00
|
|
|
def new_workspace(i3, event):
|
2021-07-05 19:54:21 +02:00
|
|
|
"""Create a new workspace and optionally move a window to it."""
|
2021-07-05 19:52:25 +02:00
|
|
|
if event.payload not in {"new-workspace", "move-to-new-workspace"}:
|
2021-07-05 19:07:41 +02:00
|
|
|
return
|
|
|
|
|
2021-07-05 19:54:21 +02:00
|
|
|
# Get the currently focused window
|
2021-07-05 19:52:25 +02:00
|
|
|
if event.payload == "move-to-new-workspace":
|
|
|
|
current = i3.get_tree().find_focused()
|
2021-07-05 19:54:21 +02:00
|
|
|
if not current:
|
|
|
|
return
|
2021-07-05 19:52:25 +02:00
|
|
|
|
2021-07-05 19:54:21 +02:00
|
|
|
# Create a new workspace
|
2021-07-05 19:07:41 +02:00
|
|
|
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}"')
|
|
|
|
|
2021-07-05 19:54:21 +02:00
|
|
|
# Move the window to this workspace
|
2021-07-05 19:52:25 +02:00
|
|
|
if event.payload == "move-to-new-workspace":
|
|
|
|
current.command(f'move container to workspace number "{available}"')
|
|
|
|
|
2021-07-05 19:07:41 +02:00
|
|
|
|
2021-07-04 19:08:48 +02:00
|
|
|
if __name__ == "__main__":
|
|
|
|
options = parse_args()
|
|
|
|
setup_logging(options)
|
|
|
|
|
|
|
|
try:
|
2021-07-04 23:50:48 +02:00
|
|
|
i3 = Connection()
|
2021-07-04 19:08:48 +02:00
|
|
|
|
|
|
|
# Rename workspace depending on what is inside
|
2021-07-04 23:50:48 +02:00
|
|
|
for event in {Event.WINDOW_MOVE,
|
|
|
|
Event.WINDOW_NEW,
|
|
|
|
Event.WINDOW_CLOSE}:
|
2021-07-04 19:08:48 +02:00
|
|
|
i3.on(event, workspace_rename)
|
|
|
|
|
2021-07-05 19:52:25 +02:00
|
|
|
# Create a new workspace or move to a new workspace
|
2021-07-05 19:07:41 +02:00
|
|
|
i3.on(Event.TICK, new_workspace)
|
|
|
|
|
2021-07-04 19:08:48 +02:00
|
|
|
i3.main()
|
|
|
|
except Exception as e:
|
|
|
|
logger.exception("%s", e)
|
|
|
|
sys.exit(1)
|
|
|
|
sys.exit(0)
|