From d07e4bfc21cf7486cb7af01bc104717049168073 Mon Sep 17 00:00:00 2001 From: brkosebay Date: Sun, 8 Nov 2026 16:00:00 +0100 Subject: [PATCH 1/2] Keep rotation layers mapped and touch on the panel Assisted-by: LLM --- desktop/configuration/rotate-output | 14 ++++-- desktop/shell/bin/omarchy-rotate | 24 ++++++++++ desktop/shell/bin/omarchy-rotation-lock | 12 +++++ .../shell/default/omarchy/omarchy-menu.jsonc | 3 ++ desktop/shell/labwc-rc.xml | 1 + desktop/shell/shell/Ui/ScreenMoveRemap.qml | 44 +++---------------- release/public_root.py | 14 +++++- tests/test_rotate_output.py | 18 ++++++++ tests/test_tablet_port.py | 40 +++++++++++++++++ 9 files changed, 127 insertions(+), 43 deletions(-) create mode 100755 desktop/shell/bin/omarchy-rotate create mode 100755 desktop/shell/bin/omarchy-rotation-lock create mode 100644 tests/test_tablet_port.py diff --git a/desktop/configuration/rotate-output b/desktop/configuration/rotate-output index 82083d2..39661ac 100644 --- a/desktop/configuration/rotate-output +++ b/desktop/configuration/rotate-output @@ -23,7 +23,7 @@ transform() { apply() { local t output - [[ -e $lock ]] && return + [[ -e $lock ]] && return 1 t=$(transform "$1") || return output=$(wlr-randr | awk '/^[^[:space:]]/ {print $1; exit}') [[ -n $output ]] && wlr-randr --output "$output" --transform "$t" @@ -36,7 +36,7 @@ orientation() { esac } -exec {sensor}< <(exec monitor-sensor --accel) +exec {sensor}< <(exec stdbuf -oL monitor-sensor --accel) sensor_pid=$! watcher='' if [[ -n ${LABWC_PID:-} ]]; then @@ -55,8 +55,14 @@ while :; do continue fi ((status > 128)) || break # end of input, not a timeout - [[ $pending != "$applied" ]] && apply "$pending" && applied=$pending - pending='' + [[ -e $lock ]] && applied='' + if [[ $pending != "$applied" ]]; then + # Keep the latest settled pose while locked. Retrying the read timeout + # reapplies it after unlock even if the sensor sends no fresh event. + apply "$pending" || continue + applied=$pending + fi + # Keep the last pose to restore auto orientation after a manual lock. else IFS= read -r -u "$sensor" line || break next=$(orientation "$line") && [[ -n $next ]] && pending=$next diff --git a/desktop/shell/bin/omarchy-rotate b/desktop/shell/bin/omarchy-rotate new file mode 100755 index 0000000..512d377 --- /dev/null +++ b/desktop/shell/bin/omarchy-rotate @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +"""Manual rotation uses the same output protocol as auto-rotation; holds auto until unlock.""" +import json, os, subprocess, sys +from pathlib import Path +mode = sys.argv[1] if len(sys.argv)>1 else 'status' +outputs = json.loads(subprocess.check_output(['wlr-randr', '--json'], text=True)) +panel = next((o for o in outputs if o['name'] == 'eDP-1'), None) +if panel is None: + panel = next(o for o in outputs if o.get('enabled')) +current = panel['transform'] +turns = ['normal', '90', '180', '270'] +if mode == 'status': + print(current); raise SystemExit(0) +if mode == 'next': mode = turns[(turns.index(current)+1)%4] +mode = {'portrait':'normal','landscape':'90','portrait-flipped':'180','landscape-flipped':'270'}.get(mode,mode) +if mode not in turns: raise SystemExit('Usage: omarchy-rotate status|next|normal|90|180|270') +lock = Path(os.environ.get('XDG_CONFIG_HOME', str(Path.home()/'.config')))/'omarchy/rotation-lock' +was_locked = lock.exists() +lock.parent.mkdir(parents=True,exist_ok=True); lock.touch() +try: + subprocess.run(['wlr-randr','--output',panel['name'],'--transform',mode],check=True,timeout=5) +except (OSError, subprocess.SubprocessError): + if not was_locked: lock.unlink(missing_ok=True) + raise diff --git a/desktop/shell/bin/omarchy-rotation-lock b/desktop/shell/bin/omarchy-rotation-lock new file mode 100755 index 0000000..744dc37 --- /dev/null +++ b/desktop/shell/bin/omarchy-rotation-lock @@ -0,0 +1,12 @@ +#!/bin/bash +# Share the orientation lock with arch-ipad-rotate-output. +set -euo pipefail +lock=${XDG_CONFIG_HOME:-$HOME/.config}/omarchy/rotation-lock +case ${1:-toggle} in + enabled) [[ -e $lock ]]; exit ;; + status) if [[ -e $lock ]]; then echo locked; else echo automatic; fi ;; + on) mkdir -p "$(dirname "$lock")"; touch "$lock" ;; + off) rm -f -- "$lock" ;; + toggle) if [[ -e $lock ]]; then rm -f -- "$lock"; else mkdir -p "$(dirname "$lock")"; touch "$lock"; fi ;; + *) echo 'Usage: omarchy-rotation-lock [on|off|toggle|enabled|status]' >&2; exit 2 ;; +esac diff --git a/desktop/shell/default/omarchy/omarchy-menu.jsonc b/desktop/shell/default/omarchy/omarchy-menu.jsonc index b337560..8cb5c0d 100644 --- a/desktop/shell/default/omarchy/omarchy-menu.jsonc +++ b/desktop/shell/default/omarchy/omarchy-menu.jsonc @@ -181,6 +181,8 @@ "trigger.tests.disk-speedtest": {"icon":"󰋊","label":"Disk Speed Test","action":"omarchy-shell shell summon omarchy.disk-speedtest"}, // Setup + "setup.tablet": {"icon":"󰦧","label":"Tablet"}, + "setup.tablet.rotation-lock": {"icon":"󰿆","label":"Rotation Lock","checked":"omarchy-rotation-lock enabled","action":"omarchy-rotation-lock toggle"}, "setup.monitors": {"icon":"󰍹","label":"Monitors","action":"omarchy-launch-config-editor \"$HOME/.config/hypr/monitors.lua\""}, "setup.keybindings": {"icon":"","label":"Keybindings","when":"[[ -f ~/.config/hypr/bindings.lua ]]","action":"omarchy-launch-config-editor \"$HOME/.config/hypr/bindings.lua\""}, "setup.input": {"icon":"","label":"Input","when":"[[ -f ~/.config/hypr/input.lua ]]","action":"omarchy-launch-config-editor \"$HOME/.config/hypr/input.lua\""}, @@ -450,4 +452,5 @@ "update.hardware.trackpad": {"icon":"󰟸","label":"Trackpad","action":"omarchy-launch-floating-terminal-with-presentation omarchy-restart-trackpad"}, "update.password.drive": {"icon":"","label":"Drive Encryption","action":"omarchy-launch-floating-terminal-with-presentation omarchy-drive-password"}, "update.password.user": {"icon":"","label":"User","action":"omarchy-launch-floating-terminal-with-presentation passwd"}, + "setup.tablet.rotate": {"icon": "󰑢", "label": "Rotate Once and Lock", "action": "omarchy-rotate next"}, } diff --git a/desktop/shell/labwc-rc.xml b/desktop/shell/labwc-rc.xml index 6fea059..7ec8313 100644 --- a/desktop/shell/labwc-rc.xml +++ b/desktop/shell/labwc-rc.xml @@ -117,6 +117,7 @@ + no diff --git a/desktop/shell/shell/Ui/ScreenMoveRemap.qml b/desktop/shell/shell/Ui/ScreenMoveRemap.qml index 44d2308..7c9d24d 100644 --- a/desktop/shell/shell/Ui/ScreenMoveRemap.qml +++ b/desktop/shell/shell/Ui/ScreenMoveRemap.qml @@ -1,43 +1,13 @@ import QtQuick -// Hyprland leaves an already-mapped layer surface at its old global position -// when its monitor moves within the layout: undocking disables the internal -// panel, the external monitor shifts to x=0, and long-lived surfaces such as -// the bar and background keep rendering at the old offset — or entirely -// off-screen — until they are unmapped and remapped. Watch the screen's -// origin and pulse `remapping` when it moves; the owning window folds that -// into its `visible` binding so the compositor re-places the surface at the -// monitor's new origin. +// Labwc repositions mapped layer-shell surfaces itself when outputs move or +// resize. Qt emits x/y notifications for a size-only change too. The old +// Hyprland workaround hid every panel and wallpaper 200ms later, destroying +// their layer surfaces and buffers for at least another 50ms. On a software +// renderer recreation/decoding extends that gap. Keep the public component +// interface, but never unmap a labwc layer just because its output changed. Item { - id: root - required property var window - readonly property var screen: window ? window.screen : null - - // Fold into the window's binding: visible: && !guard.remapping - property bool remapping: false - + readonly property bool remapping: false visible: false - - // A layout reshuffle can move the monitor more than once before it lands. - // Let the positions settle before the single remap pulse. - Timer { - id: settleTimer - interval: 200 - onTriggered: root.remapping = true - } - - // Hold the surface unmapped for a beat so the compositor processes the - // unmap before the remap instead of coalescing them into a no-op. - Timer { - interval: 50 - running: root.remapping - onTriggered: root.remapping = false - } - - Connections { - target: root.screen - function onXChanged() { settleTimer.restart() } - function onYChanged() { settleTimer.restart() } - } } diff --git a/release/public_root.py b/release/public_root.py index 2df5ef0..76e3f6c 100644 --- a/release/public_root.py +++ b/release/public_root.py @@ -39,6 +39,10 @@ def desktop_replacements(): 'bin/omarchy-bluetooth-device')} replacements['home/tablet/.config/labwc/rc.xml'] = (PROJECT / 'desktop/shell/labwc-rc.xml').read_bytes() replacements['home/tablet/.config/labwc/autostart'] = (PROJECT / 'desktop/configuration/labwc-autostart').read_bytes() + replacements[prefix + 'shell/Ui/ScreenMoveRemap.qml'] = (PROJECT / 'desktop/shell/shell/Ui/ScreenMoveRemap.qml').read_bytes() + menu = (PROJECT / 'desktop/shell/default/omarchy/omarchy-menu.jsonc').read_text() + replacements[prefix + 'default/omarchy/omarchy-menu.jsonc'] = menu.encode() + replacements['usr/local/libexec/arch-ipad-rotate-output'] = (PROJECT / 'desktop/configuration/rotate-output').read_bytes() return replacements @@ -121,8 +125,14 @@ def labwc_files(directory, architecture='aarch64'): def desktop_additions(): - name = 'home/tablet/.local/share/omarchy-ipad/shell/bluetooth-pairing/shell.qml' - return {name: ((PROJECT / 'desktop/shell/shell/bluetooth-pairing/shell.qml').read_bytes(), 0o644)} + prefix = 'home/tablet/.local/share/omarchy-ipad/' + files = {prefix + 'shell/bluetooth-pairing/shell.qml': ((PROJECT / 'desktop/shell/shell/bluetooth-pairing/shell.qml').read_bytes(), 0o644)} + files['usr/local/libexec/arch-ipad-touch-output'] = ((PROJECT / 'desktop/configuration/touch-output').read_bytes(), 0o755) + files[prefix + 'bin/omarchy-rotation-lock'] = ((PROJECT / 'desktop/shell/bin/omarchy-rotation-lock').read_bytes(), 0o755) + files['usr/local/bin/omarchy-rotation-lock'] = files[prefix + 'bin/omarchy-rotation-lock'] + files[prefix + 'bin/omarchy-rotate'] = ((PROJECT / 'desktop/shell/bin/omarchy-rotate').read_bytes(), 0o755) + files['usr/local/bin/omarchy-rotate'] = files[prefix + 'bin/omarchy-rotate'] + return files def system_files(): diff --git a/tests/test_rotate_output.py b/tests/test_rotate_output.py index d97c256..bdd3489 100644 --- a/tests/test_rotate_output.py +++ b/tests/test_rotate_output.py @@ -87,6 +87,24 @@ def test_lock_holds_the_orientation(self): self.assertEqual(self.run_script(events, locked=True), []) + def test_unlock_applies_last_settled_pose_without_another_sensor_event(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp); bindir = root/'bin'; bindir.mkdir() + lock = root/'config/omarchy/rotation-lock'; lock.parent.mkdir(parents=True); lock.touch() + log = root/'randr.log' + for name, body in (('wlr-randr', RANDR), ('monitor-sensor', + monitor([(0, '=== Has accelerometer (orientation: left-up)'), (0.8, 'waiting')]))): + f=bindir/name; f.write_text(body); f.chmod(0o755) + env={**os.environ,'PATH':str(bindir)+':'+os.environ['PATH'], + 'XDG_CONFIG_HOME':str(root/'config'),'RANDR_LOG':str(log),'ROTATE_SETTLE':'0.1'} + proc=subprocess.Popen([BASH,str(SCRIPT)],env=env) + try: + threading.Event().wait(0.35); self.assertFalse(log.exists()); lock.unlink() + proc.wait(timeout=3) + self.assertEqual(log.read_text().splitlines(),['--output Unknown-1 --transform 90']) + finally: + if proc.poll() is None: proc.kill(); proc.wait() + def test_ends_with_the_compositor(self): # monitor-sensor never ends by itself; the helper must stop it once # the compositor is gone instead of outliving the session diff --git a/tests/test_tablet_port.py b/tests/test_tablet_port.py new file mode 100644 index 0000000..269e16c --- /dev/null +++ b/tests/test_tablet_port.py @@ -0,0 +1,40 @@ +import importlib.machinery +import importlib.util +import json +import os +from pathlib import Path +import struct +import subprocess +import unittest +from unittest.mock import patch +import xml.etree.ElementTree as ET + +PROJECT = Path(__file__).resolve().parents[1] + +def load(name, path): + loader=importlib.machinery.SourceFileLoader(name,str(PROJECT/path)) + spec=importlib.util.spec_from_loader(name,loader) + mod=importlib.util.module_from_spec(spec); loader.exec_module(mod); return mod + +p=load('power','system/overlay/usr/libexec/omarchy-powerd') + +class TabletPort(unittest.TestCase): + + + + + + + + def test_no_false_client_remap_and_native_touch(self): + source=(PROJECT/'desktop/shell/shell/Ui/ScreenMoveRemap.qml').read_text() + self.assertIn('readonly property bool remapping: false',source) + self.assertNotIn('Timer {',source) + tree=ET.fromstring((PROJECT/'desktop/shell/labwc-rc.xml').read_text()) + touch=next(t for t in tree.findall('touch') if t.get('deviceName')=='iPad6,11 Touchscreen') + self.assertEqual(touch.get('mapToOutput'),'eDP-1') + self.assertEqual(touch.get('mouseEmulation'),'no') + + + +if __name__=='__main__': unittest.main() From 4724b8157938c1379d081649f1f92040982bf9ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= Date: Thu, 12 Nov 2026 09:00:00 +0100 Subject: [PATCH 2/2] Rotation: manual rotation and rotation lock next to automatic rotation Automatic rotation could not be overridden: there was no manual rotation or lock in the menu. Add omarchy-rotate with rotate and lock commands, menu entries for them, and the release root wiring. Assisted-by: LLM --- desktop/shell/bin/omarchy-rotate | 24 +++++++++++++++++++ .../shell/default/omarchy/omarchy-menu.jsonc | 2 ++ release/public_root.py | 13 ++++++---- 3 files changed, 35 insertions(+), 4 deletions(-) create mode 100755 desktop/shell/bin/omarchy-rotate diff --git a/desktop/shell/bin/omarchy-rotate b/desktop/shell/bin/omarchy-rotate new file mode 100755 index 0000000..512d377 --- /dev/null +++ b/desktop/shell/bin/omarchy-rotate @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +"""Manual rotation uses the same output protocol as auto-rotation; holds auto until unlock.""" +import json, os, subprocess, sys +from pathlib import Path +mode = sys.argv[1] if len(sys.argv)>1 else 'status' +outputs = json.loads(subprocess.check_output(['wlr-randr', '--json'], text=True)) +panel = next((o for o in outputs if o['name'] == 'eDP-1'), None) +if panel is None: + panel = next(o for o in outputs if o.get('enabled')) +current = panel['transform'] +turns = ['normal', '90', '180', '270'] +if mode == 'status': + print(current); raise SystemExit(0) +if mode == 'next': mode = turns[(turns.index(current)+1)%4] +mode = {'portrait':'normal','landscape':'90','portrait-flipped':'180','landscape-flipped':'270'}.get(mode,mode) +if mode not in turns: raise SystemExit('Usage: omarchy-rotate status|next|normal|90|180|270') +lock = Path(os.environ.get('XDG_CONFIG_HOME', str(Path.home()/'.config')))/'omarchy/rotation-lock' +was_locked = lock.exists() +lock.parent.mkdir(parents=True,exist_ok=True); lock.touch() +try: + subprocess.run(['wlr-randr','--output',panel['name'],'--transform',mode],check=True,timeout=5) +except (OSError, subprocess.SubprocessError): + if not was_locked: lock.unlink(missing_ok=True) + raise diff --git a/desktop/shell/default/omarchy/omarchy-menu.jsonc b/desktop/shell/default/omarchy/omarchy-menu.jsonc index de7b289..28043c5 100644 --- a/desktop/shell/default/omarchy/omarchy-menu.jsonc +++ b/desktop/shell/default/omarchy/omarchy-menu.jsonc @@ -184,6 +184,8 @@ "trigger.tests.disk-speedtest": {"icon":"󰋊","label":"Disk Speed Test","action":"omarchy-shell shell summon omarchy.disk-speedtest"}, // Setup + "setup.tablet": {"icon":"󰦧","label":"Tablet"}, + "setup.tablet.rotate": {"icon":"󰑢","label":"Rotate Once and Lock","action":"omarchy-rotate next"}, "setup.monitors": {"icon":"󰍹","label":"Monitors","action":"omarchy-launch-config-editor \"$HOME/.config/hypr/monitors.lua\""}, "setup.keybindings": {"icon":"","label":"Keybindings","when":"[[ -f ~/.config/hypr/bindings.lua ]]","action":"omarchy-launch-config-editor \"$HOME/.config/hypr/bindings.lua\""}, "setup.input": {"icon":"","label":"Input","when":"[[ -f ~/.config/hypr/input.lua ]]","action":"omarchy-launch-config-editor \"$HOME/.config/hypr/input.lua\""}, diff --git a/release/public_root.py b/release/public_root.py index 63a2d35..45a909c 100644 --- a/release/public_root.py +++ b/release/public_root.py @@ -42,6 +42,7 @@ def desktop_replacements(): replacements['home/tablet/.config/labwc/rc.xml'] = (PROJECT / 'desktop/shell/labwc-rc.xml').read_bytes() replacements['home/tablet/.config/labwc/autostart'] = (PROJECT / 'desktop/configuration/labwc-autostart').read_bytes() replacements['usr/local/libexec/arch-ipad-rotate-output'] = (PROJECT / 'desktop/configuration/rotate-output').read_bytes() + replacements[prefix + 'default/omarchy/omarchy-menu.jsonc'] = (PROJECT / 'desktop/shell/default/omarchy/omarchy-menu.jsonc').read_bytes() return replacements @@ -129,10 +130,14 @@ def labwc_files(directory, architecture='aarch64'): def desktop_additions(): - name = 'home/tablet/.local/share/omarchy-ipad/shell/bluetooth-pairing/shell.qml' - return {name: ((PROJECT / 'desktop/shell/shell/bluetooth-pairing/shell.qml').read_bytes(), 0o644), - 'home/tablet/.local/share/omarchy-ipad/bin/omarchy-rotation-lock': - ((PROJECT / 'desktop/shell/bin/omarchy-rotation-lock').read_bytes(), 0o755)} + prefix = 'home/tablet/.local/share/omarchy-ipad/' + return { + 'usr/local/bin/omarchy-rotate': ((PROJECT / 'desktop/shell/bin/omarchy-rotate').read_bytes(), 0o755), + 'home/tablet/.local/share/omarchy-ipad/bin/omarchy-rotate': ((PROJECT / 'desktop/shell/bin/omarchy-rotate').read_bytes(), 0o755), + 'usr/local/bin/omarchy-rotation-lock': ((PROJECT / 'desktop/shell/bin/omarchy-rotation-lock').read_bytes(), 0o755), + 'home/tablet/.local/share/omarchy-ipad/bin/omarchy-rotation-lock': ((PROJECT / 'desktop/shell/bin/omarchy-rotation-lock').read_bytes(), 0o755), + 'home/tablet/.local/share/omarchy-ipad/shell/bluetooth-pairing/shell.qml': ((PROJECT / 'desktop/shell/shell/bluetooth-pairing/shell.qml').read_bytes(), 0o644), + } def system_files():