From 385b900361a4155a831532a1ff254540b0b1381c Mon Sep 17 00:00:00 2001 From: alex clark Date: Thu, 10 Sep 2026 15:09:56 -0400 Subject: [PATCH] Fix Lua configuration and animation lifetime handling --- DEVELOPER.md | 4 +- README.md | 28 +++++++++- src/hyprview.cpp | 131 ++++++++++++++--------------------------------- src/main.cpp | 105 ++++++++++++++++++++----------------- tests/README.md | 21 ++++++++ tests/nested.py | 121 +++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 269 insertions(+), 141 deletions(-) create mode 100644 tests/README.md create mode 100644 tests/nested.py diff --git a/DEVELOPER.md b/DEVELOPER.md index b32deab..44dd363 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -56,7 +56,7 @@ enum class EWindowCollectionMode { - `CViewGesture::begin/update/end()` - Swipe gesture handling ### Global Configuration Values -Located in `main.cpp`, registered via `HyprlandAPI::addConfigValue`: +Located in `main.cpp`, registered via `HyprlandAPI::addConfigValueV2`: - `plugin:hyprview:active_border_color` - `plugin:hyprview:inactive_border_color` - `plugin:hyprview:border_width` @@ -112,7 +112,7 @@ The plugin provides flexible dispatcher commands with various options: ### Gesture Handling - 3-finger swipe gestures handled by `CViewGesture` - Swipe detection uses distance threshold from config -- Gestures blocked when overview is active to prevent conflicts +- Legacy gestures blocked when overview is active to prevent conflicts; Lua callbacks remain available - Swipe gestures support opening and closing the overview - Configurable via `hyprview-gesture` keyword diff --git a/README.md b/README.md index a497f7b..601da18 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ https://github.com/user-attachments/assets/c0553bfe-6357-48e5-a4d0-50068096d800 * **Workspace Indicator:** Each window tile shows its workspace ID (displayed as "wsid:N") in a configurable position with customizable size and styling. The indicator color automatically matches the window's border color (active or inactive) for easy navigation across multiple workspaces. * **Window Selection:** Hover to focus and click to select a window, automatically closing the overview. * **Trackpad Gestures:** Use swipe gestures to open and close the overview. -* **Gesture Conflict Prevention:** Automatically blocks workspace gestures when overview is active to prevent accidental workspace switches. +* **Gesture Conflict Prevention:** Blocks workspace gestures in legacy configurations. Lua configurations retain their configured gesture callbacks while the overview is open. * **Smooth Animations:** Animated transitions when opening/closing the overview. * **Multi-monitor Support:** Provides a separate overview for each monitor with proper scaling and positioning. * **Customizable Appearance:** Change colors, borders, margins, background dimming, and radii. @@ -51,6 +51,32 @@ hyprpm enable hyprview plugin = /full_path_to/hyprview.so ``` +### Lua configuration (Hyprland 0.56) + +Load the plugin before invoking its callbacks. The plugin registers settings with +Hyprland's typed configuration API and exposes `hl.plugin.hyprview.toggle(args)`. +It accepts the same arguments as the `hyprview:toggle` dispatcher and returns +`success, error`. Call it inside a gesture or binding callback, after loading: + +```lua +hl.gesture({ fingers = 3, direction = "up", action = function() + if hl.plugin.hyprview then + hl.plugin.hyprview.toggle("on all special") + end +end }) +hl.gesture({ fingers = 3, direction = "down", action = function() + if hl.plugin.hyprview then + hl.plugin.hyprview.toggle("off") + end +end }) +``` + +An explicit `off` closes an overview opened with `on`; add `monitor:NAME` to +close only that monitor. Lua gestures remain enabled while the overview is open, +so a downward callback can close it. Avoid assigning competing workspace gestures +to the same fingers/direction. Legacy `hyprview-gesture` remains available in +`hyprland.conf`. + ### Keybinds You can bind the overview to a key. The dispatcher accepts optional arguments to control the behavior. diff --git a/src/hyprview.cpp b/src/hyprview.cpp index ffed922..c521b4b 100644 --- a/src/hyprview.cpp +++ b/src/hyprview.cpp @@ -29,10 +29,12 @@ using Desktop::View::CWindow; // Helper to find the CHyprView instance for a given animation variable CHyprView *findInstanceForAnimation( WP thisptr) { + if (!thisptr) + return nullptr; for (auto &[monitor, instance] : g_pHyprViewInstances) { - if (instance && (instance->size.get() == thisptr.lock().get() || - instance->pos.get() == thisptr.lock().get() || - instance->scale.get() == thisptr.lock().get())) { + if (instance && (instance->size.get() == thisptr.get() || + instance->pos.get() == thisptr.get() || + instance->scale.get() == thisptr.get())) { return instance.get(); } } @@ -264,91 +266,39 @@ CHyprView::CHyprView(PHLMONITOR pMonitor_, PHLWORKSPACE startedOn_, bool swipe_, Debug::log(LOG, "[hyprview] CHyprView(): Saved original focused window: {}", (void *)origWindow.get()); - static auto *const *PMARGIN = - (Hyprlang::INT *const *)HyprlandAPI::getConfigValue( - PHANDLE, "plugin:hyprview:margin") - ->getDataStaticPtr(); - - MARGIN = **PMARGIN; - - static auto *const *PACTIVEBORDERCOL = - (Hyprlang::INT *const *)HyprlandAPI::getConfigValue( - PHANDLE, "plugin:hyprview:active_border_color") - ->getDataStaticPtr(); - static auto *const *PINACTIVEBORDERCOL = - (Hyprlang::INT *const *)HyprlandAPI::getConfigValue( - PHANDLE, "plugin:hyprview:inactive_border_color") - ->getDataStaticPtr(); - static auto *const *PBORDERWIDTH = - (Hyprlang::INT *const *)HyprlandAPI::getConfigValue( - PHANDLE, "plugin:hyprview:border_width") - ->getDataStaticPtr(); - static auto *const *PBORDERRADIUS = - (Hyprlang::INT *const *)HyprlandAPI::getConfigValue( - PHANDLE, "plugin:hyprview:border_radius") - ->getDataStaticPtr(); - static auto *const *PBGDIM = - (Hyprlang::FLOAT *const *)HyprlandAPI::getConfigValue( - PHANDLE, "plugin:hyprview:bg_dim") - ->getDataStaticPtr(); - static auto *const *PWORKSPACEINDICATORENABLED = - (Hyprlang::INT *const *)HyprlandAPI::getConfigValue( - PHANDLE, "plugin:hyprview:workspace_indicator_enabled") - ->getDataStaticPtr(); - static auto *const *PWORKSPACEINDICATORFONTSIZE = - (Hyprlang::INT *const *)HyprlandAPI::getConfigValue( - PHANDLE, "plugin:hyprview:workspace_indicator_font_size") - ->getDataStaticPtr(); - static auto PWORKSPACEINDICATORPOSITION_VAL = HyprlandAPI::getConfigValue( - PHANDLE, "plugin:hyprview:workspace_indicator_position"); - static auto *const *PWORKSPACEINDICATORBGOPACITY = - (Hyprlang::FLOAT *const *)HyprlandAPI::getConfigValue( - PHANDLE, "plugin:hyprview:workspace_indicator_bg_opacity") - ->getDataStaticPtr(); - static auto *const *PWINDOWNAMEENABLED = - (Hyprlang::INT *const *)HyprlandAPI::getConfigValue( - PHANDLE, "plugin:hyprview:window_name_enabled") - ->getDataStaticPtr(); - static auto *const *PWINDOWNAMEFONTSIZE = - (Hyprlang::INT *const *)HyprlandAPI::getConfigValue( - PHANDLE, "plugin:hyprview:window_name_font_size") - ->getDataStaticPtr(); - static auto *const *PWINDOWNAMEBGOPACITY = - (Hyprlang::FLOAT *const *)HyprlandAPI::getConfigValue( - PHANDLE, "plugin:hyprview:window_name_bg_opacity") - ->getDataStaticPtr(); - static auto *const *PWINDOWTEXTCOLOR = - (Hyprlang::INT *const *)HyprlandAPI::getConfigValue( - PHANDLE, "plugin:hyprview:window_text_color") - ->getDataStaticPtr(); - - ACTIVE_BORDER_COLOR = **PACTIVEBORDERCOL; - INACTIVE_BORDER_COLOR = **PINACTIVEBORDERCOL; - BORDER_WIDTH = **PBORDERWIDTH; - BORDER_RADIUS = **PBORDERRADIUS; - BG_DIM = **PBGDIM; - WORKSPACE_INDICATOR_ENABLED = **PWORKSPACEINDICATORENABLED != 0; - WORKSPACE_INDICATOR_FONT_SIZE = **PWORKSPACEINDICATORFONTSIZE; - WORKSPACE_INDICATOR_BG_OPACITY = **PWORKSPACEINDICATORBGOPACITY; + static const CConfigValue PMARGIN("plugin:hyprview:margin"); + + MARGIN = *PMARGIN; + + static const CConfigValue PACTIVEBORDERCOL("plugin:hyprview:active_border_color"); + static const CConfigValue PINACTIVEBORDERCOL("plugin:hyprview:inactive_border_color"); + static const CConfigValue PBORDERWIDTH("plugin:hyprview:border_width"); + static const CConfigValue PBORDERRADIUS("plugin:hyprview:border_radius"); + static const CConfigValue PBGDIM("plugin:hyprview:bg_dim"); + static const CConfigValue PWORKSPACEINDICATORENABLED("plugin:hyprview:workspace_indicator_enabled"); + static const CConfigValue PWORKSPACEINDICATORFONTSIZE("plugin:hyprview:workspace_indicator_font_size"); + static const CConfigValue PWORKSPACEINDICATORPOSITION_VAL("plugin:hyprview:workspace_indicator_position"); + static const CConfigValue PWORKSPACEINDICATORBGOPACITY("plugin:hyprview:workspace_indicator_bg_opacity"); + static const CConfigValue PWINDOWNAMEENABLED("plugin:hyprview:window_name_enabled"); + static const CConfigValue PWINDOWNAMEFONTSIZE("plugin:hyprview:window_name_font_size"); + static const CConfigValue PWINDOWNAMEBGOPACITY("plugin:hyprview:window_name_bg_opacity"); + static const CConfigValue PWINDOWTEXTCOLOR("plugin:hyprview:window_text_color"); + + ACTIVE_BORDER_COLOR = *PACTIVEBORDERCOL; + INACTIVE_BORDER_COLOR = *PINACTIVEBORDERCOL; + BORDER_WIDTH = *PBORDERWIDTH; + BORDER_RADIUS = *PBORDERRADIUS; + BG_DIM = *PBGDIM; + WORKSPACE_INDICATOR_ENABLED = *PWORKSPACEINDICATORENABLED != 0; + WORKSPACE_INDICATOR_FONT_SIZE = *PWORKSPACEINDICATORFONTSIZE; + WORKSPACE_INDICATOR_BG_OPACITY = *PWORKSPACEINDICATORBGOPACITY; WORKSPACE_INDICATOR_POSITION = ""; - WINDOW_NAME_ENABLED = **PWINDOWNAMEENABLED != 0; - WINDOW_NAME_FONT_SIZE = **PWINDOWNAMEFONTSIZE; - WINDOW_NAME_BG_OPACITY = **PWINDOWNAMEBGOPACITY; - WINDOW_TEXT_COLOR = **PWINDOWTEXTCOLOR; - - try { - if (PWORKSPACEINDICATORPOSITION_VAL) { - if (auto strPtr = - (Hyprlang::STRING const *) - PWORKSPACEINDICATORPOSITION_VAL->getDataStaticPtr()) { - if (*strPtr) { - WORKSPACE_INDICATOR_POSITION = *strPtr; - } - } - } - } catch (...) { - // Keep default on any exception - } + WINDOW_NAME_ENABLED = *PWINDOWNAMEENABLED != 0; + WINDOW_NAME_FONT_SIZE = *PWINDOWNAMEFONTSIZE; + WINDOW_NAME_BG_OPACITY = *PWINDOWNAMEBGOPACITY; + WINDOW_TEXT_COLOR = *PWINDOWTEXTCOLOR; + + WORKSPACE_INDICATOR_POSITION = *PWORKSPACEINDICATORPOSITION_VAL; std::vector windowsToRender; @@ -1236,15 +1186,12 @@ void CHyprView::onSwipeUpdate(double delta) { if (swipeWasCommenced) return; - static auto *const *PDISTANCE = - (Hyprlang::INT *const *)HyprlandAPI::getConfigValue( - PHANDLE, "plugin:hyprview:gesture_distance") - ->getDataStaticPtr(); + static const CConfigValue PDISTANCE("plugin:hyprview:gesture_distance"); // Calculate progress percentage based on swipe direction // For opening: delta 0 -> distance means scale 0 -> 1 (original -> tile) // For closing: delta 0 -> distance means scale 1 -> 0 (tile -> original) - const float PERC = std::clamp(delta / (double)**PDISTANCE, 0.0, 1.0); + const float PERC = std::clamp(delta / (double)*PDISTANCE, 0.0, 1.0); scale->setValueAndWarp(closing ? (1.0f - PERC) : PERC); } diff --git a/src/main.cpp b/src/main.cpp index 847e04e..5c660f1 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,3 +1,7 @@ +#include +#include +#include +#include #define WLR_USE_UNSTABLE #include "PlacementAlgorithms.hpp" @@ -337,11 +341,8 @@ static SDispatchResult onHyprviewDispatcher(std::string arg) { } // Get margin from config - static auto *const *PMARGIN = - (Hyprlang::INT *const *)HyprlandAPI::getConfigValue( - PHANDLE, "plugin:hyprview:margin") - ->getDataStaticPtr(); - int margin = **PMARGIN; + static const CConfigValue PMARGIN("plugin:hyprview:margin"); + int margin = *PMARGIN; // Prepare screen info ScreenInfo screenInfo = {availableSize.x, availableSize.y, @@ -398,12 +399,9 @@ static SDispatchResult onHyprviewDispatcher(std::string arg) { // Close all instances, similar to onCursorSelect in hyprview.cpp for (auto &[monitor, instance] : g_pHyprViewInstances) { if (instance && !instance->closing) { - // For a general OFF, close all non-explicit instances. - // If a specific monitor is targeted, close it regardless. - bool isExplicitlyTargeted = !parsedArgs.targetMonitor.empty() && - monitor->m_name == parsedArgs.targetMonitor; - - if (!instance->stickyOn || isExplicitlyTargeted) { + // An explicit close command must also close overviews opened with "on". + if (parsedArgs.targetMonitor.empty() || + monitor->m_name == parsedArgs.targetMonitor) { instance->close(); } } @@ -730,6 +728,8 @@ APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) { // Block workspace gestures when overview is active static auto gestureBeginHook = Event::bus()->m_events.gesture.swipe.begin.listen( [](const IPointer::SSwipeBeginEvent&, SCallbackInfo &info) { + if (Config::mgr()->type() == Config::CONFIG_LUA) + return; // If any overview is active and it's not the hyprview gesture itself, // cancel the gesture if (!g_pHyprViewInstances.empty()) { @@ -753,6 +753,8 @@ APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) { static auto gestureUpdateHook = Event::bus()->m_events.gesture.swipe.update.listen( [](const IPointer::SSwipeUpdateEvent&, SCallbackInfo &info) { + if (Config::mgr()->type() == Config::CONFIG_LUA) + return; // Block gesture updates when overview is active (unless it's the // hyprview gesture) if (!g_pHyprViewInstances.empty()) { @@ -772,6 +774,8 @@ APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) { static auto gestureEndHook = Event::bus()->m_events.gesture.swipe.end.listen( [](const IPointer::SSwipeEndEvent&, SCallbackInfo &info) { + if (Config::mgr()->type() == Config::CONFIG_LUA) + return; // Block gesture end when overview is active (unless it's the hyprview // gesture) if (!g_pHyprViewInstances.empty()) { @@ -789,48 +793,57 @@ APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) { } }); + if (Config::mgr()->type() == Config::CONFIG_LUA) { + if (!HyprlandAPI::addLuaFunction(PHANDLE, "hyprview", "toggle", [](lua_State *L) -> int { + const std::string args = luaL_optstring(L, 1, ""); + const auto result = onHyprviewDispatcher(args); + lua_pushboolean(L, result.success); + lua_pushlstring(L, result.error.data(), result.error.size()); + return 2; + })) + throw std::runtime_error("[hyprview] Cannot register Lua toggle function"); + } else { + HyprlandAPI::addConfigKeyword(PHANDLE, "hyprview-gesture", + ::hyprviewGestureKeyword, {}); + } + HyprlandAPI::addDispatcherV2(PHANDLE, "hyprview:toggle", ::onHyprviewDispatcher); Debug::log(LOG, "[hyprview] Plugin initialized, dispatchers " "'hyprview:toggle' registered"); - HyprlandAPI::addConfigKeyword(PHANDLE, "hyprview-gesture", - ::hyprviewGestureKeyword, {}); - HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprview:margin", - Hyprlang::INT{10}); - HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprview:gesture_distance", - Hyprlang::INT{200}); - HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprview:active_border_color", - Hyprlang::INT{0xFFCA7815}); - HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprview:inactive_border_color", - Hyprlang::INT{0x88c0c0c0}); - HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprview:border_width", - Hyprlang::INT{5}); - HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprview:border_radius", - Hyprlang::INT{5}); - HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprview:bg_dim", - Hyprlang::FLOAT{0.4}); - HyprlandAPI::addConfigValue( - PHANDLE, "plugin:hyprview:workspace_indicator_enabled", Hyprlang::INT{1}); - HyprlandAPI::addConfigValue(PHANDLE, - "plugin:hyprview:workspace_indicator_font_size", - Hyprlang::INT{28}); - HyprlandAPI::addConfigValue(PHANDLE, - "plugin:hyprview:workspace_indicator_position", - Hyprlang::STRING{""}); - HyprlandAPI::addConfigValue(PHANDLE, - "plugin:hyprview:workspace_indicator_bg_opacity", - Hyprlang::FLOAT{0.85}); - HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprview:window_name_enabled", - Hyprlang::INT{1}); - HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprview:window_name_font_size", - Hyprlang::INT{20}); - HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprview:window_name_bg_opacity", - Hyprlang::FLOAT{0.85}); - HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprview:window_text_color", - Hyprlang::INT{0xFFFFFFFF}); + if (!HyprlandAPI::addConfigValueV2(PHANDLE, makeShared("plugin:hyprview:margin", "Hyprview option", 10))) + throw std::runtime_error("[hyprview] Cannot register plugin:hyprview:margin"); + if (!HyprlandAPI::addConfigValueV2(PHANDLE, makeShared("plugin:hyprview:gesture_distance", "Hyprview option", 200))) + throw std::runtime_error("[hyprview] Cannot register plugin:hyprview:gesture_distance"); + if (!HyprlandAPI::addConfigValueV2(PHANDLE, makeShared("plugin:hyprview:active_border_color", "Hyprview option", 0xFFCA7815))) + throw std::runtime_error("[hyprview] Cannot register plugin:hyprview:active_border_color"); + if (!HyprlandAPI::addConfigValueV2(PHANDLE, makeShared("plugin:hyprview:inactive_border_color", "Hyprview option", 0x88c0c0c0))) + throw std::runtime_error("[hyprview] Cannot register plugin:hyprview:inactive_border_color"); + if (!HyprlandAPI::addConfigValueV2(PHANDLE, makeShared("plugin:hyprview:border_width", "Hyprview option", 5))) + throw std::runtime_error("[hyprview] Cannot register plugin:hyprview:border_width"); + if (!HyprlandAPI::addConfigValueV2(PHANDLE, makeShared("plugin:hyprview:border_radius", "Hyprview option", 5))) + throw std::runtime_error("[hyprview] Cannot register plugin:hyprview:border_radius"); + if (!HyprlandAPI::addConfigValueV2(PHANDLE, makeShared("plugin:hyprview:bg_dim", "Hyprview option", 0.4))) + throw std::runtime_error("[hyprview] Cannot register plugin:hyprview:bg_dim"); + if (!HyprlandAPI::addConfigValueV2(PHANDLE, makeShared("plugin:hyprview:workspace_indicator_enabled", "Hyprview option", 1))) + throw std::runtime_error("[hyprview] Cannot register plugin:hyprview:workspace_indicator_enabled"); + if (!HyprlandAPI::addConfigValueV2(PHANDLE, makeShared("plugin:hyprview:workspace_indicator_font_size", "Hyprview option", 28))) + throw std::runtime_error("[hyprview] Cannot register plugin:hyprview:workspace_indicator_font_size"); + if (!HyprlandAPI::addConfigValueV2(PHANDLE, makeShared("plugin:hyprview:workspace_indicator_position", "Hyprview option", ""))) + throw std::runtime_error("[hyprview] Cannot register plugin:hyprview:workspace_indicator_position"); + if (!HyprlandAPI::addConfigValueV2(PHANDLE, makeShared("plugin:hyprview:workspace_indicator_bg_opacity", "Hyprview option", 0.85))) + throw std::runtime_error("[hyprview] Cannot register plugin:hyprview:workspace_indicator_bg_opacity"); + if (!HyprlandAPI::addConfigValueV2(PHANDLE, makeShared("plugin:hyprview:window_name_enabled", "Hyprview option", 1))) + throw std::runtime_error("[hyprview] Cannot register plugin:hyprview:window_name_enabled"); + if (!HyprlandAPI::addConfigValueV2(PHANDLE, makeShared("plugin:hyprview:window_name_font_size", "Hyprview option", 20))) + throw std::runtime_error("[hyprview] Cannot register plugin:hyprview:window_name_font_size"); + if (!HyprlandAPI::addConfigValueV2(PHANDLE, makeShared("plugin:hyprview:window_name_bg_opacity", "Hyprview option", 0.85))) + throw std::runtime_error("[hyprview] Cannot register plugin:hyprview:window_name_bg_opacity"); + if (!HyprlandAPI::addConfigValueV2(PHANDLE, makeShared("plugin:hyprview:window_text_color", "Hyprview option", 0xFFFFFFFF))) + throw std::runtime_error("[hyprview] Cannot register plugin:hyprview:window_text_color"); HyprlandAPI::reloadConfig(); return {"hyprview", "Window overview with multiple placement algorithms", diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..3050473 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,21 @@ +# Nested integration test + +Build against headers matching the installed Hyprland binary. The test requires +Python 3, Hyprland/hyprctl, foot, and grim, plus a running Wayland compositor. + +```sh +make -C src +python3 tests/nested.py build/hyprview.so --scale 1 +python3 tests/nested.py build/hyprview.so --scale 2 +``` + +Use `--parent-display wayland-N` if the terminal's `WAYLAND_DISPLAY` is stale. +The test starts its own nested compositor with a temporary Lua configuration and +a headless output. It identifies that compositor by its child PID and sends all +commands to that instance. It never loads the test plugin into the parent. +Logs and screenshots are retained in the printed temporary directory. + +Checks cover two windows on separate workspaces, three open/close cycles, exact +workspace/fullscreen-state restoration, config reload, and unloading while open. +This exercises Lua callbacks directly; physical touchpad gestures still require +a manual check. It does not cover older Hyprland versions or rotated outputs. diff --git a/tests/nested.py b/tests/nested.py new file mode 100644 index 0000000..1cb7334 --- /dev/null +++ b/tests/nested.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +"""Exercise Hyprview only in a compositor launched by this test.""" + +import argparse +import json +import os +from pathlib import Path +import shutil +import subprocess +import tempfile +import time + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("plugin", type=lambda p: Path(p).resolve()) + parser.add_argument("--parent-display", default=os.environ.get("WAYLAND_DISPLAY")) + parser.add_argument("--scale", type=int, choices=(1, 2), default=1) + args = parser.parse_args() + if not args.plugin.is_file() or not args.parent_display: + parser.error("provide a built plugin and an existing parent Wayland display") + for command in ("Hyprland", "hyprctl", "foot", "grim"): + if not shutil.which(command): + parser.error(f"missing test dependency: {command}") + + # Retain the log and screenshots on failure as well as success. + root = Path(tempfile.mkdtemp(prefix="hyprview-test-")) + print(f"Test artifacts: {root}", flush=True) + config = root / "hyprland.lua" + config.write_text( + 'hl.monitor({ output="", mode="1280x800@60", position="1280x0", scale=1 })\n' + f'hl.monitor({{ output="TEST", mode="1280x800@60", position="0x0", scale={args.scale} }})\n' + 'hl.config({ misc={disable_hyprland_logo=true,disable_splash_rendering=true} })\n' + ) + env = dict(os.environ, WAYLAND_DISPLAY=args.parent_display, AQ_DRM_DEVICES="/dev/null") + with (root / "compositor.log").open("w") as log: + child = subprocess.Popen(["Hyprland", "--config", str(config)], env=env, + stdout=log, stderr=subprocess.STDOUT) + try: + instance = None + for _ in range(50): + if child.poll() is not None: + raise RuntimeError("nested compositor exited; see compositor.log") + instances = json.loads(subprocess.check_output( + ["hyprctl", "-j", "instances"], timeout=10)) + instance = next((i for i in instances if i["pid"] == child.pid), None) + if instance: + break + time.sleep(0.2) + assert instance, "nested compositor did not become ready" + + def ctl(*command): + assert child.poll() is None, "nested compositor crashed" + result = subprocess.run(["hyprctl", "-i", instance["instance"], *command], + capture_output=True, text=True, timeout=10, check=True) + if "error" in result.stdout.lower() or "invalid" in result.stdout.lower(): + raise RuntimeError(result.stdout) + return result.stdout + + def lua(code): + return ctl("eval", code) + + def clients(): + return json.loads(ctl("-j", "clients")) + + def state(): + return {c["address"]: (c["workspace"]["id"], c["fullscreen"], + c["fullscreenClient"]) for c in clients()} + + def overview(command): + lua(f"assert(hl.plugin.hyprview.toggle({json.dumps(command)}))") + time.sleep(1.5) + + time.sleep(1) + ctl("output", "create", "headless", "TEST") + ctl("plugin", "load", str(args.plugin)) + lua('hl.dispatch(hl.dsp.focus({monitor="TEST"}))') + lua('hl.exec_cmd("foot --title=Hyprview-test-one"); ' + 'hl.exec_cmd("foot --title=Hyprview-test-two")') + time.sleep(2) + assert len(clients()) == 2, clients() + test_monitor = next(m for m in json.loads(ctl("-j", "monitors")) if m["name"] == "TEST") + assert all(c["monitor"] == test_monitor["id"] for c in clients()) + other_workspace = clients()[0]["workspace"]["id"] + 1 + lua(f'hl.dispatch(hl.dsp.window.move({{workspace="{other_workspace}",follow=false}}))') + time.sleep(0.5) + before = state() + assert len({s[0] for s in before.values()}) == 2, before + for cycle in range(3): + overview("on all special") + if cycle == 0: + subprocess.run(["grim", "-o", "TEST", str(root / "overview.png")], + env=dict(env, WAYLAND_DISPLAY=instance["wl_socket"]), + check=True, timeout=10) + overview("off") + assert state() == before, (before, state()) + print(f"Open/close cycle {cycle + 1}: PASS", flush=True) + ctl("reload") + time.sleep(1) + assert not ctl("configerrors").strip() + overview("on all special") + overview("off") + assert state() == before + # Unload while open must restore windows as well. + overview("on all special") + ctl("plugin", "unload", str(args.plugin)) + time.sleep(1) + assert state() == before + print("Reload, reopen, and unload restoration: PASS", flush=True) + finally: + if child.poll() is None: + child.terminate() + try: + child.wait(timeout=10) + except subprocess.TimeoutExpired: + child.kill() + child.wait() + + +if __name__ == "__main__": + main()