diff options
| author | Arpit Chakladar <arpitchakladar+git@gmail.com> | 2026-08-26 16:33:16 +0530 |
|---|---|---|
| committer | Arpit Chakladar <arpitchakladar+git@gmail.com> | 2026-08-29 03:50:30 +0530 |
| commit | dd76ef7e28a15b095de6afe61a825281f90df1be (patch) | |
| tree | abd9a28c38099fbc992af11a40e2c55adc1dda60 | |
| parent | d1858c51b53e3ebcd841416490c17f7b22334e6a (diff) | |
| download | home-manager-config-dd76ef7e28a15b095de6afe61a825281f90df1be.tar.gz home-manager-config-dd76ef7e28a15b095de6afe61a825281f90df1be.zip | |
desktop: showing currently active windows in eww
- successfully showing the icons for the currently active windows
running in the current workspace
| -rw-r--r-- | modules/desktop/eww/activate-windows.py | 108 | ||||
| -rw-r--r-- | modules/desktop/eww/eww.mustache.scss | 39 | ||||
| -rw-r--r-- | modules/desktop/eww/eww.scss.mustache | 56 | ||||
| -rw-r--r-- | modules/desktop/eww/scss.nix | 2 | ||||
| -rw-r--r-- | modules/desktop/eww/workspaces.sh | 16 | ||||
| -rw-r--r-- | modules/desktop/eww/yuck.nix | 67 | ||||
| -rw-r--r-- | modules/desktop/niri/default.nix | 14 | ||||
| -rw-r--r-- | modules/scripts/activate-windows/default.nix | 23 | ||||
| -rw-r--r-- | modules/scripts/activate-windows/script.sh | 90 | ||||
| -rw-r--r-- | modules/scripts/default.nix | 2 | ||||
| -rw-r--r-- | modules/scripts/workspaces/default.nix | 23 | ||||
| -rw-r--r-- | modules/scripts/workspaces/script.sh | 3 |
12 files changed, 220 insertions, 223 deletions
diff --git a/modules/desktop/eww/activate-windows.py b/modules/desktop/eww/activate-windows.py new file mode 100644 index 0000000..b0dba4a --- /dev/null +++ b/modules/desktop/eww/activate-windows.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +"""active-windows: emit icons for windows on the currently active +niri workspace, 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") + + # fall back: scan installed .desktop files for a matching StartupWMClass + 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, + 32, + Gtk.IconLookupFlags.FORCE_SIZE) + if lookup: + icon_path = lookup.get_filename() or "" + + if not icon_path: + # last resort: treat app_id itself as an icon-theme name + lookup = icon_theme.lookup_icon(app_id, + 32, + 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_active_windows(): + workspaces = niri_json("workspaces") + windows = niri_json("windows") + + active_ws = next((w for w in workspaces if w.get("is_active")), None) + active_id = active_ws["id"] if active_ws else None + + result = [ + { + "id": w["id"], + "title": w.get("title", ""), + "app_id": w.get("app_id") or "", + "is_focused": w.get("is_focused", False), + "icon": resolve_icon_path(w.get("app_id") or ""), + } + for w in windows + if w.get("workspace_id") == active_id + ] + print(json.dumps(result), flush=True) + + +RELEVANT_EVENTS = { + "WindowOpenedOrChanged", "WindowClosed", "WindowFocusChanged", + "WorkspaceActivated", "WorkspacesChanged", "WindowsChanged", +} + + +def main(): + emit_active_windows() + 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_active_windows() + except subprocess.CalledProcessError: + pass + + +if __name__ == "__main__": + main() diff --git a/modules/desktop/eww/eww.mustache.scss b/modules/desktop/eww/eww.mustache.scss new file mode 100644 index 0000000..a24b71e --- /dev/null +++ b/modules/desktop/eww/eww.mustache.scss @@ -0,0 +1,39 @@ +* { + font-family: "Fira Code Nerd Font"; + font-size: 18px; + min-height: 0; +} + +window#eww-bar { + background: transparent; +} + +.bar-container { + background: #{{base00}}; + margin: 3px 5px; + border: 1px solid #{{base07}}; + color: #{{base07}}; + padding: 5px; +} + +.workspaces { + background: #{{base00}}; + padding: 0; + margin: 0; +} + +.workspace-btn { + padding: 8px; + color: #{{base07}}; + font-size: 24px; + background-color: #{{base00}}; + border: 1px solid #{{base03}}; + background-image: none; + box-shadow: none; + text-shadow: none; + outline: none; +} + +.workspace-btn.active { + border-color: #{{base07}}; +} diff --git a/modules/desktop/eww/eww.scss.mustache b/modules/desktop/eww/eww.scss.mustache deleted file mode 100644 index a791633..0000000 --- a/modules/desktop/eww/eww.scss.mustache +++ /dev/null @@ -1,56 +0,0 @@ -* { - font-family: "Fira Code Nerd Font"; - font-size: 18px; - min-height: 0; -} - -window#eww-bar { - background: transparent; -} - -.bar-container { - background: #{{base00}}; - border: 1px solid #{{base07}}; - padding: 5px 0; - margin: 5px; -} - -.workspaces { - background: transparent; - padding: 0; - margin: 0; -} - -.workspaces button { - background-image: none; - box-shadow: none; - text-shadow: none; - outline: none; - border: none; - padding: 5px 10px; - color: #{{base07}}; - border: 1px solid #{{base07}}; - border-color: #{{base07}}; - border-radius: 0; - min-width: 0; - min-height: 0; - margin: 0 5px; - font-size: 24px; - background: transparent; -} - -.workspaces button:hover { - background: #{{base00}}; - border: 1px solid #{{base07}}; - box-shadow: none; -} - -.workspaces button:active { - background-image: none; - box-shadow: none; -} - -.workspaces button:hover { - background: #{{base00}}; - border-color: #{{base07}}; -} diff --git a/modules/desktop/eww/scss.nix b/modules/desktop/eww/scss.nix index b535c3a..408ff64 100644 --- a/modules/desktop/eww/scss.nix +++ b/modules/desktop/eww/scss.nix @@ -3,5 +3,5 @@ ... }: config.scheme { - template = builtins.readFile ./eww.scss.mustache; + template = builtins.readFile ./eww.mustache.scss; } diff --git a/modules/desktop/eww/workspaces.sh b/modules/desktop/eww/workspaces.sh new file mode 100644 index 0000000..18193fa --- /dev/null +++ b/modules/desktop/eww/workspaces.sh @@ -0,0 +1,16 @@ +#!/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 d61f70a..df688fe 100644 --- a/modules/desktop/eww/yuck.nix +++ b/modules/desktop/eww/yuck.nix @@ -6,46 +6,65 @@ }: { config = lib.mkIf config.desktop.enable { - home.file.".config/eww/icons/fallback.svg".text = '' - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"> - <rect x="2" y="2" width="20" height="20" rx="3" fill="none" stroke="#c8ccd4" stroke-width="2"/> - </svg> - ''; - programs.eww.yuckConfig = let workspaces = pkgs.writeShellApplication { name = "eww-workspaces"; - # This automatically adds these packages to the script's PATH runtimeInputs = with pkgs; [ - niri + config.desktop.niri.package jq ]; - text = '' - #!/usr/bin/env bash - # Outputs JSON array of workspaces for eww bar - niri msg -j workspaces | jq -c 'sort_by(.idx)' - ''; + text = builtins.readFile ./workspaces.sh; }; + + activate-windows = + let + pythonEnv = pkgs.python3.withPackages (ps: [ ps.pygobject3 ]); + in + pkgs.stdenv.mkDerivation { + pname = "eww-activate-windows"; + version = "0.1.0"; + src = ./activate-windows.py; + dontUnpack = true; + meta.mainProgram = "eww-activate-windows"; + + nativeBuildInputs = [ + pkgs.wrapGAppsHook3 + pkgs.gobject-introspection + ]; + buildInputs = [ pkgs.gtk3 ]; + + installPhase = '' + mkdir -p $out/bin + install -m755 $src $out/bin/eww-activate-windows + patchShebangs $out/bin/eww-activate-windows + ''; + + # wrapGAppsHook3 wraps every executable in $out/bin automatically, + # setting GI_TYPELIB_PATH etc. from buildInputs' closure. + preFixup = '' + gappsWrapperArgs+=(--prefix PATH : "${pythonEnv}/bin") + ''; + }; in '' (defpoll time :interval "10s" "date '+%H:%M'") (defpoll date :interval "60s" "date '+%d/%m'") - (defpoll workspaces :interval "1s" :initial "[]" "${lib.getExe workspaces}") - (defpoll active-windows :interval "1s" "${lib.getExe config.scripts.eww-active-windows.package}") + (deflisten workspaces :initial "[]" "${lib.getExe workspaces}") + (deflisten active-windows :initial "[]" "${lib.getExe activate-windows}") (defwindow bar :monitor 0 :geometry (geometry :x "0px" :y "0px" - :width "50px" + :width "60px" :height "100%" :anchor "top right") :stacking "fg" :focusable "none" - :reserve (struts :distance "50px" :side "right") + :reserve (struts :distance "60px" :side "right") (bar-content)) (defwidget bar-content [] @@ -55,15 +74,19 @@ (windows-widget)))) (defwidget workspaces-widget [] - (box :orientation "v" :space-evenly false :spacing 4 :halign "center" :class "workspaces" + (box :orientation "v" :space-evenly false :spacing 4 :halign "fill" :class "workspaces" (for ws in workspaces - (button :class {ws.is_active ? "ws-active" : "ws-inactive"} - :onclick "niri msg action focus-workspace ''${ws.idx}" - (label :text "''${ws.idx}"))))) + (eventbox :onclick "niri msg action focus-workspace ''${ws.idx}" + :cursor "pointer" + :halign "fill" + (box :halign "fill" :class "workspace-btn ''${ws.is_active ? 'active' : '''}" + (label :text "''${ws.idx}")))))) (defwidget windows-widget [] (box :orientation "v" :space-evenly false :spacing 4 :halign "center" :class "windows" - (image :class "win-icon" :path "/tmp/test-icon.svg" :image-width 20 :image-height 20))) + + (for w in active-windows + (image :class "win-icon" :path "''${w.icon != ''' ? w.icon : '${../../../assets/icons/obs.svg}'}" :image-width 20 :image-height 20)))) ''; }; } diff --git a/modules/desktop/niri/default.nix b/modules/desktop/niri/default.nix index fbcb812..5c5d81b 100644 --- a/modules/desktop/niri/default.nix +++ b/modules/desktop/niri/default.nix @@ -7,9 +7,17 @@ { imports = [ ./keybindings.nix ]; - options.desktop.hardware.gpu = { - nvidia.enable = lib.mkEnableOption "Nvidia GPU Wayland optimizations"; - amd.enable = lib.mkEnableOption "AMD GPU Wayland optimizations"; + options.desktop = { + niri.package = lib.mkOption { + type = lib.types.package; + readOnly = true; + default = config.wayland.windowManager.niri.package; + description = "The niri package to use."; + }; + hardware.gpu = { + nvidia.enable = lib.mkEnableOption "Nvidia GPU Wayland optimizations"; + amd.enable = lib.mkEnableOption "AMD GPU Wayland optimizations"; + }; }; config = lib.mkIf config.desktop.enable { diff --git a/modules/scripts/activate-windows/default.nix b/modules/scripts/activate-windows/default.nix deleted file mode 100644 index 441410f..0000000 --- a/modules/scripts/activate-windows/default.nix +++ /dev/null @@ -1,23 +0,0 @@ -{ - config, - lib, - pkgs, - ... -}: -let - inherit ((import ../lib.nix { inherit lib pkgs; })) mkScriptModule; - base = mkScriptModule { - name = "eww-active-windows"; - path = ./script.sh; - description = "Outputs JSON array of active windows for eww bar\nUsed by eww to display window icons in the sidebar"; - deps = [ - pkgs.bash - pkgs.jq - ]; - inherit config; - }; -in -{ - options = base.options; - config = base.moduleConfig; -} diff --git a/modules/scripts/activate-windows/script.sh b/modules/scripts/activate-windows/script.sh deleted file mode 100644 index 2abe394..0000000 --- a/modules/scripts/activate-windows/script.sh +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env bash - -CACHE_FILE="${XDG_CACHE_HOME:-$HOME/.cache}/eww-icon-cache.tsv" -mkdir -p "$(dirname "$CACHE_FILE")" -touch "$CACHE_FILE" - -APP_DIRS=( - "$HOME/.local/share/applications" - "$HOME/.nix-profile/share/applications" - "/etc/profiles/per-user/$USER/share/applications" - "/run/current-system/sw/share/applications" - "$HOME/.local/share/flatpak/exports/share/applications" - "/var/lib/flatpak/exports/share/applications" -) - -ICON_DIRS=( - "$HOME/.local/share/icons" - "$HOME/.icons" - "$HOME/.nix-profile/share/icons" - "/etc/profiles/per-user/$USER/share/icons" - "/run/current-system/sw/share/icons" - "$HOME/.nix-profile/share/pixmaps" - "/run/current-system/sw/share/pixmaps" -) - -find_desktop_file() { - local id="$1" - for dir in "${APP_DIRS[@]}"; do - [ -d "$dir" ] || continue - local match - match=$(find "$dir" -maxdepth 1 -iname "${id}.desktop" 2>/dev/null | head -1) - [ -n "$match" ] && { echo "$match"; return; } - done - for dir in "${APP_DIRS[@]}"; do - [ -d "$dir" ] || continue - local match - match=$(grep -ilE "StartupWMClass=${id}$|^Name=${id}$" "$dir"/*.desktop 2>/dev/null | head -1) - [ -n "$match" ] && { echo "$match"; return; } - done -} - -find_icon_path() { - local icon_name="$1" - [ -z "$icon_name" ] && return - if [[ "$icon_name" == /* ]]; then - echo "$icon_name" - return - fi - for dir in "${ICON_DIRS[@]}"; do - [ -d "$dir" ] || continue - local match - match=$(find "$dir" -type f \( -iname "${icon_name}.svg" -o -iname "${icon_name}.png" -o -iname "${icon_name}.xpm" \) 2>/dev/null | sort -r | head -1) - [ -n "$match" ] && { echo "$match"; return; } - done -} - -resolve_icon() { - local app_id="$1" - local cached - cached=$(awk -F'\t' -v k="$app_id" '$1==k{print $2}' "$CACHE_FILE") - if [ -n "$cached" ]; then - echo "$cached" - return - fi - - local desktop_file icon_name icon_path - desktop_file=$(find_desktop_file "$app_id") - if [ -n "$desktop_file" ]; then - icon_name=$(grep -m1 '^Icon=' "$desktop_file" | cut -d= -f2-) - fi - icon_path=$(find_icon_path "$icon_name") - [ -z "$icon_path" ] && icon_path="$HOME/.config/eww/icons/fallback.svg" - - printf '%s\t%s\n' "$app_id" "$icon_path" >> "$CACHE_FILE" - echo "$icon_path" -} - -ws=$(niri msg -j workspaces) -active_id=$(echo "$ws" | jq '[.[] | select(.is_focused==true)][0].id') - -niri msg -j windows | jq -c --argjson wsid "$active_id" \ - '[.[] | select(.workspace_id == $wsid) | {app_id: (.app_id // "unknown"), title: (.title // "")}]' \ - | jq -c '.[]' \ - | while read -r win; do - app_id=$(jq -r '.app_id' <<< "$win") - title=$(jq -r '.title' <<< "$win") - icon=$(resolve_icon "$app_id") - jq -nc --arg app_id "$app_id" --arg title "$title" --arg icon "$icon" \ - '{app_id: $app_id, title: $title, icon: $icon}' - done | jq -sc '.' diff --git a/modules/scripts/default.nix b/modules/scripts/default.nix index d225726..ae18329 100644 --- a/modules/scripts/default.nix +++ b/modules/scripts/default.nix @@ -5,7 +5,6 @@ }: { imports = [ - ./activate-windows ./deep-clean ./gopass-ssh-load ./gpg-backup @@ -14,7 +13,6 @@ ./screen-recording ./system-monitor ./usque-warp - ./workspaces ./yazi-file-chooser ]; diff --git a/modules/scripts/workspaces/default.nix b/modules/scripts/workspaces/default.nix deleted file mode 100644 index e856c2b..0000000 --- a/modules/scripts/workspaces/default.nix +++ /dev/null @@ -1,23 +0,0 @@ -{ - config, - lib, - pkgs, - ... -}: -let - inherit ((import ../lib.nix { inherit lib pkgs; })) mkScriptModule; - base = mkScriptModule { - name = "eww-workspaces"; - path = ./script.sh; - description = "Outputs JSON array of workspaces for eww bar\nUsed by eww to display workspace indicators in the sidebar"; - deps = [ - pkgs.bash - pkgs.jq - ]; - inherit config; - }; -in -{ - options = base.options; - config = base.moduleConfig; -} diff --git a/modules/scripts/workspaces/script.sh b/modules/scripts/workspaces/script.sh deleted file mode 100644 index 061d0f7..0000000 --- a/modules/scripts/workspaces/script.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -niri msg -j workspaces | jq -c 'sort_by(.idx)' |
