diff options
Diffstat (limited to 'modules/desktop/eww')
| -rw-r--r-- | modules/desktop/eww/eww.mustache.scss | 21 | ||||
| -rw-r--r-- | modules/desktop/eww/workspaces.py | 93 | ||||
| -rw-r--r-- | modules/desktop/eww/workspaces.sh | 16 | ||||
| -rw-r--r-- | modules/desktop/eww/yuck.nix | 47 |
4 files changed, 144 insertions, 33 deletions
diff --git a/modules/desktop/eww/eww.mustache.scss b/modules/desktop/eww/eww.mustache.scss index a546aaf..64a5c7f 100644 --- a/modules/desktop/eww/eww.mustache.scss +++ b/modules/desktop/eww/eww.mustache.scss @@ -9,15 +9,13 @@ window#eww-bar { } .bar-container { - background: #{{base00}}; - margin: 3px 5px; - border: 1px solid #{{base07}}; - color: #{{base07}}; + background: transparent; } .workspaces, .windows { padding: 5px; - border-bottom: 1px solid #{{base07}}; + border: 1px solid #{{base07}}; + margin: 5px; } .workspace-button, .window-button { @@ -35,3 +33,16 @@ window#eww-bar { .workspace-button.active, .window-button.focused { border-color: #{{base07}}; } + +.workspace-button .workspace-icon { + padding: 0; + margin: 0; +} + +.workspace-button .workspace-idx { + font-size: 14px; + background: #{{base00}}; + color: #{{base07}}; + border-radius: 4px; + margin: 20px 0 0 20px; /* nudges the label into the bottom-right corner over the icon */ +} diff --git a/modules/desktop/eww/workspaces.py b/modules/desktop/eww/workspaces.py new file mode 100644 index 0000000..b8fae1b --- /dev/null +++ b/modules/desktop/eww/workspaces.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +"""workspaces: emit workspace list with each workspace's focused-app icon, +driven by niri's event-stream instead of polling.""" +import json +import subprocess +import gi +from gi.repository import Gio, Gtk +gi.require_version("Gtk", "3.0") + +icon_theme = Gtk.IconTheme.get_default() +_icon_cache: dict[str, str] = {} + + +def resolve_icon_path(app_id: str) -> str: + if not app_id: + return "" + if app_id in _icon_cache: + return _icon_cache[app_id] + + app_info = Gio.DesktopAppInfo.new(f"{app_id}.desktop") \ + or Gio.DesktopAppInfo.new(f"{app_id.lower()}.desktop") + + if app_info is None: + for info in Gio.AppInfo.get_all(): + if isinstance(info, Gio.DesktopAppInfo): + wm_class = info.get_startup_wm_class() + if wm_class and wm_class.lower() == app_id.lower(): + app_info = info + break + + icon_path = "" + gicon = app_info.get_icon() if app_info else None + if gicon is not None: + lookup = icon_theme.lookup_by_gicon(gicon, 24, Gtk.IconLookupFlags.FORCE_SIZE) + if lookup: + icon_path = lookup.get_filename() or "" + if not icon_path: + lookup = icon_theme.lookup_icon(app_id, 24, Gtk.IconLookupFlags.FORCE_SIZE) + if lookup: + icon_path = lookup.get_filename() or "" + + _icon_cache[app_id] = icon_path + return icon_path + + +def niri_json(*args): + out = subprocess.run(["niri", "msg", "-j", *args], capture_output=True, text=True, check=True) + return json.loads(out.stdout) + + +def emit_workspaces(): + workspaces = niri_json("workspaces") + windows = {w["id"]: w for w in niri_json("windows")} + + result = [] + for ws in sorted(workspaces, key=lambda w: w["idx"]): + active_win = windows.get(ws.get("active_window_id")) + app_id = (active_win or {}).get("app_id") or "" + result.append({ + "idx": ws["idx"], + "id": ws["id"], + "is_active": ws.get("is_active", False), + "icon": resolve_icon_path(app_id), + }) + print(json.dumps(result), flush=True) + + +RELEVANT_EVENTS = { + "WorkspacesChanged", "WorkspaceActivated", "WorkspaceUrgencyChanged", + "WorkspaceActiveWindowChanged", "WindowOpenedOrChanged", "WindowClosed", +} + + +def main(): + emit_workspaces() + proc = subprocess.Popen( + ["niri", "msg", "-j", "event-stream"], + stdout=subprocess.PIPE, text=True, bufsize=1, + ) + for line in proc.stdout: + try: + key = next(iter(json.loads(line))) + except (json.JSONDecodeError, StopIteration): + continue + if key in RELEVANT_EVENTS: + try: + emit_workspaces() + except subprocess.CalledProcessError: + pass + + +if __name__ == "__main__": + main() diff --git a/modules/desktop/eww/workspaces.sh b/modules/desktop/eww/workspaces.sh deleted file mode 100644 index 18193fa..0000000 --- a/modules/desktop/eww/workspaces.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -print_workspaces() { - niri msg -j workspaces | jq -c --unbuffered 'sort_by(.idx)' -} - -print_workspaces - -niri msg -j event-stream | while IFS= read -r line; do - case "$line" in - *WorkspacesChanged*|*WorkspaceActivated*|*WorkspaceUrgencyChanged*|*WorkspaceActiveWindowChanged*) - print_workspaces - ;; - esac -done diff --git a/modules/desktop/eww/yuck.nix b/modules/desktop/eww/yuck.nix index b4757b8..0654d9a 100644 --- a/modules/desktop/eww/yuck.nix +++ b/modules/desktop/eww/yuck.nix @@ -8,15 +8,35 @@ config = lib.mkIf config.desktop.enable { programs.eww.yuckConfig = let - workspaces = pkgs.writeShellApplication { - name = "eww-workspaces"; - runtimeInputs = with pkgs; [ - config.desktop.niri.package - jq - ]; + workspaces = + let + pythonEnv = pkgs.python3.withPackages (ps: [ ps.pygobject3 ]); + in + pkgs.stdenv.mkDerivation { + pname = "eww-workspaces"; + version = "0.1.0"; + src = ./workspaces.py; + dontUnpack = true; + meta.mainProgram = "eww-workspaces"; + + nativeBuildInputs = [ + pkgs.wrapGAppsHook3 + pkgs.gobject-introspection + ]; + buildInputs = [ pkgs.gtk3 ]; - text = builtins.readFile ./workspaces.sh; - }; + installPhase = '' + mkdir -p $out/bin + install -m755 $src $out/bin/eww-workspaces + patchShebangs $out/bin/eww-workspaces + ''; + + # wrapGAppsHook3 wraps every executable in $out/bin automatically, + # setting GI_TYPELIB_PATH etc. from buildInputs' closure. + preFixup = '' + gappsWrapperArgs+=(--prefix PATH : "${pythonEnv}/bin") + ''; + }; activate-windows = let @@ -77,10 +97,13 @@ (box :orientation "v" :space-evenly false :spacing 4 :halign "fill" :class "workspaces" (for ws in workspaces (eventbox :onclick "niri msg action focus-workspace ''${ws.idx}" - :cursor "pointer" - :halign "fill" - (box :halign "fill" :class "workspace-button ''${ws.is_active ? 'active' : '''}" - (label :text "''${ws.idx}")))))) + :cursor "pointer" :halign "fill" + (box :class "workspace-button ''${ws.is_active ? 'active' : '''}" :halign "fill" + (overlay :halign "fill" + (image :class "workspace-icon" + :path "''${ws.icon != ''' ? ws.icon : '${../../../assets/icons/obs.svg}'}" + :image-width 20 :image-height 20) + (label :class "workspace-idx" :text "''${ws.idx}" :halign "end" :valign "end"))))))) (defwidget windows-widget [] (box :orientation "v" :space-evenly false :spacing 4 :halign "center" :class "windows" |
