Last active
May 4, 2025 10:14
-
-
Save aparatext/2ba97dc3ed9a5675841e96ad81b56c74 to your computer and use it in GitHub Desktop.
Auto-name Sway/i3wm Workspaces
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env -S uv run --script | |
| # /// script | |
| # requires-python = ">=3.12" | |
| # dependencies = [ | |
| # "i3ipc", | |
| # ] | |
| # /// | |
| # autoname-workspaces: adds icons to the workspace name for each open window. | |
| # Refactored https://github.com/OctopusET/sway-contrib/blob/master/autoname-workspaces.py | |
| import argparse | |
| import logging | |
| import re | |
| import signal | |
| import sys | |
| from typing import Dict, Optional, Any | |
| import i3ipc | |
| WINDOW_ICONS = { | |
| "librewolf": "", | |
| "org.telegram.desktop": "", | |
| "alacritty": "", | |
| "org.kde.kate": "", | |
| "code": "", | |
| "org.kde.dolphin": "", | |
| "org.keepassxc.keepassxc": "", | |
| "org.mozilla.thunderbird": "", | |
| "net.ankiweb.anki": "", | |
| "tor browser": "", | |
| "org.kde.gwenview": "", | |
| "org.kde.okular": "", | |
| "zoom": "", | |
| } | |
| DEFAULT_ICON = "" | |
| def window_icon(window: i3ipc.Con) -> str: | |
| name = ( | |
| (window.app_id and window.app_id.lower()) | |
| or (window.window_class and window.window_class.lower()) | |
| or None | |
| ) | |
| if name and name in WINDOW_ICONS: | |
| return WINDOW_ICONS[name] | |
| logging.info(f"Missing icon for window ID: {name!r}") | |
| return DEFAULT_ICON | |
| def parse_ws_name(name: str) -> Dict[str, Optional[str]]: | |
| m = re.match(r"(?P<num>[0-9]+):?(?P<shortname>\w+)? ?(?P<icons>.+)?", name) | |
| return m.groupdict() if m else {"num": name, "shortname": None, "icons": None} | |
| def cons_ws_name(parts: dict[str, str | None]) -> str: | |
| num = parts['num'] | |
| fields = [parts.get('shortname'), parts.get('icons')] | |
| suffix = " ".join(filter(None, fields)) | |
| return f"{num}: {suffix} " if suffix else num | |
| def rename_ws(ipc: i3ipc.Connection, args: argparse.Namespace) -> None: | |
| for workspace in ipc.get_tree().workspaces(): | |
| name_parts = parse_ws_name(workspace.name) | |
| icons = [] | |
| seen = set() | |
| for w in workspace.leaves(): | |
| if w.app_id or w.window_class: | |
| icon = window_icon(w) | |
| if args.duplicates or icon not in seen: | |
| icons.append(icon) | |
| seen.add(icon) | |
| name_parts["icons"] = (" ".join(icons)) if icons else None | |
| assert name_parts["num"].isdigit() | |
| new_name = cons_ws_name(name_parts) | |
| if workspace.name != new_name: | |
| ipc.command(f'rename workspace "{workspace.name}" to "{new_name}"') | |
| def undo_ws_renaming(ipc: i3ipc.Connection) -> None: | |
| for workspace in ipc.get_tree().workspaces(): | |
| name_parts = parse_ws_name(workspace.name) | |
| assert name_parts["num"].isdigit() | |
| name_parts["icons"] = None | |
| new_name = cons_ws_name(name_parts) | |
| ipc.command(f'rename workspace "{workspace.name}" to "{new_name}"') | |
| ipc.main_quit() | |
| def main() -> None: | |
| parser = argparse.ArgumentParser( | |
| description="set workspace names based on open applications." | |
| ) | |
| parser.add_argument( | |
| "-d", | |
| "--duplicates", | |
| action="store_true", | |
| help="Allow duplicate icons per application per workspace.", | |
| ) | |
| parser.add_argument( | |
| "-l", | |
| "--log", | |
| type=str, | |
| default="/tmp/sway-autoname-workspaces.log", | |
| help="Log file path.", | |
| ) | |
| args = parser.parse_args() | |
| logging.basicConfig( | |
| level=logging.INFO, | |
| format="%(message)s", | |
| handlers=[ | |
| logging.FileHandler(args.log, "w"), | |
| logging.StreamHandler(sys.stdout), | |
| ], | |
| ) | |
| ipc: i3ipc.Connection = i3ipc.Connection() | |
| def cleanup(sig: int, frame: Any) -> None: | |
| undo_ws_renaming(ipc) | |
| sys.exit(0) | |
| signal.signal(signal.SIGINT, cleanup) | |
| signal.signal(signal.SIGTERM, cleanup) | |
| def window_event_handler( | |
| _ipc: i3ipc.Connection, e: i3ipc.events.WindowEvent | |
| ) -> None: | |
| if e.change in ["new", "close", "move"]: | |
| rename_ws(_ipc, args) | |
| ipc.on("window", window_event_handler) | |
| rename_ws(ipc, args) | |
| ipc.main() | |
| if __name__ == "__main__": | |
| main() |
Author
aparatext
commented
May 4, 2025

Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment