aboutsummaryrefslogtreecommitdiff
path: root/sway-de/xdg-names/.local/bin/xdg-names.py
diff options
context:
space:
mode:
Diffstat (limited to 'sway-de/xdg-names/.local/bin/xdg-names.py')
-rwxr-xr-xsway-de/xdg-names/.local/bin/xdg-names.py157
1 files changed, 157 insertions, 0 deletions
diff --git a/sway-de/xdg-names/.local/bin/xdg-names.py b/sway-de/xdg-names/.local/bin/xdg-names.py
new file mode 100755
index 0000000..fb4767a
--- /dev/null
+++ b/sway-de/xdg-names/.local/bin/xdg-names.py
@@ -0,0 +1,157 @@
+#!/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 <https://www.gnu.org/licenses/>.
+
+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()