mirror of
https://github.com/DeeJanuz/frametop.git
synced 2026-10-06 03:00:06 +02:00
Add named layouts and a head pin for screens
Named layouts: Save current arrangement in Frametop Display Settings now asks for a name, and saved layouts are listed with the presets under Arrangement, with rename and delete next to the list. A named layout is the custom arrangement under a name: each screen's place relative to your head, width, curve, and pin, but not resolution or scale. Using one copies it into the custom arrangement, so desktop start, Meta+Shift+R, and Arrange now apply it unchanged; "active" remembers the name, and a plain capture clears it. ft-layout gains save, use, layouts, rename, and delete. A layout saved with fewer screens than there are now leaves the others where they were saved last, or where the preset puts them. Head pin: ft-screens' pin command takes "head" as well as left and right, and pins the screen to the headset (device 0) where it is, like a HUD. A head-pinned screen skips the wrist facing rule and shows whenever the screens do. Carrying it re-pins it to the head on release, like a wrist pin, so it can be adjusted in VR. The Visibility tab (now Visibility & pins) sets each screen's pin: in the room, either wrist, or your head, and the pin command now rejects anything but left, right, or head (it used to take anything else as left). This removes the "no HUD" limit from the docs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f01c84f9a3
commit
0be802c7e1
7 files changed
+399
-62
No files matched your search
+117
-3
@@ -15,6 +15,9 @@ you face (yaw only), like a recenter. It lives in ~/.config/frametop-layout.json
|
||||
"mode": "preset" | "custom",
|
||||
"preset": {"kind": "arc" | "flat", "rows": 1, "distance": 2.0, "gap": 0.05, "height": 0},
|
||||
"primary": 2, the screen with the taskbar (1-based; default: the biggest)
|
||||
"layouts": {"Work": [{"pos": ..., "face": ..., "roll": ..., "metres": ..., "curve": ...,
|
||||
"pin": ...}, ...]}, named layouts: each screen's place (SPATIAL)
|
||||
"active": "Work", the named layout the custom arrangement came from
|
||||
"visibility": {"mode": "always", ft-screens: always | dashboard (only with the SteamVR
|
||||
"wrist_angle": 60, dashboard open) | gesture (while you look at a controller)
|
||||
"gesture_hand": "left", "gesture_angle": 20}, | toggle (hidden until shown);
|
||||
@@ -23,6 +26,7 @@ you face (yaw only), like a recenter. It lives in ~/.config/frametop-layout.json
|
||||
"screens": [{"size": [w, h], "metres": 3.6, ft-screens: pixels, and width in VR
|
||||
"curve": 0, ft-screens: cylinder radius in metres, 0 = flat
|
||||
"pin": {"hand": "left", "rel": [12]}, ft-screens: riding on that controller
|
||||
(left | right), or on the headset (head)
|
||||
"scale": 1.0, KWin output scale (1.0 = 100%)
|
||||
"pos": [x, y, z], "face": [yaw, pitch], "roll": 0, custom layout
|
||||
"rotation": "normal" | "left" | "right"}, ...], gamescope only
|
||||
@@ -38,12 +42,17 @@ Usage (on the Frame host; Frametop Display Settings calls it too):
|
||||
ft-layout apply [--wait SECONDS] arrange every screen; --wait is for desktop start:
|
||||
wait for the screens, skip if "auto" is off
|
||||
ft-layout capture save the current arrangement as the custom layout
|
||||
ft-layout save NAME save it as a named layout too, and use that
|
||||
ft-layout use NAME switch to a named layout and arrange the screens in it
|
||||
ft-layout layouts list the named layouts (* = the one in use)
|
||||
ft-layout rename OLD NEW | delete NAME
|
||||
ft-layout plan print the arrangement as JSON (no VR needed)
|
||||
ft-layout scale per-screen scale, positions (as the screens are around
|
||||
you), and primary to KWin
|
||||
ft-layout screen-args ft-screens' --screen arguments for the session script
|
||||
ft-layout toggle hide or show all screens (ft-screens)
|
||||
ft-layout pin all|N left|right pin screens to a wrist as they are; unpin all|N
|
||||
ft-layout pin all|N left|right|head pin screens to a wrist or your head as they are;
|
||||
unpin all|N
|
||||
"""
|
||||
import fcntl
|
||||
import json
|
||||
@@ -68,6 +77,7 @@ PIXELS_PER_METRE = 800 # ft-screens: a new screen's default size in VR (
|
||||
# or leaves them up (visible).
|
||||
VISIBILITY = {"mode": "always", "wrist_angle": 60, "gesture_hand": "left", "gesture_angle": 20,
|
||||
"controllers": "outside_games", "in_games": "hide"}
|
||||
SPATIAL = ("pos", "face", "roll", "metres", "curve", "pin") # what a named layout keeps of a screen
|
||||
DEFAULTS = {"auto": True, "mode": "preset",
|
||||
"preset": {"kind": "arc", "rows": 1, "distance": 2.0, "gap": 0.05, "height": 0.0},
|
||||
"screens": [], "panel_size": list(DEFAULT_PANEL)}
|
||||
@@ -574,7 +584,82 @@ def apply(wait=0):
|
||||
|
||||
|
||||
def capture():
|
||||
return capture_screens() if backend() == "screens" else capture_gamescope()
|
||||
"""Where the screens are now as the custom layout. It's no longer a named one's until
|
||||
`save NAME` (placed by hand since)."""
|
||||
screens = capture_screens() if backend() == "screens" else capture_gamescope()
|
||||
layout = load_layout()
|
||||
if layout.pop("active", None) is not None:
|
||||
save_layout(layout)
|
||||
return screens
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- named layouts
|
||||
|
||||
def layout_names(layout):
|
||||
return sorted(layout.get("layouts", {}), key=str.casefold)
|
||||
|
||||
|
||||
def check_name(name):
|
||||
name = " ".join(name.split())
|
||||
if not name or len(name) > 40:
|
||||
raise RuntimeError("a layout's name needs 1 to 40 characters")
|
||||
return name
|
||||
|
||||
|
||||
def save_named(layout, name):
|
||||
"""The custom arrangement (as captured) under `name`, replacing one of that name, and
|
||||
in use."""
|
||||
name = check_name(name)
|
||||
screens = [s for s in layout.get("screens", [])[:screen_count(layout)] if "pos" in s]
|
||||
if not screens:
|
||||
raise RuntimeError("nothing to save: no arrangement captured")
|
||||
layout.setdefault("layouts", {})[name] = [{k: s[k] for k in SPATIAL if k in s} for s in screens]
|
||||
layout["mode"], layout["active"] = "custom", name
|
||||
return name
|
||||
|
||||
|
||||
def use_named(layout, name):
|
||||
"""Make a named layout the custom arrangement (not arranged yet). A layout saved with
|
||||
fewer screens leaves the others where the preset would put them, or where they were
|
||||
saved last; one saved with more keeps its extra screens for later."""
|
||||
saved = layout.get("layouts", {}).get(name)
|
||||
if saved is None:
|
||||
raise RuntimeError(f"no layout called {name!r}")
|
||||
count = screen_count(layout)
|
||||
preset = plan(dict(layout, mode="preset"), count)
|
||||
screens = layout.setdefault("screens", [])
|
||||
while len(screens) < count:
|
||||
screens.append({})
|
||||
for i in range(count):
|
||||
if i < len(saved):
|
||||
for k in SPATIAL:
|
||||
screens[i].pop(k, None)
|
||||
screens[i].update(json.loads(json.dumps(saved[i])))
|
||||
elif "pos" not in screens[i]:
|
||||
screens[i].update({"pos": list(preset[i]["pos"]), "face": list(preset[i]["face"]),
|
||||
"roll": preset[i]["roll"]})
|
||||
layout["mode"], layout["active"] = "custom", name
|
||||
|
||||
|
||||
def rename_named(layout, old, new):
|
||||
new = check_name(new)
|
||||
named = layout.get("layouts", {})
|
||||
if old not in named:
|
||||
raise RuntimeError(f"no layout called {old!r}")
|
||||
if new != old and new in named:
|
||||
raise RuntimeError(f"there's already a layout called {new!r}")
|
||||
named[new] = named.pop(old)
|
||||
if layout.get("active") == old:
|
||||
layout["active"] = new
|
||||
return new
|
||||
|
||||
|
||||
def delete_named(layout, name):
|
||||
"""The screens stay where the layout put them, as an unnamed custom arrangement."""
|
||||
if layout.get("layouts", {}).pop(name, None) is None:
|
||||
raise RuntimeError(f"no layout called {name!r}")
|
||||
if layout.get("active") == name:
|
||||
layout.pop("active")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- KWin (scale, positions, primary)
|
||||
@@ -737,10 +822,21 @@ def main(argv):
|
||||
print(screen_args())
|
||||
elif cmd == "toggle":
|
||||
log(screens_socket().ask("toggle"))
|
||||
elif cmd == "layouts":
|
||||
layout = load_layout()
|
||||
for name in layout_names(layout):
|
||||
print(("* " if name == layout.get("active") and layout.get("mode") == "custom" else " ") + name)
|
||||
elif cmd in ("rename", "delete") and len(argv) == (4 if cmd == "rename" else 3):
|
||||
layout = load_layout()
|
||||
if cmd == "rename":
|
||||
rename_named(layout, argv[2], argv[3])
|
||||
else:
|
||||
delete_named(layout, argv[2])
|
||||
save_layout(layout)
|
||||
elif cmd in ("pin", "unpin") and len(argv) >= 3:
|
||||
log(screens_socket().ask(" ".join(argv[1:])))
|
||||
kwin_follow() # pinned screens go last
|
||||
elif cmd in ("apply", "capture", "scale"):
|
||||
elif cmd in ("apply", "capture", "scale") or (cmd in ("save", "use") and len(argv) == 3):
|
||||
with open(LOCK_PATH, "w") as lock:
|
||||
try:
|
||||
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
@@ -779,6 +875,24 @@ def main(argv):
|
||||
for i, s in enumerate(capture()):
|
||||
log(f"screen {i + 1}: {s}")
|
||||
kwin_follow()
|
||||
elif cmd == "save":
|
||||
check_name(argv[2])
|
||||
capture()
|
||||
layout = load_layout()
|
||||
log(f"saved layout {save_named(layout, argv[2])!r}")
|
||||
save_layout(layout)
|
||||
kwin_follow()
|
||||
elif cmd == "use":
|
||||
layout = load_layout()
|
||||
use_named(layout, argv[2])
|
||||
save_layout(layout)
|
||||
log(f"using layout {argv[2]!r}")
|
||||
try:
|
||||
apply()
|
||||
except RuntimeError as e:
|
||||
log(f"not arranged now: {e}")
|
||||
else:
|
||||
kwin_follow()
|
||||
else:
|
||||
changes = apply_scales()
|
||||
log("kwin: " + (" ".join(changes) if changes else "unchanged"))
|
||||
|
||||
Reference in new issue
Block a user