mirror of
https://github.com/DeeJanuz/frametop.git
synced 2026-10-06 06:00:09 +02:00
[verified] merge experimental into nested AT-SPI fix
This commit is contained in:
commit
5e3323c188
143 files changed
+20723
-841
No files matched your search
Executable
+143
@@ -0,0 +1,143 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Bring back a taskbar that Plasma saved against a screen this desktop doesn't have.
|
||||
|
||||
Plasma 6.2.5 ties each panel to a screen number (lastScreen in its containment), and the
|
||||
numbers rank the enabled outputs by priority: 0 is the primary screen. A panel whose
|
||||
number is past the screen count gets no view and stays hidden; Plasma never moves it,
|
||||
even on a later start (sanitizeScreenLayout() only remaps a panel whose number has no
|
||||
desktop, and the Frametop desktop keeps a desktop for every spare output it has seen).
|
||||
So a taskbar saved on a spare output (issue #18), or on a screen that a smaller layout
|
||||
dropped, was lost until the config was deleted.
|
||||
|
||||
The session runs this before Plasma starts, so nothing races Plasma for the file. Each
|
||||
panel numbered at or past the screen count moves to screen 0, with its system tray's
|
||||
containment, and keeps its widgets and settings. A panel is left where it is when screen
|
||||
0 already has a panel on that edge: it comes back by itself if the screens do. The file
|
||||
is backed up once per repair (<file>.ft-bak), and the changes go through kwriteconfig6.
|
||||
|
||||
fix-panels.py [--screens N] [--file APPLETSRC] [--check]
|
||||
--screens the desktop's screen count (default: the configured layout's)
|
||||
--file default: $XDG_CONFIG_HOME/plasma-org.kde.plasma.desktop-appletsrc, with
|
||||
XDG_CONFIG_HOME defaulting to the Frametop desktop's ~/.config/frametop
|
||||
--check change nothing; list the panels, and exit 1 if one is lost
|
||||
"""
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
EDGES = {3: "top", 4: "bottom", 5: "left", 6: "right"} # Plasma::Types::Location
|
||||
SYSTRAY = "org.kde.plasma.private.systemtray"
|
||||
GROUP = re.compile(r"\[([^\]]*)\]")
|
||||
|
||||
|
||||
def parse(text):
|
||||
"""KConfig text -> {(group, subgroup, ...): {key: value}}."""
|
||||
groups, cur = {}, None
|
||||
for line in text.splitlines():
|
||||
line = line.strip()
|
||||
if line.startswith("["):
|
||||
cur = tuple(GROUP.findall(line))
|
||||
groups.setdefault(cur, {})
|
||||
elif cur is not None and "=" in line and not line.startswith("#"):
|
||||
k, v = line.split("=", 1)
|
||||
groups[cur][re.sub(r"\[\$.*\]$", "", k.strip())] = v.strip()
|
||||
return groups
|
||||
|
||||
|
||||
def number(v, default=-1):
|
||||
try:
|
||||
return int(v)
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
|
||||
|
||||
def panels(groups):
|
||||
"""The panels, by containment id: location, lastScreen, and the ids of their system
|
||||
trays' own containments (which sit on the same edge and screen as the panel)."""
|
||||
trays, found = {}, {}
|
||||
for g, keys in groups.items():
|
||||
if len(g) == 5 and g[0] == "Containments" and g[2] == "Applets" and g[4] == "Configuration":
|
||||
tray = keys.get("SystrayContainmentId")
|
||||
if tray:
|
||||
trays.setdefault(g[1], []).append(tray)
|
||||
owned = {t for ts in trays.values() for t in ts}
|
||||
for g, keys in groups.items():
|
||||
if len(g) != 2 or g[0] != "Containments" or number(g[1]) <= 0 or g[1] in owned:
|
||||
continue
|
||||
loc = number(keys.get("location"), 0)
|
||||
if loc in EDGES and keys.get("plugin") != SYSTRAY:
|
||||
found[g[1]] = {"location": loc, "screen": number(keys.get("lastScreen")),
|
||||
"plugin": keys.get("plugin", "?"), "trays": trays.get(g[1], [])}
|
||||
return dict(sorted(found.items(), key=lambda kv: number(kv[0])))
|
||||
|
||||
|
||||
def plan(groups, screens):
|
||||
"""(moves, kept): moves are (panel id, from screen, containment ids to put on screen 0);
|
||||
kept are (panel id, from screen, why) for lost panels left alone."""
|
||||
found = panels(groups)
|
||||
taken = {(p["screen"], p["location"]) for p in found.values() if 0 <= p["screen"] < screens}
|
||||
moves, kept = [], []
|
||||
for pid, p in found.items():
|
||||
if p["screen"] < screens:
|
||||
continue # on a screen this desktop has (Plasma puts -1 on the first one itself)
|
||||
if (0, p["location"]) in taken:
|
||||
kept.append((pid, p["screen"], f"the first screen already has a {EDGES[p['location']]} panel"))
|
||||
continue
|
||||
taken.add((0, p["location"]))
|
||||
moves.append((pid, p["screen"], [pid, *p["trays"]]))
|
||||
return moves, kept
|
||||
|
||||
|
||||
def default_screens():
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), "..", "layout"))
|
||||
import ft_layout
|
||||
return ft_layout.screen_count()
|
||||
|
||||
|
||||
def main(argv):
|
||||
ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
|
||||
ap.add_argument("--screens", type=int)
|
||||
ap.add_argument("--file")
|
||||
ap.add_argument("--check", action="store_true")
|
||||
a = ap.parse_args(argv)
|
||||
config = os.environ.get("XDG_CONFIG_HOME") or os.path.expanduser("~/.config/frametop")
|
||||
path = a.file or os.path.join(config, "plasma-org.kde.plasma.desktop-appletsrc")
|
||||
screens = a.screens if a.screens else default_screens()
|
||||
try:
|
||||
with open(path) as f:
|
||||
groups = parse(f.read())
|
||||
except FileNotFoundError:
|
||||
if a.check:
|
||||
print("no Plasma config yet (Plasma makes the default taskbar)")
|
||||
return 0
|
||||
moves, kept = plan(groups, screens)
|
||||
|
||||
if a.check:
|
||||
found = panels(groups)
|
||||
for pid, p in found.items():
|
||||
lost = " (lost: no such screen)" if p["screen"] >= screens else ""
|
||||
print(f"panel {pid}: screen {p['screen']}, {EDGES[p['location']]}{lost}")
|
||||
if not found:
|
||||
print("no panels")
|
||||
print(f"{screens} screen(s)")
|
||||
return 1 if moves or kept else 0
|
||||
|
||||
if moves:
|
||||
shutil.copy2(path, path + ".ft-bak")
|
||||
for pid, was, ids in moves:
|
||||
for cid in ids:
|
||||
subprocess.run(["kwriteconfig6", "--file", os.path.abspath(path), "--group", "Containments",
|
||||
"--group", cid, "--key", "lastScreen", "0"], check=True)
|
||||
print(f"frametop: panel {pid} was saved on screen {was}, which this desktop doesn't have "
|
||||
f"({screens} screen(s)); moved it to the first screen (backup: {path}.ft-bak)", file=sys.stderr)
|
||||
for pid, was, why in kept:
|
||||
print(f"frametop: panel {pid} is saved on screen {was}, which this desktop doesn't have; "
|
||||
f"left there: {why}", file=sys.stderr)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv[1:]))
|
||||
@@ -249,8 +249,47 @@ elif [ "$(kreadconfig6 --file "$kwinrc" --group org.kde.kdecoration2 --key theme
|
||||
kwriteconfig6 --file "$kwinrc" --group org.kde.kdecoration2 --key theme --delete
|
||||
fi
|
||||
|
||||
# KWin draws through zink on the headset's GPU, which vrcompositor needs, so the blur and
|
||||
# background contrast behind panels and menus and the window animations cost frames for
|
||||
# little. They're off by default in this desktop. Each is written once, before KWin first
|
||||
# reads it, and only if this desktop's config doesn't have it yet; the marker keeps it
|
||||
# from coming back, since System Settings deletes a setting put back to KDE's default.
|
||||
# docs/reference.md says how to turn them on again.
|
||||
default() { # file group key value
|
||||
[ -n "$(kreadconfig6 --file "$1" --group "$2" --key "$3")" ] || kwriteconfig6 --file "$1" --group "$2" --key "$3" "$4"
|
||||
}
|
||||
frametoprc=$XDG_CONFIG_HOME/frametoprc
|
||||
if [ "$(kreadconfig6 --file "$frametoprc" --group Defaults --key effects)" != 1 ]; then
|
||||
default "$kwinrc" Plugins blurEnabled false
|
||||
default "$kwinrc" Plugins contrastEnabled false
|
||||
default "$XDG_CONFIG_HOME/kdeglobals" KDE AnimationDurationFactor 0
|
||||
kwriteconfig6 --file "$frametoprc" --group Defaults --key effects 1
|
||||
fi
|
||||
|
||||
# System autostart entries this desktop doesn't need, hidden for it alone by a copy with
|
||||
# Hidden=true in its own autostart folder, once and unless there's a file of that name
|
||||
# already. Discover's update notifier starts Discover itself to check for updates (about
|
||||
# 600 MB and a share of a core, with Flatpak's helper and AppStream behind it); updates
|
||||
# come with SteamOS and from Discover in the stock desktop. IBus can't reach the
|
||||
# desktop's apps: KWin's input method is ft-textinput, and the session drops the
|
||||
# variables that point apps at IBus or XIM. Deleting the copy brings an entry back.
|
||||
if [ "$(kreadconfig6 --file "$frametoprc" --group Defaults --key autostart)" != 1 ]; then
|
||||
for entry in org.kde.discover.notifier ibus; do
|
||||
src=/etc/xdg/autostart/$entry.desktop dst=$XDG_CONFIG_HOME/autostart/$entry.desktop
|
||||
[ -r "$src" ] && [ ! -e "$dst" ] || continue
|
||||
mkdir -p "$(dirname "$dst")"
|
||||
sed '/^\[Desktop Entry\]$/a Hidden=true' "$src" > "$dst"
|
||||
done
|
||||
kwriteconfig6 --file "$frametoprc" --group Defaults --key autostart 1
|
||||
fi
|
||||
|
||||
# Profiles reopen apps (docs/profiles.md), so Plasma's own session restore stays off here;
|
||||
# with both, apps would open twice.
|
||||
kwriteconfig6 --file "$XDG_CONFIG_HOME/ksmserverrc" --group General --key loginMode emptySession
|
||||
|
||||
# Plasma keeps a panel on a screen number, and never moves one whose screen this desktop
|
||||
# doesn't have, like a spare output or a screen a smaller layout dropped. Put such a panel
|
||||
# back on the first (primary) screen before Plasma reads the file (session/fix-panels.py).
|
||||
python3 "$here/fix-panels.py" --screens "$screens" || true
|
||||
|
||||
dbus-run-session startplasma-wayland
|
||||
@@ -38,7 +38,7 @@ POINTER_GAZE_SHOW=1 # gaze mode, with POINTER_GAZE_DOT=moving: the dot sh
|
||||
POINTER_GAZE_MOUSE=precision # gaze mode: the left button holds back its press, the mouse steers, the release clicks (precision) | clicks right away (direct)
|
||||
POINTER_HEAD_DEADZONE=0.5 # keyboard clicks at the gaze (Meta+J, Meta+K): degrees the head turns before the held dot moves with it
|
||||
POINTER_KEY_TAP=0.25 # keyboard clicks: let go within this (s) and it clicks where the dot was at the press, and tells the gaze tracker it was right
|
||||
GAZE_TRACKER=steam # gaze service: steam = SteamVR's eye tracker | own = our own (gaze/tracker: its frame grabber needs gaze/tracker/install.sh; calibrated with Calibrate on Frametop Input Settings' Gaze page)
|
||||
GAZE_TRACKER=auto # gaze service: auto = our own eye tracker when it's installed (gaze/tracker/install.sh, which install.sh offers), else SteamVR's | own = ours | steam = SteamVR's. Each keeps its own calibration (Calibrate on Frametop Input Settings' Gaze page)
|
||||
GAZE_EYE=auto # gaze service: eye bias. auto = each eye weighted by how far off it was at your recent nudges | left | right = that eye counts twice
|
||||
HANDS_SWAP_SIDES=0 # hand tracking (hands/run.sh install): 1 = the side cameras' names are swapped, which some SteamVR restarts cause (hands/tools/check_sides.py --ring tells)
|
||||
HANDS_CPUS=5,6,7 # hand tracking: the CPUs its model threads run on
|
||||
@@ -54,7 +54,6 @@ POINTER_PINCH_DEADZONE=1.5 # hands: degrees the pinching hand moves before the p
|
||||
POINTER_GRIP_GAIN=1 # hands: the pointer moves this times the gripping hand's angle
|
||||
POINTER_GRIP_BELOW=0.35 # hands: grips beginning more than this (m) below the eyes are ignored (hands on a desk)
|
||||
POINTER_PINCH_TYPING=1 # hands: no pinch within this many seconds of a key (typing touches thumb to index)
|
||||
META_DASHBOARD=0 # 1 = a Meta tap on a pass-through keyboard toggles the SteamVR dashboard (pointer mode only)
|
||||
SHARE_KEYS=0 # 1 = keys of keyboards grabbed for the desktop also go to @frametop_keys, for hotkey tools (any local process can listen)
|
||||
DISPLAY_OFF_MIN=0 # minutes without use before frametop-power turns the displays off, even if the headset seems worn (0 = never)
|
||||
DISPLAY_MOVE_MM=5 # displays off: the headset or a controller moving this far (mm) within 10 s counts as use
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests for session/fix-panels.py: issue #18's config (the taskbar saved on spare output 8
|
||||
of a 3-screen desktop), panels that are fine, an edge that's taken, and a real run of
|
||||
kwriteconfig6 on a copy. Nothing here touches the running desktop or its config.
|
||||
|
||||
python3 session/tests/test_fix_panels.py
|
||||
"""
|
||||
import importlib.util
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
HERE = os.path.dirname(os.path.realpath(__file__))
|
||||
SCRIPT = os.path.join(HERE, "..", "fix-panels.py")
|
||||
spec = importlib.util.spec_from_file_location("fix_panels", SCRIPT)
|
||||
fp = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(fp)
|
||||
|
||||
|
||||
def desktop(cid, screen):
|
||||
return f"""[Containments][{cid}]
|
||||
activityId=7e1d1a3c-0000-4000-8000-000000000000
|
||||
formfactor=0
|
||||
immutability=1
|
||||
lastScreen={screen}
|
||||
location=0
|
||||
plugin=org.kde.plasma.folder
|
||||
wallpaperplugin=org.kde.image
|
||||
"""
|
||||
|
||||
|
||||
def panel(cid, screen, location=4, tray=None):
|
||||
text = f"""[Containments][{cid}]
|
||||
activityId=
|
||||
formfactor=2
|
||||
immutability=1
|
||||
lastScreen={screen}
|
||||
location={location}
|
||||
plugin=org.kde.panel
|
||||
wallpaperplugin=org.kde.image
|
||||
|
||||
[Containments][{cid}][Applets][{cid}1]
|
||||
immutability=1
|
||||
plugin=org.kde.plasma.kickoff
|
||||
"""
|
||||
if tray:
|
||||
text += f"""
|
||||
[Containments][{cid}][Applets][{cid}2]
|
||||
immutability=1
|
||||
plugin=org.kde.plasma.systemtray
|
||||
|
||||
[Containments][{cid}][Applets][{cid}2][Configuration]
|
||||
PreloadWeight=90
|
||||
SystrayContainmentId={tray}
|
||||
|
||||
[Containments][{tray}]
|
||||
activityId=
|
||||
formfactor=2
|
||||
immutability=1
|
||||
lastScreen={screen}
|
||||
location={location}
|
||||
plugin=org.kde.plasma.private.systemtray
|
||||
popupHeight=432
|
||||
"""
|
||||
return text
|
||||
|
||||
|
||||
ISSUE_18 = "\n".join([desktop(1, 0), desktop(18, 1), desktop(19, 2)] + [desktop(20 + i, 3 + i) for i in range(8)]
|
||||
+ [panel(10, 8, tray=11), "[ScreenMapping]\nitemsOnDisabledScreens=\n"])
|
||||
|
||||
|
||||
class Plan(unittest.TestCase):
|
||||
def test_issue_18(self):
|
||||
moves, kept = fp.plan(fp.parse(ISSUE_18), 3)
|
||||
self.assertEqual(moves, [("10", 8, ["10", "11"])])
|
||||
self.assertEqual(kept, [])
|
||||
|
||||
def test_panel_on_a_screen_stays(self):
|
||||
for screen in (0, 2, -1):
|
||||
moves, kept = fp.plan(fp.parse(desktop(1, 0) + panel(2, screen, tray=8)), 3)
|
||||
self.assertEqual((moves, kept), ([], []), screen)
|
||||
|
||||
def test_tray_is_not_a_panel(self):
|
||||
found = fp.panels(fp.parse(panel(2, 0, tray=8)))
|
||||
self.assertEqual(list(found), ["2"])
|
||||
self.assertEqual(found["2"]["trays"], ["8"])
|
||||
|
||||
def test_desktops_never_move(self):
|
||||
moves, _ = fp.plan(fp.parse(ISSUE_18), 1)
|
||||
self.assertEqual([m[0] for m in moves], ["10"])
|
||||
|
||||
def test_edge_taken_on_first_screen(self):
|
||||
text = panel(2, 0, tray=8) + panel(10, 8, tray=11)
|
||||
moves, kept = fp.plan(fp.parse(text), 3)
|
||||
self.assertEqual(moves, [])
|
||||
self.assertEqual([k[:2] for k in kept], [("10", 8)])
|
||||
|
||||
def test_other_edge_is_free(self):
|
||||
moves, kept = fp.plan(fp.parse(panel(2, 0) + panel(10, 4, location=3)), 3)
|
||||
self.assertEqual(moves, [("10", 4, ["10"])])
|
||||
self.assertEqual(kept, [])
|
||||
|
||||
def test_two_lost_on_one_edge(self):
|
||||
moves, kept = fp.plan(fp.parse(panel(10, 5) + panel(12, 8)), 3)
|
||||
self.assertEqual([m[0] for m in moves], ["10"])
|
||||
self.assertEqual([k[0] for k in kept], ["12"])
|
||||
|
||||
def test_screen_count_went_down(self):
|
||||
moves, _ = fp.plan(fp.parse(panel(2, 1, location=3)), 1)
|
||||
self.assertEqual(moves, [("2", 1, ["2"])])
|
||||
|
||||
|
||||
class Run(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.dir = tempfile.TemporaryDirectory()
|
||||
self.path = os.path.join(self.dir.name, "plasma-org.kde.plasma.desktop-appletsrc")
|
||||
with open(self.path, "w") as f:
|
||||
f.write(ISSUE_18)
|
||||
|
||||
def tearDown(self):
|
||||
self.dir.cleanup()
|
||||
|
||||
def run_script(self, *args):
|
||||
return subprocess.run([sys.executable, SCRIPT, "--file", self.path, "--screens", "3", *args],
|
||||
capture_output=True, text=True)
|
||||
|
||||
def test_check_reports_and_changes_nothing(self):
|
||||
r = self.run_script("--check")
|
||||
self.assertEqual(r.returncode, 1)
|
||||
self.assertIn("panel 10: screen 8, bottom (lost", r.stdout)
|
||||
with open(self.path) as f:
|
||||
self.assertEqual(f.read(), ISSUE_18)
|
||||
|
||||
def test_repair(self):
|
||||
r = self.run_script()
|
||||
self.assertEqual(r.returncode, 0, r.stderr)
|
||||
self.assertIn("moved it to the first screen", r.stderr)
|
||||
with open(self.path) as f:
|
||||
groups = fp.parse(f.read())
|
||||
self.assertEqual(groups[("Containments", "10")]["lastScreen"], "0")
|
||||
self.assertEqual(groups[("Containments", "11")]["lastScreen"], "0")
|
||||
self.assertEqual(groups[("Containments", "10", "Applets", "101")]["plugin"], "org.kde.plasma.kickoff")
|
||||
self.assertEqual(groups[("Containments", "25")]["lastScreen"], "8") # a spare's desktop
|
||||
with open(self.path + ".ft-bak") as f:
|
||||
self.assertEqual(f.read(), ISSUE_18)
|
||||
# A second run finds nothing to do and leaves the backup alone.
|
||||
os.remove(self.path + ".ft-bak")
|
||||
r = self.run_script()
|
||||
self.assertEqual((r.returncode, r.stderr), (0, ""))
|
||||
self.assertFalse(os.path.exists(self.path + ".ft-bak"))
|
||||
self.assertEqual(self.run_script("--check").returncode, 0)
|
||||
|
||||
def test_no_config_yet(self):
|
||||
os.remove(self.path)
|
||||
self.assertEqual(self.run_script().returncode, 0)
|
||||
self.assertEqual(self.run_script("--check").returncode, 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+105
-15
@@ -11,6 +11,16 @@
|
||||
# screen placed lower or further right. Instead the VNC screen is the primary's size, and
|
||||
# the workspace-sized RDP window inside it is shifted so the primary fills it. The pointer
|
||||
# maps 1:1. When the layout changes, the VNC screen resizes and the RDP client reconnects.
|
||||
#
|
||||
# The RDP client runs only while someone watches. While connected, krdp captures and
|
||||
# H.264-encodes every redraw in software, about 60% of a core, with FreeRDP and Xvnc adding
|
||||
# about 30% more, even with no VNC viewer. krdp starts its capture per RDP connection and
|
||||
# stops it when the connection closes, so it idles without one. So FreeRDP starts when a
|
||||
# VNC client connects (the screen is black for the few seconds that takes) and stops
|
||||
# VNC_IDLE_SEC (45) seconds after the last one leaves. Xvnc has no hook for clients, so ss
|
||||
# counts them: whenever Xvnc writes to its log (it logs each connection), every second
|
||||
# while FreeRDP runs, and every 5 seconds otherwise. The log only wakes this script up;
|
||||
# what it says doesn't matter.
|
||||
set -eu
|
||||
|
||||
here=$(dirname "$(readlink -f "$0")")
|
||||
@@ -57,16 +67,51 @@ stop_rdp() { pkill -f "[x]freerdp /v:127.0.0.1:$rdp_port " 2>/dev/null || true;
|
||||
trap 'stop_rdp; pkill -f "[X]vnc $display " 2>/dev/null || true' EXIT
|
||||
|
||||
box bash -c 'vncpasswd -f < "$1/vnc-password" > "$1/vnc-passwd.bin" && chmod 600 "$1/vnc-passwd.bin"' - "$creds"
|
||||
box Xvnc "$display" -geometry "${w}x${h}" -depth 24 \
|
||||
exec {xlog}< <(box Xvnc "$display" -geometry "${w}x${h}" -depth 24 \
|
||||
-interface "$addr" -rfbport "$vnc_port" \
|
||||
-SecurityTypes VncAuth -PasswordFile "$creds/vnc-passwd.bin" \
|
||||
-AlwaysShared -desktop "Steam Frame (Frametop)" &
|
||||
-AlwaysShared -desktop "Steam Frame (Frametop)" 2>&1)
|
||||
xvnc=$!
|
||||
sleep 2
|
||||
|
||||
# Keep an RDP connection open inside the VNC screen. Reconnect if it drops or the layout changes.
|
||||
# Wait up to $1 seconds, less if Xvnc logs something; its lines go on to this log.
|
||||
nap() {
|
||||
local line rc=0
|
||||
IFS= read -rt "$1" -u "$xlog" line || rc=$?
|
||||
if [ $rc -eq 0 ]; then
|
||||
printf '%s\n' "$line"
|
||||
while IFS= read -rt 0.1 -u "$xlog" line; do printf '%s\n' "$line"; done
|
||||
elif [ $rc -le 128 ]; then
|
||||
sleep 1 # Xvnc's output closed: it's exiting
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
nap 2
|
||||
|
||||
# A connected VNC client: an established TCP connection to Xvnc's port. Any connection
|
||||
# counts, authenticated or not; it's on the tailnet only.
|
||||
clients() { [ -n "$(ss -Htn state established "( sport = :$vnc_port )" 2>/dev/null)" ]; }
|
||||
|
||||
# ft-screens drops screens you aren't looking at to a low frame rate, and krdp would
|
||||
# stream that. "watch SECONDS" asks it for full rate on every screen for that long: sent
|
||||
# when a client connects and renewed every few seconds while one stays, so it lapses by
|
||||
# itself if this script dies. An older ft-screens just answers that it doesn't know it.
|
||||
watch() {
|
||||
if command -v socat >/dev/null; then
|
||||
printf 'watch 15' | socat -u - ABSTRACT-SENDTO:ft_screens 2>/dev/null
|
||||
else
|
||||
python3 -c 'import socket; socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM).sendto(b"watch 15", "\0ft_screens")' 2>/dev/null
|
||||
fi || true
|
||||
}
|
||||
|
||||
# What remote-view depends on: KWin's saved outputs (positions, scales, primary) and
|
||||
# Frametop's layout (screen sizes). It's read again only after one of these changes, and
|
||||
# once a minute in case a change didn't touch them, and only while FreeRDP runs.
|
||||
layout_files=("$HOME/.config/frametop/kwinoutputconfig.json" "$HOME/.config/frametop-layout.json")
|
||||
stamp() { stat -c %y "${layout_files[@]}" 2>/dev/null || true; }
|
||||
|
||||
# Start FreeRDP inside the VNC screen, sized and shifted for $v.
|
||||
# /cert:ignore is fine here: the connection never leaves this host.
|
||||
while kill -0 $xvnc 2>/dev/null; do
|
||||
start_rdp() {
|
||||
read -r x y w h ww wh <<< "$v"
|
||||
box env DISPLAY=$display bash -c '
|
||||
size=$1 x=$2 y=$3 ww=$4 wh=$5 creds=$6 rdp_port=$7
|
||||
@@ -80,6 +125,7 @@ while kill -0 $xvnc 2>/dev/null; do
|
||||
rdp=$!
|
||||
# FreeRDP takes no negative position, so move its window once it is up.
|
||||
for _ in $(seq 60); do
|
||||
kill -0 $rdp 2>/dev/null || break
|
||||
win=$(xdotool search --class xfreerdp 2>/dev/null | tail -1)
|
||||
[ -n "$win" ] && break
|
||||
sleep 0.5
|
||||
@@ -88,14 +134,58 @@ while kill -0 $xvnc 2>/dev/null; do
|
||||
wait $rdp
|
||||
' vnc-rdp "${w}x$h" "$x" "$y" "$ww" "$wh" "$creds" "$rdp_port" || true &
|
||||
rdp=$!
|
||||
while kill -0 $rdp 2>/dev/null; do
|
||||
sleep 5
|
||||
now=$(view) || continue
|
||||
[ -n "$now" ] && [ "$now" != "$v" ] || continue
|
||||
echo "layout changed: $v -> $now"
|
||||
v=$now
|
||||
stop_rdp
|
||||
done
|
||||
wait $rdp 2>/dev/null || true
|
||||
sleep 2
|
||||
}
|
||||
|
||||
idle_sec=${VNC_IDLE_SEC:-45}
|
||||
rdp= # FreeRDP's job while it runs
|
||||
seen=0 # when a client was last seen ($SECONDS)
|
||||
watched=-99 # when "watch" was last sent
|
||||
stamp_v= # stamp() when $v was read
|
||||
recheck=0 # read the layout every 2 seconds until then
|
||||
next_view=0 # next time to read it
|
||||
while kill -0 $xvnc 2>/dev/null; do
|
||||
if clients; then
|
||||
if [ $((SECONDS - watched)) -ge 5 ]; then watch; watched=$SECONDS; fi
|
||||
seen=$SECONDS
|
||||
if [ -z "$rdp" ]; then
|
||||
echo "VNC client connected, starting the RDP client"
|
||||
now=$(view) && [ -n "$now" ] && v=$now
|
||||
stamp_v=$(stamp) recheck=0 next_view=$((SECONDS + 60))
|
||||
start_rdp
|
||||
fi
|
||||
elif [ "$seen" -ne 0 ]; then
|
||||
watched=-99
|
||||
if [ $((SECONDS - seen)) -ge "$idle_sec" ]; then
|
||||
seen=0
|
||||
if [ -n "$rdp" ]; then
|
||||
echo "no VNC client for ${idle_sec}s, stopping the RDP client"
|
||||
stop_rdp
|
||||
wait "$rdp" 2>/dev/null || true
|
||||
rdp=
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
if [ -n "$rdp" ] && ! kill -0 "$rdp" 2>/dev/null; then
|
||||
# It dropped, or the layout changed: reconnect next round if a client is still there.
|
||||
wait "$rdp" 2>/dev/null || true
|
||||
rdp=
|
||||
nap 2
|
||||
continue
|
||||
fi
|
||||
if [ -n "$rdp" ]; then
|
||||
# A layout change shows up in KWin's outputs a few seconds after the files change.
|
||||
s=$(stamp)
|
||||
[ "$s" = "$stamp_v" ] || { stamp_v=$s; recheck=$((SECONDS + 10)) next_view=$SECONDS; }
|
||||
if [ "$SECONDS" -ge "$next_view" ]; then
|
||||
next_view=$((SECONDS + 60))
|
||||
[ "$SECONDS" -ge "$recheck" ] || next_view=$((SECONDS + 2))
|
||||
now=$(view) || now=
|
||||
if [ -n "$now" ] && [ "$now" != "$v" ]; then
|
||||
echo "layout changed: $v -> $now"
|
||||
v=$now
|
||||
stop_rdp
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
if [ -n "$rdp" ] || [ "$seen" -ne 0 ]; then nap 1; else nap 5; fi
|
||||
done
|
||||
Reference in new issue
Block a user