#!/usr/bin/env python3 """ Rename i3 and Sway workspaces based on the focused app's XDG categories. Copyright (C) 2024 Matt Singleton This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Adapted from this script: https://github.com/nwg-piotr/swayinfo/blob/master/wsdnames.py Which was in turn based on this example: https://github.com/altdesktop/i3ipc-python/blob/master/README.rst Dependencies: python-i3ipc>=2.0.1 (i3ipc-python), python-xlib Pay attention to the fact that your workspaces need to be numbered, not named, for the script to work. Use: bindsym $mod+1 workspace number 1 instead of: bindsym $mod+1 workspace 1 in your ~/.config/sway/config or ~/.config/i3/config file. """ import argparse import configparser import logging import os import os.path import traceback from i3ipc import Connection, Event def get_xdg_categories(app_id): path = os.path.join("/usr/share/applications", f"{app_id}.desktop") try: config = configparser.ConfigParser() config.read(path) return config["Desktop Entry"]["Categories"].split(";") except Exception: return [] def glyph(ws_num, app_id=None): """ Return a single glyph based on the app_id, if given, then a default based on the workspace number, and finally an empty glyph. """ global CONFIG_MTIME latest_mtime = os.stat(CONFIG_PATH).st_mtime if CONFIG_MTIME < latest_mtime: CONFIG_MTIME = latest_mtime CONFIG.read(CONFIG_PATH) if app_id is not None: if app_id in CONFIG["app"]: return CONFIG["app"][app_id] categories = get_xdg_categories(app_id) for c in categories: if c in CONFIG["xdg_additional"]: return CONFIG["xdg_additional"][c] for c in categories: if c in CONFIG["xdg_main"]: return CONFIG["xdg_main"][c] logger.debug(f"no xdg icon found for: {app_id}") ws_num = str(ws_num) if ws_num in CONFIG["desktop"]: return CONFIG["desktop"][ws_num] return CONFIG["fallback"]["icon"] def get_ws_name(ws_num, app_id=None): ws_glyph = glyph(ws_num, app_id) return f"{ws_num}:{ws_glyph}" def assign_generic_name(i3, e): """ Name the workspace after the focused window name """ try: if e.change == 'rename': return if e.change in ['new', 'move']: # if a change results in a window spawning in an unfocused # state (i.e. on another workspace), we need to look # it up by id con = i3.get_tree().find_by_id(e.container.id) else: con = i3.get_tree().find_focused() if con.type == 'workspace': # empty workspace ws_num = con.workspace().num name = get_ws_name(ws_num) i3.command(f'rename workspace to "{name}"') else: old_name = con.workspace().name # assume windows without an app_id are xwayland app_id = "xwayland" if con.app_id is None else con.app_id name = get_ws_name(con.workspace().num, app_id) i3.command(f'rename workspace "{old_name}" to "{name}"') except Exception as e: logger.debug("".join(traceback.format_exception(e))) parser = argparse.ArgumentParser() parser.add_argument("--debug", action="store_true") args = parser.parse_args() log_format = "%(levelname)s %(name)s: %(message)s" if args.debug is True: logging.basicConfig(level=logging.DEBUG, format=log_format) else: logging.basicConfig(level=logging.INFO, format=log_format) logger = logging.getLogger(__name__) CONFIG_MTIME = 0 CONFIG_PATH = os.path.join( os.environ.get( "XDG_CONFIG_HOME", os.path.join(os.environ["HOME"], ".config"), ), "xdg-names.ini", ) CONFIG = configparser.ConfigParser() # Create the Connection object that can be used to send commands and # subscribe to events. i3 = Connection() # Subscribe to events i3.on(Event.WORKSPACE_FOCUS, assign_generic_name) i3.on(Event.WINDOW_FOCUS, assign_generic_name) i3.on(Event.WINDOW_TITLE, assign_generic_name) i3.on(Event.WINDOW_CLOSE, assign_generic_name) i3.on(Event.WINDOW_NEW, assign_generic_name) i3.on(Event.WINDOW_MOVE, assign_generic_name) i3.on(Event.BINDING, assign_generic_name) i3.main()