Skip to content

Instantly share code, notes, and snippets.

@aparatext
Last active May 4, 2025 10:14
Show Gist options
  • Select an option

  • Save aparatext/2ba97dc3ed9a5675841e96ad81b56c74 to your computer and use it in GitHub Desktop.

Select an option

Save aparatext/2ba97dc3ed9a5675841e96ad81b56c74 to your computer and use it in GitHub Desktop.
Auto-name Sway/i3wm Workspaces
#!/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()
@aparatext
Copy link
Author

image

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