wren-linux / bin / wren-dim
#!/bin/sh
# wren-dim: written by Wren Linux parts/core-commands.sh
# Makes it plain which window has the focus: the other windows on its workspace fade, so
# whatever is behind them (the wallpaper, or the window under a pop-up) shows through. sway can't
# darken a window, only make it see-through. Windows on other workspaces and screens are left alone.
# wren-dim run (sway starts it)
# wren-dim off stop, for good; every window goes back
# wren-dim on start again
# wren-dim 0.4 how much they fade, from 0.1 to 0.9; 0.3 unless you choose (it's kept)
# wren-dim status
# It only reads window numbers from sway, and only sets opacity. It fades relative to what a
# window had, so a window that is already see-through stays that way when it has the focus.
# (the name after "-" is how a later wren-dim tells this one is running)
WREN_DIM_SELF=$(readlink -f -- "$0") exec python3 -I - wren-dim "$@" <<'PY'
import json
import os
import signal
import subprocess
import sys
CONFIG_HOME = os.environ.get("XDG_CONFIG_HOME") or os.path.expanduser("~/.config")
SETTING = os.path.join(CONFIG_HOME, "wren", "dim")
RUN = os.environ.get("XDG_RUNTIME_DIR") or "/tmp"
PID = os.path.join(RUN, "wren-dim.pid")
DEFAULT = 0.3
def setting():
try:
with open(SETTING) as f:
return f.read().strip()
except OSError:
return ""
def strength():
try:
s = float(setting())
except ValueError:
return DEFAULT
return s if 0.1 <= s <= 0.9 else DEFAULT
def running():
try:
with open(PID) as f:
pid = int(f.read())
with open(f"/proc/{pid}/cmdline", "rb") as f:
return pid if b"wren-dim" in f.read() else None
except (OSError, ValueError):
return None
def stop():
pid = running()
if pid:
os.kill(pid, signal.SIGTERM)
for _ in range(50):
if not running():
break
subprocess.run(["sleep", "0.1"])
def keep(value):
os.makedirs(os.path.dirname(SETTING), exist_ok=True)
with open(SETTING, "w") as f:
f.write(value + "\n")
def start():
subprocess.Popen([os.environ["WREN_DIM_SELF"]], start_new_session=True, stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
def swaymsg(command):
subprocess.run(["swaymsg", "-q", command], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
def to_dim():
"""The numbers of the windows to fade: the others on the focused window's workspace."""
out = subprocess.run(["swaymsg", "-t", "get_tree"], capture_output=True, text=True).stdout
tree = json.loads(out)
dim = set()
def windows(node, focused, found):
focused = focused or node.get("focused", False)
kids = node.get("nodes", []) + node.get("floating_nodes", [])
if not kids and node.get("type") in ("con", "floating_con"):
found.append((node["id"], focused))
for k in kids:
windows(k, focused, found)
def workspaces(node):
if node.get("type") == "workspace":
found = []
windows(node, False, found)
if any(f for _, f in found):
dim.update(i for i, f in found if not f)
return
for k in node.get("nodes", []):
workspaces(k)
workspaces(tree)
return dim
def run():
if setting() == "off" or running():
return 0
with open(PID, "w") as f:
f.write(str(os.getpid()))
fade = strength()
dimmed = set()
def apply(want):
nonlocal dimmed
cmds = [f"[con_id={i}] opacity minus {fade}" for i in want - dimmed]
cmds += [f"[con_id={i}] opacity plus {fade}" for i in dimmed - want]
if cmds:
swaymsg("; ".join(cmds))
dimmed = set(want)
def finish(*_):
apply(set())
try:
os.unlink(PID)
except OSError:
pass
os._exit(0)
signal.signal(signal.SIGTERM, finish)
signal.signal(signal.SIGINT, finish)
watch = subprocess.Popen(["swaymsg", "-m", "-t", "subscribe", '["window","workspace"]'],
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
apply(to_dim())
# (sway's events give whole objects, one after another: only the fact of one matters here)
for _ in watch.stdout:
try:
apply(to_dim())
except (ValueError, KeyError):
pass
finish()
def main():
arg = sys.argv[2] if len(sys.argv) > 2 else ""
if not arg:
return run()
if arg == "status":
print(f"on, fading {strength()}" if running() else "off")
return 0
if arg == "off":
keep("off")
stop()
return 0
if arg == "on":
if setting() == "off":
keep(str(DEFAULT))
else:
try:
value = float(arg)
except ValueError:
value = -1
if not 0.1 <= value <= 0.9:
print("usage: wren-dim [on | off | status | a number from 0.1 to 0.9]", file=sys.stderr)
return 2
keep(arg)
stop()
start()
return 0
sys.exit(main())
PY