diff --git a/config/niri/config.kdl b/config/niri/config.kdl deleted file mode 100644 index 930175b..0000000 --- a/config/niri/config.kdl +++ /dev/null @@ -1,256 +0,0 @@ -// ============================================================================ -// GENERAL & SYSTEM SETTINGS -// Wiki: https://github.com/YaLTeR/niri/wiki/Configuration:-Introduction -// ============================================================================ -config-notification { - disable-failed -} - -hotkey-overlay { - skip-at-startup -} - -prefer-no-csd - -screenshot-path "~/Pictures/Screenshots/Screenshot from %Y-%m-%d %H-%M-%S.png" - -debug { - honor-xdg-activation-with-invalid-serial -} - -// ============================================================================ -// ENVIRONMENT VARIABLES -// ============================================================================ -environment { - XDG_CURRENT_DESKTOP "niri" - QT_QPA_PLATFORM "wayland;xcb" - ELECTRON_OZONE_PLATFORM_HINT "auto" - QT_QPA_PLATFORMTHEME "gtk3" - QT_QPA_PLATFORMTHEME_QT6 "gtk3" - TERMINAL "alacritty" -} - -// ============================================================================ -// AUTOSTART / SPAWN AT STARTUP -// ============================================================================ -spawn-at-startup "dms" "run" - -// testing -spawn-sh-at-startup "python3 /home/jason/Projects/dotfiles/config/niri/scripts/niri_tilemod.py --num_stack 1" - -// Export the session env to D-Bus so xdg-desktop-portal works -spawn-at-startup "dbus-update-activation-environment" "--all" - -// Audio stack -spawn-at-startup "pipewire" - -// PolicyKit authentication agent (verify path: xbps-query -f mate-polkit | grep -i agent) -spawn-at-startup "sh" "-c" "/usr/libexec/polkit-mate-authentication-agent-1 &" - -// Clipboard history backing store for Mod+V -spawn-at-startup "sh" "-c" "wl-paste --watch cliphist store" - -// ============================================================================ -// INPUT & GESTURES -// Wiki: https://github.com/YaLTeR/niri/wiki/Configuration:-Input -// ============================================================================ -input { - keyboard { - xkb { - // If this section is empty, niri will fetch xkb settings - // from org.freedesktop.locale1. You can control these using - // localectl set-x11-keymap. - } - - // Enable numlock on startup, omitting this setting disables it. - numlock - } - - // Focus windows and outputs automatically when moving the mouse into them. - focus-follows-mouse max-scroll-amount="95%" -} - -gestures { - hot-corners { - off - } -} - -// ============================================================================ -// LAYOUT & VISUAL APPEARANCE -// Wiki: https://github.com/YaLTeR/niri/wiki/Configuration:-Layout -// ============================================================================ -layout { - background-color "transparent" - - // When to center a column when changing focus: "never", "always", or "on-overflow" - center-focused-column "never" - - // Custom widths toggled by "switch-preset-column-width" (Mod+R) - preset-column-widths { - proportion 0.5 - proportion 1.0 - } - - default-column-width { proportion 0.5; } - - border { - off - width 4 - active-color "#707070" // Neutral gray - inactive-color "#d0d0d0" // Light gray - urgent-color "#cc4444" // Softer red - } - - shadow { - softness 30 - spread 5 - offset x=0 y=5 - color "#0007" - } - - struts { - } -} - -overview { - workspace-shadow { - off - } -} - -// ============================================================================ -// ANIMATIONS -// ============================================================================ -animations { - workspace-switch { - spring damping-ratio=0.80 stiffness=523 epsilon=0.0001 - } - window-open { - duration-ms 150 - curve "ease-out-expo" - } - window-close { - duration-ms 150 - curve "ease-out-quad" - } - horizontal-view-movement { - spring damping-ratio=0.85 stiffness=423 epsilon=0.0001 - } - window-movement { - spring damping-ratio=0.75 stiffness=323 epsilon=0.0001 - } - window-resize { - spring damping-ratio=0.85 stiffness=423 epsilon=0.0001 - } - config-notification-open-close { - spring damping-ratio=0.65 stiffness=923 epsilon=0.001 - } - screenshot-ui-open { - duration-ms 200 - curve "ease-out-quad" - } - overview-open-close { - spring damping-ratio=0.85 stiffness=800 epsilon=0.0001 - } -} - -// ============================================================================ -// WINDOW & LAYER RULES -// Wiki: https://github.com/YaLTeR/niri/wiki/Configuration:-Window-Rules -// ============================================================================ -layer-rule { - match namespace="^quickshell$" - place-within-backdrop true -} - -window-rule { - match app-id=r#"^org\.gnome\."# - draw-border-with-background false - geometry-corner-radius 12 - clip-to-geometry true -} - -window-rule { - match app-id=r#"^gnome-control-center$"# - match app-id=r#"^pavucontrol$"# - match app-id=r#"^nm-connection-editor$"# - default-column-width { proportion 0.5; } - open-floating false -} - -window-rule { - match app-id=r#"^org\.gnome\.Calculator$"# - match app-id=r#"^gnome-calculator$"# - match app-id=r#"^galculator$"# - match app-id=r#"^blueman-manager$"# - match app-id=r#"^org\.gnome\.Nautilus$"# - match app-id=r#"^xdg-desktop-portal$"# - open-floating true -} - -window-rule { - match app-id="Alacritty" - match app-id="kitty" - draw-border-with-background false -} - -window-rule { - match app-id=r#"firefox$"# title="^Picture-in-Picture$" - match app-id="zoom" - open-floating true -} - -// Force the Steam client, Friends List, and settings windows to float -window-rule { - match app-id="steam" - open-floating true -} - -window-rule { - match app-id="steam" - match title="Friends List" - open-floating true - default-floating-position x=10 y=10 relative-to="bottom-right" -} - -window-rule { - match app-id=r#"^steam$"# title=r#"^notificationtoasts_\d+_desktop$"# - default-floating-position x=10 y=10 relative-to="top-right" - open-focused false -} - -// Automatically fullscreen games launched via Proton -window-rule { - // Proton games typically use "steam_app_" as their app-id. - // The r#"..."# syntax tells niri to evaluate this as a regular expression. - match app-id=r#"^steam_app_\d+$"# - open-fullscreen true -} - -// ============================================================================ -// NAVIGATION & KEYBINDINGS -// ============================================================================ -// Override to disable super+tab -recent-windows { - binds { - Alt+Tab { next-window scope="output"; } - Alt+Shift+Tab { previous-window scope="output"; } - Alt+grave { next-window filter="app-id"; } - Alt+Shift+grave { previous-window filter="app-id"; } - } -} - -// ============================================================================ -// INCLUDES (DMS MANAGEMENT) -// ============================================================================ -// @NOTE: only dms file we could edit -include optional=true "dms/binds.kdl" - -// @NOTE: Non-editable files below all managed by dms -include optional=true "dms/colors.kdl" -include optional=true "dms/layout.kdl" -include optional=true "dms/alttab.kdl" -include optional=true "dms/outputs.kdl" -include optional=true "dms/cursor.kdl" -include optional=true "dms/input.kdl" \ No newline at end of file diff --git a/config/niri/dms/binds.kdl b/config/niri/dms/binds.kdl deleted file mode 100644 index 15194bc..0000000 --- a/config/niri/dms/binds.kdl +++ /dev/null @@ -1,153 +0,0 @@ -binds { - // ======================================================================== - // SYSTEM & DMS APPLICATION LAUNCHERS - // ======================================================================== - // Screenshots - Alt+Print hotkey-overlay-title="Screenshot: Window" { spawn "dms" "screenshot" "window"; } - Alt+XF86Launch1 hotkey-overlay-title="Screenshot: Window" { spawn "dms" "screenshot" "window"; } - Ctrl+Print hotkey-overlay-title="Screenshot: Full Screen" { spawn "dms" "screenshot" "full"; } - Ctrl+XF86Launch1 hotkey-overlay-title="Screenshot: Full Screen" { spawn "dms" "screenshot" "full"; } - Mod+P hotkey-overlay-title="Screenshot: Region" { spawn "dms" "screenshot"; } - - // Launchers & Overlays - Mod+D hotkey-overlay-title="Application Launcher" { spawn "dms" "ipc" "call" "spotlight" "toggle"; } - Mod+space hotkey-overlay-title="Spotlight Bar: Toggle" { spawn "dms" "ipc" "call" "spotlight-bar" "toggle"; } - Mod+Y hotkey-overlay-title="Browse Wallpapers" { spawn "dms" "ipc" "call" "dash" "toggle" "wallpaper"; } - Super+Backspace hotkey-overlay-title="Power Menu: Toggle" { spawn "dms" "ipc" "call" "powermenu" "toggle"; } - Mod+Alt+L hotkey-overlay-title="Lock Screen" { spawn "dms" "ipc" "call" "lock" "lock"; } - - // Utilities & Tools - Ctrl+Alt+Delete hotkey-overlay-title="Task Manager" { spawn "dms" "ipc" "call" "processlist" "focusOrToggle"; } - Mod+M hotkey-overlay-title="Task Manager" { spawn "dms" "ipc" "call" "processlist" "focusOrToggle"; } - Mod+N hotkey-overlay-title="Notification Center" { spawn "dms" "ipc" "call" "notifications" "toggle"; } - Mod+S hotkey-overlay-title="Notepad" { spawn "dms" "ipc" "call" "notepad" "toggle"; } - Mod+V hotkey-overlay-title="Clipboard Manager" { spawn "dms" "ipc" "call" "clipboard" "toggle"; } - Mod+Comma hotkey-overlay-title="Settings" { spawn "dms" "ipc" "call" "settings" "focusOrToggle"; } - - // Configuration Helpers - Ctrl+Shift+R hotkey-overlay-title="Rename Workspace" { spawn "dms" "ipc" "call" "workspace-rename" "open"; } - Mod+Shift+W hotkey-overlay-title="Create window rule" { spawn "dms" "ipc" "call" "window-rules" "toggle"; } - - // Audio Control (MPRIS) - Ctrl+XF86AudioLowerVolume allow-when-locked=true { spawn "dms" "ipc" "call" "mpris" "decrement" "3"; } - Ctrl+XF86AudioRaiseVolume allow-when-locked=true { spawn "dms" "ipc" "call" "mpris" "increment" "3"; } - - // ======================================================================== - // WINDOW & COLUMN FOCUS - // ======================================================================== - // Focus Columns (Horizontal) - Mod+H { focus-column-left; } - Mod+Left { focus-column-left; } - Mod+L { focus-column-right; } - Mod+Right { focus-column-right; } - Mod+Home { focus-column-first; } - Mod+End { focus-column-last; } - - // Focus Windows within Column (Vertical) - Mod+J { focus-window-down; } - Mod+Down { focus-window-down; } - Mod+K { focus-window-up; } - Mod+Up { focus-window-up; } - - // Mouse Wheel Focus Navigation - Mod+WheelScrollLeft { focus-column-left; } - Mod+WheelScrollRight { focus-column-right; } - Mod+Shift+WheelScrollUp { focus-column-left; } - Mod+Shift+WheelScrollDown { focus-column-right; } - - // ======================================================================== - // WINDOW & COLUMN MANIPULATION - // ======================================================================== - // Close Window - // Mod+Q repeat=false { close-window; } - Mod+Q { spawn-sh "bash /home/jason/Projects/dotfiles/config/niri/scripts/niri_close_helper.sh"; } - - // Move Columns (Horizontal) - Mod+Shift+H { move-column-left; } - Mod+Shift+Left { move-column-left; } - Mod+Shift+L { move-column-right; } - Mod+Shift+Right { move-column-right; } - Mod+Ctrl+Home { move-column-to-first; } - Mod+Ctrl+End { move-column-to-last; } - Mod+Ctrl+WheelScrollLeft { move-column-left; } - Mod+Ctrl+WheelScrollRight { move-column-right; } - Mod+Ctrl+Shift+WheelScrollUp { move-column-left; } - Mod+Ctrl+Shift+WheelScrollDown { move-column-right; } - - // Move Windows within Column (Vertical) - Mod+Shift+J { move-window-down; } - Mod+Shift+Down { move-window-down; } - Mod+Shift+K { move-window-up; } - Mod+Shift+Up { move-window-up; } - - // Sizing & Layout Constraints - Mod+Equal { set-column-width "+10%"; } - Mod+Minus { set-column-width "-10%"; } - Mod+Shift+Equal { set-window-height "+10%"; } - Mod+Shift+Minus { set-window-height "-10%"; } - Mod+R { switch-preset-column-width; } - Mod+Shift+R { switch-preset-window-height; } - Mod+Ctrl+R { reset-window-height; } - Mod+Ctrl+F { expand-column-to-available-width; } - Mod+F { maximize-column; } - Mod+Shift+F { fullscreen-window; } - - // Positioning, Floating & Tiling States - Mod+C { center-column; } - Mod+Ctrl+C { center-visible-columns; } - Mod+W { toggle-column-tabbed-display; } - Mod+Shift+T { toggle-window-floating; } - Mod+Shift+V { switch-focus-between-floating-and-tiling; } - Mod+BracketLeft { consume-or-expel-window-left; } - Mod+BracketRight { consume-or-expel-window-right; } - Mod+Period { expel-window-from-column; } - - // ======================================================================== - // WORKSPACE MANAGEMENT - // ======================================================================== - // Switch Workspace Focus - Mod+Page_Up { focus-workspace-up; } - Mod+I { focus-workspace-up; } - Mod+Page_Down { focus-workspace-down; } - Mod+U { focus-workspace-down; } - Mod+WheelScrollUp cooldown-ms=150 { focus-workspace-up; } - Mod+WheelScrollDown cooldown-ms=150 { focus-workspace-down; } - - // Switch Workspace by Index - Mod+1 { focus-workspace 1; } - Mod+2 { focus-workspace 2; } - Mod+3 { focus-workspace 3; } - Mod+4 { focus-workspace 4; } - - // Move Focused Column to Workspace - Mod+Ctrl+Up { move-column-to-workspace-up; } - Mod+Ctrl+I { move-column-to-workspace-up; } - Mod+Ctrl+Down { move-column-to-workspace-down; } - Mod+Ctrl+U { move-column-to-workspace-down; } - Mod+Ctrl+WheelScrollUp cooldown-ms=150 { move-column-to-workspace-up; } - Mod+Ctrl+WheelScrollDown cooldown-ms=150 { move-column-to-workspace-down; } - - // Move Focused Column to Workspace Index - Mod+Shift+1 { move-column-to-workspace 1; } - Mod+Shift+2 { move-column-to-workspace 2; } - Mod+Shift+3 { move-column-to-workspace 3; } - Mod+Shift+4 { move-column-to-workspace 4; } - - // Move Entire Workspace Position - Mod+Shift+Page_Up { move-workspace-up; } - Mod+Shift+I { move-workspace-up; } - Mod+Shift+Page_Down { move-workspace-down; } - Mod+Shift+U { move-workspace-down; } - - // ======================================================================== - // COMPOSITOR & SESSION CONTROLS - // ======================================================================== - - Mod+T hotkey-overlay-title="Open Terminal" { spawn "alacritty"; } - Mod+O repeat=false { toggle-overview; } - Mod+Tab repeat=false { toggle-overview; } - Mod+Slash { show-hotkey-overlay; } - Mod+Escape allow-inhibiting=false { toggle-keyboard-shortcuts-inhibit; } - Mod+Shift+P { power-off-monitors; } - Mod+Shift+E { quit; } -} \ No newline at end of file diff --git a/config/niri/scripts/niri_close_helper.sh b/config/niri/scripts/niri_close_helper.sh deleted file mode 100755 index b360cdb..0000000 --- a/config/niri/scripts/niri_close_helper.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/bin/bash - -# Parse 'close left' flag if present -ENABLE_CLOSE_LEFT=false -if [[ $# -gt 0 ]]; then - if [[ $1 == "-l" || $1 == "-left" || $1 == "--left" ]]; then - ENABLE_CLOSE_LEFT=true - else - notify-send "close_helper.sh" "Flag error!\nProvide '-l' or '--left' to enable close-left mode" - fi -fi - -# ----- Handle special cases ----- - -# Bail if we don't have a focused window -# -> Will also happen when overview is open, so try to close anyways -CURR_WIN_INFO=$(niri msg -j focused-window) -WINID_TO_CLOSE=$(jq .id <<< $CURR_WIN_INFO) -if [[ $WINID_TO_CLOSE == "null" ]]; then - niri msg action close-window - exit -fi - -# Close the window if floating or has no workspace -# -> No workspace can happen if window is being dragged, for example -WSPACE_ID=$(jq .workspace_id <<< $CURR_WIN_INFO) -IS_FLOATING=$(jq .is_floating <<< $CURR_WIN_INFO) -if $IS_FLOATING || [[ $WSPACE_ID == "null" ]]; then - niri msg action close-window --id $WINID_TO_CLOSE - exit -fi - -# If we're in the first column, close normally (nothing else to do) -COL_IDX=$(jq .layout.pos_in_scrolling_layout[0] <<< $CURR_WIN_INFO) -if [[ $COL_IDX -eq 1 ]]; then - niri msg action close-window --id $WINID_TO_CLOSE - exit -fi - -# If we're in a stacked column, close normally (let niri handle focus change within column) -ALL_WIN_INFO=$(niri msg -j windows | jq --argjson wsid $WSPACE_ID 'map(select(.workspace_id == $wsid))') -NUM_ROWS_IN_COL=$(jq --argjson cidx $COL_IDX 'map(select(.layout.pos_in_scrolling_layout[0] == $cidx)) | length' <<< $ALL_WIN_INFO) -if [[ $NUM_ROWS_IN_COL -gt 1 ]]; then - niri msg action close-window --id $WINID_TO_CLOSE - exit -fi - -# If there are exactly 2 (un-stacked) windows, close normally -# -> Odd detail, but needed to preserve 'always-center-single-column' behavior -NUM_WINS=$(jq length <<< $ALL_WIN_INFO) -if [[ $NUM_WINS -eq 2 ]]; then - niri msg action close-window --id $WINID_TO_CLOSE - exit -fi - -# ----- End of special cases ----- - -# Figure out which window (left or right) to focus after closing -LAST_COL_IDX=$(jq '[.[].layout.pos_in_scrolling_layout[0]] | max' <<< $ALL_WIN_INFO) -if $ENABLE_CLOSE_LEFT || [[ $COL_IDX -eq $LAST_COL_IDX ]]; then - niri msg action focus-column-left -else - niri msg action focus-column-right -fi -AFTERCLOSE_WIN_ID=$(niri msg -j focused-window | jq .id) - -# Close and force view re-alignment to get rid of empty space on the right -niri msg action close-window --id $WINID_TO_CLOSE -niri msg action focus-column-first -niri msg action focus-window --id $AFTERCLOSE_WIN_ID diff --git a/config/niri/scripts/niri_tilemod.py b/config/niri/scripts/niri_tilemod.py deleted file mode 100644 index 104c818..0000000 --- a/config/niri/scripts/niri_tilemod.py +++ /dev/null @@ -1,816 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - - -# --------------------------------------------------------------------------------------------------------------------- -# %% Imports - -import argparse -import json -import os -import subprocess - -from pathlib import Path -from time import sleep, perf_counter -from socket import socket, AF_UNIX, SOCK_STREAM -from signal import signal, SIGTERM -from enum import Enum - -# Potentially missing lib (requires python v3.11+, but v3.10 is still considered supported!) -HAS_TOML_LIB = True -try: - import tomllib -except ImportError: - HAS_TOML_LIB = False - - -# --------------------------------------------------------------------------------------------------------------------- -# %% Args - -# Define script arguments -parser = argparse.ArgumentParser( - description="Script which makes niri tile windows up to a certain stack height, for a given range of columns", -) -parser.add_argument( - "-cfg", - "--config_path", - type=str, - default="~/.config/niri/tilemod_config.toml", - help="Path to .json or .toml config file (default: '~/.config/niri/tilemod_config.toml')", -) -parser.add_argument( - "-n", - "--num_stack", - default=2, - type=int, - help="Number of windows to stack in columns (default 2). Use 0 or 1 to disable stacking", -) -parser.add_argument( - "-b", - "--column_bounds", - nargs=2, - default=[2, 2], - type=int, - metavar="column-index", - help="Column start/end range where tiling should occur (default: 2 2), niri indexing starts at 1", -) -parser.add_argument( - "-rtl", - "--right_to_left", - action="store_true", - help="Open windows in right-to-left order", -) -parser.add_argument( - "-m", - "--apply_to_moved_windows", - action="store_true", - help="When a window is moved into another workspace, treat it as a newly spawned window", -) -parser.add_argument( - "--autostack_last_column_only", - action="store_true", - help="Only enable auto-stacking when the new window is opened in the last-most column", -) -parser.add_argument( - "--autostack_into_row_gaps", - nargs="?", # 0 or 1 arg - default=False, - const=True, - choices=["strict"], - metavar="'strict' : optional", - help="If set, windows can auto-stack into columns that are not full height (e.g. to fill gaps). Include 'strict' to only stack windows that would fit the gap", -) -parser.add_argument( - "--action_maximize", - type=str, - default="MaximizeColumn", - choices=["MaximizeColumn", "MaximizeWindowToEdges", "FullscreenWindow"], - help="Set which maximization action to use (default: MaximizeColumn)", -) -parser.add_argument( - "--no_outer_stack", - action="store_true", - help="Disable auto-stacking to the right if column is available (or left, when RTL is enabled)", -) -parser.add_argument( - "--no_maximize_solos", - action="store_true", - help="Disable auto-maximize of first window opened on a workspace", -) -parser.add_argument( - "--no_maximize_on_close", - action="store_true", - help="Disable re-maximize when only one window remains", -) -parser.add_argument( - "--no_collapse_on_open", - action="store_true", - help="Disable collapse of solo maximized window when opening a second window", -) -parser.add_argument( - "--no_config_reload", - action="store_true", - help="Disable ability to reload tiling config by opening the overview", -) -parser.add_argument( - "-d", - "--delay_startup_ms", - default=250, - type=int, - help="Number of milliseconds to delay before listening to niri IPC (default: 500)", -) -parser.add_argument("-k", "--config_printout", action="store_true", help="Print out the active config (in terminal)") -parser.add_argument("--notify_unexpected", action="store_true", help="Enable notifications for unexpected behavior") - -# Get script configs -args, unknown_args = parser.parse_known_args() -ARG_CONFIG_PATH = Path(args.config_path).expanduser() -ARG_NUM_STACK = args.num_stack -ARG_COLUMN_TILING_BOUNDS = args.column_bounds -ARG_ALLOW_OUTER_STACK = not args.no_outer_stack -ARG_ENABLE_RTL = args.right_to_left -ARG_MAXIMIZE_SOLOS_ON_OPEN = not args.no_maximize_solos -ARG_MAXIMIZE_SOLOS_ON_CLOSE = not args.no_maximize_on_close -ARG_COLLAPSE_SOLOS_ON_OPEN = not args.no_collapse_on_open -ARG_APPLY_TO_MOVED_WINDOWS = args.apply_to_moved_windows -ARG_AUTOSTACK_LAST_ONLY = args.autostack_last_column_only -ARG_AUTOSTACK_INTO_GAPS = args.autostack_into_row_gaps -ARG_ACTION_MAXIMIZE = args.action_maximize -STARTUP_DELAY_MS = args.delay_startup_ms -ENABLE_CONFIG_RELOAD = not args.no_config_reload -RUN_CONFIG_PRINTOUT = args.config_printout -NOTIFY_UNEXPECTED = args.notify_unexpected - -# Handle startup delay (prevent listening to niri during potentially busy startup) -if STARTUP_DELAY_MS > 0: - sleep(STARTUP_DELAY_MS / 1000) - - -# --------------------------------------------------------------------------------------------------------------------- -# %% Classes - - -class NiriIPC: - """Helper used to read/write from niri IPC""" - - def __init__(self, socket_path: str, buffer_size: int = 4096): - niri_socket = socket(AF_UNIX, SOCK_STREAM) - niri_socket.connect(socket_path) - - self._writer = niri_socket.makefile("w", buffering=buffer_size) - self._reader = niri_socket.makefile("r", buffering=buffer_size) - self._socket = niri_socket - self._batched_actions_list = [] - self._batched_action_names = [] - - def _parse_response(self) -> tuple[bool, dict | str]: - - # Parse 'Ok' response - resp_str = self._reader.readline() - resp_dict = json.loads(resp_str) - resp_data = resp_dict.get("Ok", None) - is_ok = resp_data is not None - if is_ok: - return is_ok, resp_data - - # Handle 'Err' response - resp_data = resp_dict.get("Err", None) - return is_ok, resp_data - - def batch_action(self, action: str, **kwargs): - """Queue up an action, without executing immediately. Use .commit_batch() to execute""" - action_json_str = json.dumps({"Action": {action: kwargs}}, indent=None, separators=(",", ":")) - self._batched_actions_list.append(action_json_str) - self._batched_action_names.append(action) - return self - - def commit_batch(self) -> list[tuple[str, bool, str]]: - - # Bail if no actions have been batched - num_actions = len(self._batched_actions_list) - if num_actions == 0: - return [] - - # Write single (batched) action string - batched_action_str = "\n".join(self._batched_actions_list) - batched_names = self._batched_action_names - self._batched_actions_list = [] - self._batched_action_names = [] - self._writer.write(batched_action_str) - self._writer.write("\n") - self._writer.flush() - - # Read response for each batch item - return [(name, self._parse_response()) for name in batched_names] - - def message(self, message: str) -> tuple[bool, dict | str]: - self._writer.write(f'"{message}"\n') - self._writer.flush() - return self._parse_response() - - def close(self): - self._writer.close() - self._reader.close() - self._socket.close() - - def read_event_stream(self) -> tuple[str, dict]: - - # Try to request event stream - ok_stream, evt_resp = self.message("EventStream") - if not ok_stream: - raise ConnectionError(f"Error starting EventStream\n{evt_resp}") - - # Read responses (delimited by '\n') - self._writer.close() - for line in self._reader: - for evt_message in line.splitlines(): - msg_json = json.loads(evt_message) - evt_name = tuple(msg_json.keys())[0] - evt_data = msg_json.get(evt_name, {}) - yield evt_name, evt_data - - return - - -class WindowSizeState(Enum): - NOT_MAXIMIZED = 0 - MAX_COLUMN = 1 - MAX_TO_EDGES = 2 - FULLSCREEN = 3 - - -# --------------------------------------------------------------------------------------------------------------------- -# %% Helpers - - -def get_tiling_config(tiling_configs_dict: dict, workspace_dict: dict, outputs_dict: dict) -> tuple[str, dict]: - """Helper used to select tiling config. Checks workspace properties before monitor properties!""" - - # Get workspace/monitor-specific config - match_key, augment_cfg = None, {} - for section_key, section_properties_dict in (("workspaces", workspace_dict), ("outputs", outputs_dict)): - for prop_name, property_entries in tiling_configs_dict[section_key].items(): - if prop_name not in section_properties_dict.keys(): - continue - curr_prop_value = str(section_properties_dict[prop_name]) - if curr_prop_value in property_entries: - augment_cfg = property_entries[curr_prop_value] - match_key = f"{section_key}.{prop_name}.{curr_prop_value}" - break - - # Break outer loop (e.g. if we find a workspace match first, ignore monitor match) - if match_key is not None: - break - - # Fill in missing match key - if match_key is None: - match_key = "default" - - # Return specific tiling config - default_cfg = tiling_configs_dict["default"] - return match_key, {**default_cfg, **augment_cfg} - - -def load_user_config(config_path: Path, has_tomllib: bool = False, raise_errors: bool = True) -> tuple[bool, dict]: - """Helper used to load .toml or .json config file, with basic error handling""" - - # Initilize outputs - ok_config, loaded_config = False, {} - - config_file_ext = config_path.suffix.lower() - if config_file_ext == ".toml": - if not has_tomllib: - notify("Error, no support for .toml config\n(requires python 3.11+)") - raise ModuleNotFoundError("No support for .toml file (requires python 3.11+). Please use .json instead") - try: - with open(config_path, "rb") as in_file: - loaded_config = tomllib.load(in_file) - ok_config = True - except tomllib.TOMLDecodeError as err: - notify("Error parsing toml config!\nRun in terminal to see more details") - if raise_errors: - raise err - - elif config_file_ext == ".json": - try: - with open(config_path, "r") as in_file: - loaded_config = json.load(in_file) - ok_config = True - except json.JSONDecodeError as err: - notify("Error parsing json config!\nRun in terminal to see more details") - if raise_errors: - raise err - - return ok_config, loaded_config - - -def update_working_config(base_config: dict[str, dict], update_config: dict[str, dict]) -> dict[str, dict]: - """Helper used to update a base config with (potentially sparse) update data""" - new_config_dict = {} - for key in base_config.keys(): - new_config_dict[key] = {**base_config[key], **update_config.get(key, {})} - return new_config_dict - - -def notify(message: str, timeout_ms: int | None = None) -> None: - notify_title = f"{Path(__file__).name}" - subprocess.run(["notify-send", notify_title, message, *([] if timeout_ms is None else ["-t", str(timeout_ms)])]) - return - - -def get_tiled_workspace_windows(multi_window_dict: dict[int, dict], workspace_id: int) -> dict[int, dict]: - """ - Instead of checking the 'is_floating' state to determine if a window is tiled, - we check that it has valid column/row indexing, which should generalize better for edge cases - See: https://github.com/heyoeyo/niri_tweaks/issues/18 - """ - is_tiled_in_wspace = lambda win_info: ( - (win_info["workspace_id"] == workspace_id) and (win_info["layout"]["pos_in_scrolling_layout"] is not None) - ) - return {win_id: win_info for win_id, win_info in multi_window_dict.items() if is_tiled_in_wspace(win_info)} - - -def get_focused_id(multi_info_dict: dict[int, dict]) -> int | None: - focused_id = None - for data in multi_info_dict.values(): - if data["is_focused"]: - focused_id = data["id"] - break - return focused_id - - -def get_missing_size_state( - single_window_dict: dict, - monitor_info: dict, - max_w_threshold: float = 0.8, -) -> WindowSizeState: - """Helper used to guess at missing maximization/fullscreen window state""" - - monitor_w = monitor_info["logical"]["width"] - monitor_h = monitor_info["logical"]["height"] - win_w_px, win_h_px = single_window_dict["layout"]["tile_size"] - gap_w, gap_h = (monitor_w - int(win_w_px)), (monitor_h - int(win_h_px)) - - win_state = WindowSizeState.NOT_MAXIMIZED - if (gap_w < 5) and (gap_h < 5): - win_state = WindowSizeState.FULLSCREEN - elif (gap_w < 5) or (gap_h < 5): - win_state = WindowSizeState.MAX_TO_EDGES - elif (win_w_px / monitor_w) > max_w_threshold: - win_state = WindowSizeState.MAX_COLUMN - - return win_state - - -def get_rows_per_column(multi_window_dict: dict[int, dict]) -> dict[int, int]: - """Assumes tiled windows only! Gets rows per column as a dictionary, format: {column_index: row_count}""" - - col_count_per_idx = {} - for win_info in multi_window_dict.values(): - col_idx = win_info["layout"]["pos_in_scrolling_layout"][0] - if col_idx not in col_count_per_idx.keys(): - col_count_per_idx[col_idx] = 0 - col_count_per_idx[col_idx] += 1 - - return col_count_per_idx - - -def get_col_index(single_window_dict: dict) -> int: - """Helper used to simplify access to column x-index. Assumes a tiled window!""" - return single_window_dict["layout"]["pos_in_scrolling_layout"][0] - - -def sigterm_to_interrupt(signum, frame): - raise InterruptedError - - -# --------------------------------------------------------------------------------------------------------------------- -# %% Parse config - -# Define base/default config, based on script args -base_configs_dict = { - "default": { - "num_stack": ARG_NUM_STACK, - "column_bounds": ARG_COLUMN_TILING_BOUNDS, - "maximize_solos_on_open": ARG_MAXIMIZE_SOLOS_ON_OPEN, - "maximize_solos_on_close": ARG_MAXIMIZE_SOLOS_ON_CLOSE, - "collapse_solos_on_open": ARG_COLLAPSE_SOLOS_ON_OPEN, - "allow_outer_stack": ARG_ALLOW_OUTER_STACK, - "apply_to_moved_windows": ARG_APPLY_TO_MOVED_WINDOWS, - "autostack_last_column_only": ARG_AUTOSTACK_LAST_ONLY, - "autostack_into_row_gaps": ARG_AUTOSTACK_INTO_GAPS, - "right_to_left": ARG_ENABLE_RTL, - "action_maximize": ARG_ACTION_MAXIMIZE, - "disabled": False, - }, - "outputs": {}, - "workspaces": {}, -} - -# Try to load/parse from config file -last_config_modify_time = 0 -all_configs_dict = {**base_configs_dict} -if ARG_CONFIG_PATH.exists(): - last_config_modify_time = ARG_CONFIG_PATH.stat().st_mtime - _, loaded_config = load_user_config(ARG_CONFIG_PATH, HAS_TOML_LIB, raise_errors=True) - all_configs_dict = update_working_config(base_configs_dict, loaded_config) - print("", "Loaded config file:", ARG_CONFIG_PATH, sep="\n") -else: - print("", "No config file found:", ARG_CONFIG_PATH, "-> Will use default config", sep="\n") - - -# --------------------------------------------------------------------------------------------------------------------- -# %% Setup - -# Set up separate connections for sending commands during event stream -SCRIPTNAME = Path(__file__).name -PATH_SOCKET = os.environ["NIRI_SOCKET"] -if not os.path.exists(PATH_SOCKET): - raise FileNotFoundError(f"Cannot connect to niri socket: {PATH_SOCKET}") -event_stream_reader = NiriIPC(PATH_SOCKET) -niri = NiriIPC(PATH_SOCKET) -print("", f"({SCRIPTNAME}) - Opened niri IPC connection", sep="\n") - -# Check versioning -response_ok, response_version = niri.message("Version") -expected_version = "26.04" -if (not response_ok) or (expected_version not in response_version.get("Version")): - version_msg = f"Expecting version {expected_version}, got: {response_version.get('Version', None)}" - if NOTIFY_UNEXPECTED: - sleep(1) - notify(version_msg) - print("", version_msg, sep="\n") - -# Initialize global state -is_overview_open: bool = False -focused_win_id: int | None = None -focused_wspace_id: int | None = None -all_win_dict: dict[int, dict] = {} -all_wspace_dict: dict[int, dict] = {} -all_monitor_dict: dict[str, dict] = {} - - -# --------------------------------------------------------------------------------------------------------------------- -# %% *** IPC listening loop *** - -# Convert SIGTERM into python interrupt error for graceful shutdown -signal(SIGTERM, sigterm_to_interrupt) - -try: - # Print out active config and close. Used for debugging - if RUN_CONFIG_PRINTOUT: - _, response_focused_output = niri.message("FocusedOutput") - _, response_workspaces = niri.message("Workspaces") - curr_wspace_dict = [info for info in response_workspaces["Workspaces"] if info["is_focused"]][0] - curr_monitor_dict = response_focused_output["FocusedOutput"] - match_key, config = get_tiling_config(all_configs_dict, curr_wspace_dict, curr_monitor_dict) - print("", f"Tiling config: {match_key}", sep="\n") - print(json.dumps(config, indent=2) if not config["disabled"] else "***DISABLED***", flush=True) - raise KeyboardInterrupt() - - # For all event docs, see: https://niri-wm.github.io/niri/niri_ipc/enum.Event.html - timestamp_ms = int(1000 * perf_counter()) - for evt_name, evt_data in event_stream_reader.read_event_stream(): - - # Uncomment for debugging - # new_timestamp = int(1000 * perf_counter()) - # time_elapsed_ms, timestamp_ms = (new_timestamp - timestamp_ms), new_timestamp - # if time_elapsed_ms > 250: - # print("", f"Time elapsed (ms): {time_elapsed_ms}", sep="\n", flush=True) - # print("EVENT:", evt_name, flush=True) - # print(evt_data) - - # Reset event state - new_win_dict = None - closed_win_dict = None - is_win_move_wspace_event = False - - # Handle events - if "WindowFocusChanged" == evt_name: - evt_win_id = evt_data["id"] - if focused_win_id is not None: - all_win_dict[focused_win_id]["is_focused"] = False - if evt_win_id is not None: - all_win_dict[evt_win_id]["is_focused"] = True - focused_win_id = evt_win_id - - elif "WindowFocusTimestampChanged" == evt_name: - # Very noisy event but not needed, so skip it. Usage would look like: - # all_win_dict[evt_data["id"]]["focus_timestamp"] = evt_data["focus_timestamp"] - pass - - elif "WorkspaceActiveWindowChanged" == evt_name: - # Very noisy event but not needed, so skip it. Usage would look like: - # all_wspace_dict[evt_data["workspace_id"]]["active_window_id"] = evt_data["active_window_id"] - pass - - elif "WindowOpenedOrChanged" == evt_name: - # Confusing event! This triggers when a new window is opened but... - # - also triggers if certain window properties change (e.g. title, is_floating, workspace_id) - # - does NOT trigger for all property changes (e.g. is_focused, layout) - # - will trigger on drag-start/end with floating windows - evt_win_data = evt_data["window"] - evt_win_id = evt_win_data["id"] - - # Check for new window vs. moved window events - is_new_window = evt_win_id not in all_win_dict.keys() - if not is_new_window: - is_win_move_wspace_event = evt_win_data["workspace_id"] != all_win_dict[evt_win_id]["workspace_id"] - if is_new_window or is_win_move_wspace_event: - new_win_dict = evt_win_data - - # Update focus, if needed - if evt_data["window"]["is_focused"]: - if focused_win_id is not None: - all_win_dict[focused_win_id]["is_focused"] = False - focused_win_id = evt_win_id - - # Record new/updated window data - all_win_dict[evt_win_id] = evt_win_data - - elif "WindowClosed" == evt_name: - evt_win_id = evt_data["id"] - if focused_win_id == evt_win_id: - focused_win_id = None - closed_win_dict = all_win_dict.pop(evt_win_id) - - elif "WindowUrgencyChanged" == evt_name: - all_win_dict[evt_data["id"]]["is_urgent"] = evt_data["urgent"] - - elif "WindowLayoutsChanged" == evt_name: - for evt_win_id, evt_new_layout in evt_data["changes"]: - all_win_dict[evt_win_id]["layout"] = evt_new_layout - - elif "WorkspaceActivated" == evt_name: - if evt_data["focused"]: - evt_wspace_id = evt_data["id"] - if focused_wspace_id is not None: - all_wspace_dict[focused_wspace_id]["is_focused"] = False - if evt_wspace_id is not None: - all_wspace_dict[evt_wspace_id]["is_focused"] = True - focused_wspace_id = evt_wspace_id - pass - - elif "WorkspaceUrgencyChanged" == evt_name: - all_wspace_dict[evt_data["id"]]["is_urgent"] = evt_data["urgent"] - - elif "OverviewOpenedOrClosed" == evt_name: - is_overview_open = evt_data["is_open"] - - # Reload config if file has changed - if ENABLE_CONFIG_RELOAD and ARG_CONFIG_PATH.exists(): - new_config_modify_time = ARG_CONFIG_PATH.stat().st_mtime - if new_config_modify_time > last_config_modify_time: - last_config_modify_time = new_config_modify_time - ok_config, loaded_config = load_user_config(ARG_CONFIG_PATH, HAS_TOML_LIB, raise_errors=False) - if ok_config: - all_configs_dict = update_working_config(base_configs_dict, loaded_config) - notify("Config reloaded!", timeout_ms=2500) - pass - pass - - elif "WorkspacesChanged" == evt_name: - # Signals full re-write of workspace data. Happens on startup or when workspaces are added/removed - # Event data format: {"workspaces": [... list of workspace dicts ...]} - all_wspace_dict = {data["id"]: data for data in evt_data["workspaces"]} - focused_wspace_id = get_focused_id(all_wspace_dict) - - elif "WindowsChanged" == evt_name: - # Signals full re-write of window data. Happens on startup (only?) - # Event data format: {"windows": [... list of window dicts ...]} - all_win_dict = {data["id"]: data for data in evt_data["windows"]} - focused_win_id = get_focused_id(all_win_dict) - - elif "ConfigLoaded" == evt_name: - # We'll re-read monitor info on config changes (not clear if there is an event for attaching monitors?) - ok_monitors, outputs_data = niri.message("Outputs") - if not ok_monitors: - notify("Error getting monitor data...") - continue - all_monitor_dict = outputs_data["Outputs"] - - elif evt_name in ( - "CastStartedOrChanged", - "CastStopped", - "CastsChanged", - "KeyboardLayoutSwitched", - "KeyboardLayoutsChanged", - "ScreenshotCaptured", - ): - # Do nothing - pass - - else: - print("Unknown event:", evt_name) - if NOTIFY_UNEXPECTED: - notify("Unknown evemt: {evt_name}") - - # ............................................................................................................. - # %% Window management - - # Handle post-processing after window close - if closed_win_dict is not None: - - # Ignore closing of floating/untiled windows (don't affect tiling) - is_untiled = closed_win_dict["layout"]["pos_in_scrolling_layout"] is None - if is_untiled: - continue - - # Get location info of closed window - curr_wspace_id = closed_win_dict["workspace_id"] - curr_wspace_dict = all_wspace_dict.get(curr_wspace_id, None) - if curr_wspace_dict is None: - # Can happen when a window is closed (remotely) on a dynamic workspace - # -> The workspace is removed before the window, so we don't have a valid id! - continue - - # Get active config - curr_monitor_dict = all_monitor_dict[curr_wspace_dict["output"]] - _, config = get_tiling_config(all_configs_dict, curr_wspace_dict, curr_monitor_dict) - if config["disabled"]: - continue - - # Handle re-maximizing solo windows - tile_win_dict = get_tiled_workspace_windows(all_win_dict, curr_wspace_id) - if len(tile_win_dict) == 1 and config["maximize_solos_on_close"]: - solo_id = tuple(tile_win_dict.keys())[0] - solo_win_size_state = get_missing_size_state(tile_win_dict[solo_id], curr_monitor_dict) - if solo_win_size_state == WindowSizeState.NOT_MAXIMIZED: - if focused_win_id != solo_id: - niri.batch_action("FocusWindow", id=solo_id) - niri.batch_action(config["action_maximize"]) - niri.commit_batch() - pass - - # *** New window event *** - if new_win_dict is not None: - - # Get window location data - new_win_id = new_win_dict["id"] - curr_wspace_id = new_win_dict["workspace_id"] - if curr_wspace_id is None: # This happens when dragging windows! - curr_wspace_id = focused_wspace_id - curr_wspace_dict = all_wspace_dict[curr_wspace_id] - curr_monitor_dict = all_monitor_dict[curr_wspace_dict["output"]] - - # Ignore floats/un-tiled or windows that seem to be created with window rules (don't want to interfere) - new_win_size_state = get_missing_size_state(new_win_dict, curr_monitor_dict) - is_already_maximized = new_win_size_state != WindowSizeState.NOT_MAXIMIZED - is_on_another_wspace = curr_wspace_id != focused_wspace_id - is_untiled = new_win_dict["layout"]["pos_in_scrolling_layout"] is None - if is_untiled or is_already_maximized or is_on_another_wspace: - continue - - # Get active tiling config (can be different for workspaces/monitors) - _, config = get_tiling_config(all_configs_dict, curr_wspace_dict, curr_monitor_dict) - if config["disabled"]: - continue - - # Skip behavior if the window is 'new' because it moved workspaces, if configured - if is_win_move_wspace_event and not config["apply_to_moved_windows"]: - continue - - # Get (tiled-only) windows on workspace of new window, since that's all we care about - tile_win_dict = get_tiled_workspace_windows(all_win_dict, curr_wspace_id) - num_tile_wins = len(tile_win_dict) - - # Important note (as of v26.04): - # When a new window appears, it will report itself as being in row 1 of the - # column to the right of the current focused window, even if there is already - # a window there (i.e it will report a duplicated column/row index). - # For example if we have windows A and B but are focused on A, like: [A-focused] [B] - # and spawn a window 'C' like: [A] [C-focused] [B], then: - # [A] has xy: (1,1), [B] *still* has xy: (2,1) and [C] *also* has xy: (2,1) - # -> This will mess up neighbouring window checks if we're not careful! - - # For clarity, get stacking config - col_tiling_bounds = config["column_bounds"] - num_stack = config["num_stack"] - enable_rtl = config["right_to_left"] - enable_strict_fill_gaps = config["autostack_into_row_gaps"] == "strict" - enable_fill_gaps = enable_strict_fill_gaps or (config["autostack_into_row_gaps"] == True) - - # Try to handle toml formatting errors on column indexing - if not isinstance(col_tiling_bounds, list) or len(col_tiling_bounds) != 2: - notify(f"Invalid column_bounds: {col_tiling_bounds}\nMust be a list of two numbers", timeout_ms=2500) - col_tiling_bounds = [0, 1_000_000] - - # Get rows per column count on current workspace - new_win_col_idx = get_col_index(new_win_dict) - rows_per_column_dict = get_rows_per_column(tile_win_dict) - rows_per_column_dict[new_win_col_idx] -= 1 # Don't count the new window itself - if rows_per_column_dict[new_win_col_idx] == 0: - rows_per_column_dict.pop(new_win_col_idx) - - # Figure out the surrounding column indices - side_col_iter = [(new_win_col_idx - 1, -1)] - if config["allow_outer_stack"]: - side_col_iter.append((new_win_col_idx, +1)) - - # Adjust column index checks if we're in right-to-left mode - if enable_rtl: - if config["allow_outer_stack"]: - side_col_iter[1] = (new_win_col_idx - 2, -3) - max_col_idx = max(rows_per_column_dict.keys(), default=0) - col_tiling_bounds = [max_col_idx - idx + 1 for idx in col_tiling_bounds] - - # Disable stacking checks if we're not in the last-most column with last-only config - if config["autostack_last_column_only"]: - rightmost_col_idx = max(rows_per_column_dict.keys(), default=1) - is_opened_last_col = (new_win_col_idx > rightmost_col_idx) if not enable_rtl else (new_win_col_idx == 2) - if not is_opened_last_col: - side_col_iter = [] - - # Check if we need to shift into another column - start_colidx, end_colidx = sorted(col_tiling_bounds) - need_shift_amt = 0 - for side_col_idx, shift_amt in side_col_iter: - if side_col_idx not in rows_per_column_dict.keys(): - continue - - # Check for auto-stacking within column bounds - num_side_rows = rows_per_column_dict[side_col_idx] - if (num_side_rows < num_stack) and (start_colidx <= side_col_idx <= end_colidx): - need_shift_amt = shift_amt - break - - # Special check for filling row gaps (e.g. if a single window is not filling column height) - if enable_fill_gaps and num_side_rows == 1: - is_neighbour = lambda info: (info["id"] != new_win_id) and (get_col_index(info) == side_col_idx) - nb_win_info = (info for info in tile_win_dict.values() if is_neighbour(info)) - nb_col_height = sum(info["layout"]["tile_size"][1] for info in nb_win_info) - monitor_height = curr_monitor_dict["logical"]["height"] - if enable_strict_fill_gaps: - new_win_height = new_win_dict["layout"]["tile_size"][1] - if (nb_col_height + new_win_height) <= monitor_height: - need_shift_amt = shift_amt - break - elif nb_col_height < (monitor_height * 0.8): - need_shift_amt = shift_amt - break - pass - - # Handle shifting - is_focus_on_new_win = focused_win_id == new_win_id - if need_shift_amt != 0: - shift_cmd = "ConsumeOrExpelWindowLeft" if need_shift_amt < 0 else "ConsumeOrExpelWindowRight" - for _ in range(abs(need_shift_amt) - 1): - niri.batch_action(shift_cmd, id=new_win_id) - niri.batch_action(shift_cmd, id=new_win_id) - - elif enable_rtl: - # Handle right-to-left movement (no stacking) - if is_focus_on_new_win: - niri.batch_action("MoveColumnLeft") - niri.batch_action("FocusColumnRight") - niri.batch_action("FocusColumnLeft") - else: - niri.batch_action("ConsumeOrExpelWindowLeft", id=new_win_id) - niri.batch_action("ConsumeOrExpelWindowLeft", id=new_win_id) - - # Handle solo-window maximization - if num_tile_wins == 1 and config["maximize_solos_on_open"]: - if new_win_size_state == WindowSizeState.NOT_MAXIMIZED: - niri.batch_action("FocusWindow", id=new_win_id) - niri.batch_action(config["action_maximize"]) - if not is_focus_on_new_win and (focused_win_id is not None): - niri.batch_action("FocusWindow", id=focused_win_id) - - # Collapse maximized windows when no longer solo - if num_tile_wins == 2 and config["collapse_solos_on_open"]: - other_win_id = [win_id for win_id in tile_win_dict.keys() if win_id != new_win_id][0] - other_win_size_state = get_missing_size_state(tile_win_dict[other_win_id], curr_monitor_dict) - if other_win_size_state != WindowSizeState.NOT_MAXIMIZED: - niri.batch_action("FocusWindow", id=other_win_id) - if other_win_size_state == WindowSizeState.MAX_COLUMN: - niri.batch_action("MaximizeColumn") - elif other_win_size_state == WindowSizeState.MAX_TO_EDGES: - niri.batch_action("MaximizeWindowToEdges") - # -> Could collapse fullscreen state, but probably not desired... - niri.batch_action("FocusWindow", id=focused_win_id if focused_win_id is not None else new_win_id) - pass - - # Trigger all actions & report any errors - action_responses = niri.commit_batch() - for name_action, (ok_action, response_msg) in action_responses: - if not ok_action: - notify(f"Action error: {name_action}\n{response_msg}") - print("Action error!", action_responses, sep="\n") - - pass - -except (KeyboardInterrupt, InterruptedError): - pass - -except ConnectionError as err: - notify(str(err)) - raise err - -except Exception as err: - notify(f"Unexpected error:\n{err}") - raise err - -finally: - event_stream_reader.close() - niri.close() - print("", f"({SCRIPTNAME}) - Closed niri IPC connection", sep="\n")