Compare commits
2
Commits
3dd8025803
...
b9b6159e0c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9b6159e0c | ||
|
|
270805d8c4 |
@@ -1,37 +1,88 @@
|
||||
{ pkgs }:
|
||||
pkgs.writeShellApplication {
|
||||
name = "niri-shortcut-swap-monitor-workspaces";
|
||||
runtimeInputs = [
|
||||
pkgs.jq
|
||||
pkgs.niri
|
||||
];
|
||||
text = ''
|
||||
focused_output="$(niri msg --json focused-output | jq -r '.name // empty')"
|
||||
if [ -z "$focused_output" ]; then
|
||||
exit 0
|
||||
fi
|
||||
pkgs.writeScriptBin "niri-shortcut-swap-monitor-workspaces" ''
|
||||
#!${pkgs.python3}/bin/python3
|
||||
import fcntl
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import sys
|
||||
|
||||
niri msg action focus-monitor-next
|
||||
|
||||
target_output="$(niri msg --json focused-output | jq -r '.name // empty')"
|
||||
if [ -z "$target_output" ] || [ "$target_output" = "$focused_output" ]; then
|
||||
exit 0
|
||||
fi
|
||||
def disposable(workspace):
|
||||
return workspace["active_window_id"] is None and workspace["name"] is None
|
||||
|
||||
target_workspace_idx="$(
|
||||
niri msg --json workspaces \
|
||||
| jq -r --arg output "$target_output" '
|
||||
first(.[] | select(.output == $output and .is_active) | .idx) // empty
|
||||
'
|
||||
)"
|
||||
if [ -z "$target_workspace_idx" ]; then
|
||||
niri msg action focus-monitor "$focused_output"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
niri msg action focus-monitor "$focused_output"
|
||||
niri msg action move-workspace-to-monitor "$target_output"
|
||||
niri msg action focus-workspace "$target_workspace_idx"
|
||||
niri msg action move-workspace-to-monitor "$focused_output"
|
||||
'';
|
||||
def swap(connection):
|
||||
with connection.makefile("rw", encoding="utf-8") as stream:
|
||||
def request(message):
|
||||
stream.write(json.dumps(message) + "\n")
|
||||
stream.flush()
|
||||
reply = json.loads(stream.readline())
|
||||
if "Err" in reply:
|
||||
raise RuntimeError(reply["Err"])
|
||||
return reply["Ok"]
|
||||
|
||||
def action(name, **arguments):
|
||||
request({"Action": {name: arguments}})
|
||||
|
||||
workspaces = request("Workspaces")["Workspaces"]
|
||||
source = next((ws for ws in workspaces if ws["is_focused"]), None)
|
||||
active = {
|
||||
ws["output"]: ws for ws in workspaces
|
||||
if ws["output"] is not None and ws["is_active"]
|
||||
}
|
||||
if source is None or source["output"] not in active or len(active) < 2:
|
||||
return
|
||||
outputs = sorted(active)
|
||||
target = active[outputs[(outputs.index(source["output"]) + 1) % len(outputs)]]
|
||||
if source["active_window_id"] is None and target["active_window_id"] is None:
|
||||
return
|
||||
|
||||
# IDs survive moves; numeric workspace positions do not.
|
||||
moves = ((source, target), (target, source))
|
||||
for workspace, destination in moves:
|
||||
action(
|
||||
"MoveWorkspaceToMonitor",
|
||||
reference={"Id": workspace["id"]},
|
||||
output=destination["output"],
|
||||
)
|
||||
|
||||
# Activate the original monitor last to keep keyboard focus there.
|
||||
for workspace, destination in moves:
|
||||
current = request("Workspaces")["Workspaces"]
|
||||
arriving = next((
|
||||
ws for ws in current
|
||||
if ws["id"] == workspace["id"] and ws["output"] == destination["output"]
|
||||
), None)
|
||||
if arriving is None and disposable(workspace):
|
||||
# Niri may discard an unnamed empty workspace during the swap.
|
||||
arriving = max((
|
||||
ws for ws in current
|
||||
if ws["output"] == destination["output"] and disposable(ws)
|
||||
), key=lambda ws: ws["idx"], default=None)
|
||||
if arriving is None:
|
||||
raise RuntimeError("workspace or monitor disappeared during swap")
|
||||
reference = {"Id": arriving["id"]}
|
||||
# Focus empty workspaces before reordering so Niri retains them.
|
||||
action("FocusWorkspace", reference=reference)
|
||||
action("MoveWorkspaceToIndex", reference=reference, index=destination["idx"])
|
||||
|
||||
|
||||
def main():
|
||||
lock_path = os.path.join(os.environ["XDG_RUNTIME_DIR"], "niri-swap-workspaces.lock")
|
||||
with open(lock_path, "a") as lock:
|
||||
try:
|
||||
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
except BlockingIOError:
|
||||
return
|
||||
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as connection:
|
||||
connection.settimeout(5)
|
||||
connection.connect(os.environ["NIRI_SOCKET"])
|
||||
swap(connection)
|
||||
|
||||
|
||||
try:
|
||||
main()
|
||||
except (OSError, ValueError, KeyError, RuntimeError) as error:
|
||||
sys.exit(f"niri workspace swap: {error}")
|
||||
''
|
||||
Reference in new issue
Block a user