Phosphor
Qt6 / Wayland library suite for window-management tools
 
Loading...
Searching...
No Matches
ScriptedAlgorithm.h
Go to the documentation of this file.
1// SPDX-FileCopyrightText: 2026 fuddlesworth
2// SPDX-License-Identifier: LGPL-2.1-or-later
3
4#pragma once
5
7#include "SplitTree.h"
8#include "TilingAlgorithm.h"
9#include <QJSValue>
10
11#include <algorithm>
12#include <atomic>
13#include <functional>
14#include <memory>
15#include <type_traits>
16
17class QJSEngine;
18
19namespace PhosphorTiles {
20
21class ScriptedAlgorithmWatchdog;
22
23// Note: ScriptedAlgorithm is NOT thread-safe despite the base class const contract.
24// All calls must occur on the main thread. QJSEngine is inherently single-threaded.
25
26namespace detail {
27
31template<typename T>
32bool jsValueHasType(const QJSValue& v)
33{
34 if constexpr (std::is_same_v<T, bool>)
35 return v.isBool();
36 else
37 return v.isNumber();
38}
39
43template<typename T>
44T jsValueTo(const QJSValue& v)
45{
46 if constexpr (std::is_same_v<T, bool>)
47 return v.toBool();
48 else if constexpr (std::is_same_v<T, int>)
49 return v.toInt();
50 else
51 return static_cast<T>(v.toNumber());
52}
53
54} // namespace detail
55
80class PHOSPHORTILES_EXPORT ScriptedAlgorithm : public TilingAlgorithm
81{
82 Q_OBJECT
83
84 // The watchdog is the only caller of interruptEngine() — it dispatches
85 // the interrupt from its supervisor thread while holding the watchdog
86 // mutex. Exposing that entry point publicly would tempt unrelated
87 // code to call it from the main thread, which would interact badly
88 // with guardedCall()'s arm-evaluate-disarm invariant.
90
91public:
114 explicit ScriptedAlgorithm(const QString& filePath, std::shared_ptr<ScriptedAlgorithmWatchdog> watchdog = nullptr,
115 QObject* parent = nullptr);
117
121 bool isValid() const;
122
126 QString filePath() const;
127
133 QString scriptId() const;
134
141 QString id() const;
142
147 void setUserScript(bool isUser);
148
149 // TilingAlgorithm interface
150 // Note: name(), description(), and the JS-override accessors below lack noexcept
151 // because they can theoretically call into QJSEngine (which may throw or fail).
152 // Only metadata-only accessors (supportsMemory, centerLayout, etc.) are noexcept.
153 QString name() const override;
154 QString description() const override;
155 QVector<QRect> calculateZones(const TilingParams& params) const override;
156 int masterZoneIndex() const override;
157 bool supportsMasterCount() const override;
158 bool supportsSplitRatio() const override;
159 qreal defaultSplitRatio() const override;
160 int minimumWindows() const override;
161 int defaultMaxWindows() const override;
162 bool producesOverlappingZones() const override;
163 bool supportsMinSizes() const noexcept override;
164 bool supportsMemory() const noexcept override;
165 QString zoneNumberDisplay() const noexcept override;
166 bool centerLayout() const override;
167 bool isScripted() const noexcept override;
168 bool isUserScript() const noexcept override;
169 void prepareTilingState(TilingState* state) const override;
170
171 // Lifecycle hooks (v2)
172 bool supportsLifecycleHooks() const noexcept override;
173 void onWindowAdded(TilingState* state, int windowIndex) override;
174 void onWindowRemoved(TilingState* state, int windowIndex) override;
175
176 // Custom parameters (v2)
177 bool supportsCustomParams() const noexcept override;
178 QVariantList customParamDefList() const override;
179 bool hasCustomParam(const QString& name) const override;
183 const QVector<ScriptedHelpers::CustomParamDef>& customParamDefs() const;
184
185private:
191 bool loadScript(const QString& filePath);
192
199 QJSValue splitNodeToJSValue(const SplitNode* node, int depth = 0) const;
200
202 QJSValue splitNodeToJSValueImpl(const SplitNode* node, const QJSValue& freezeFn, int depth, int& nodeCount) const;
203
211 template<typename T>
212 T resolveJsOverride(const QJSValue& jsFn, T cachedValue, T metadataFallback) const
213 {
214 if (m_cachedValuesLoaded && jsFn.isCallable()) {
215 return cachedValue;
216 }
217 // After loadScript() sets m_cachedValuesLoaded, all calls use the cached path above.
218 // Before that, fall back to metadata — never call JS without the watchdog.
219 return metadataFallback;
220 }
221
225 template<typename T>
226 T resolveJsOverrideClamped(const QJSValue& jsFn, T cachedValue, T metadataFallback, T minVal, T maxVal) const
227 {
228 return std::clamp(resolveJsOverride<T>(jsFn, cachedValue, metadataFallback), minVal, maxVal);
229 }
230
232 QJSValue buildJsState(const TilingState* state) const;
233
235 QJSValue buildJsWindowArray(const QVector<WindowInfo>& infos, int cap) const;
236
238 QJSValue guardedCall(const std::function<QJSValue()>& fn) const;
239
258 void interruptEngine();
259
261 static constexpr int MaxTreeConversionDepth = AutotileDefaults::MaxRuntimeTreeDepth;
262
263 // Owned via QObject parent; mutable because calculateZones() is const but JS evaluation mutates engine state
264 mutable QJSEngine* m_engine = nullptr;
271 std::shared_ptr<ScriptedAlgorithmWatchdog> m_watchdog;
272 mutable QJSValue m_calculateZonesFn;
273 QString m_filePath;
274 QString m_scriptId;
275 bool m_valid = false;
276 bool m_isUserScript = false;
277 mutable std::atomic<bool> m_evaluating{false};
278 mutable bool m_lastCallTimedOut = false;
279 mutable uint32_t m_gcCounter = 0;
280 static constexpr uint32_t GcInterval = 8;
281
282 // Consolidated parsed metadata (from // @key value comments)
283 ScriptedHelpers::ScriptMetadata m_metadata;
284
285 // Optional JS function overrides (checked at call time)
286 mutable QJSValue m_jsMasterZoneIndex;
287 mutable QJSValue m_jsSupportsMasterCount;
288 mutable QJSValue m_jsSupportsSplitRatio;
289 mutable QJSValue m_jsDefaultSplitRatio;
290 mutable QJSValue m_jsMinimumWindows;
291 mutable QJSValue m_jsDefaultMaxWindows;
292 mutable QJSValue m_jsProducesOverlappingZones;
293 mutable QJSValue m_jsCenterLayout;
294
295 // Optional lifecycle hook JS functions
296 mutable QJSValue m_jsOnWindowAdded;
297 mutable QJSValue m_jsOnWindowRemoved;
298 bool m_hasLifecycleHooks = false;
299
300 // Cached JS virtual method overrides (loaded once at script load time)
301 int m_cachedMinimumWindows = 1;
302 int m_cachedDefaultMaxWindows = 6;
303 int m_cachedMasterZoneIndex = -1;
304 qreal m_cachedDefaultSplitRatio = AutotileDefaults::DefaultSplitRatio;
305 bool m_cachedProducesOverlappingZones = false;
306 bool m_cachedSupportsMasterCount = false;
307 bool m_cachedSupportsSplitRatio = false;
308 bool m_cachedCenterLayout = false;
309 bool m_cachedValuesLoaded = false;
310};
311
312} // namespace PhosphorTiles
Per-loader watchdog for scripted algorithms.
Definition ScriptedAlgorithmWatchdog.h:54
Definition ScriptedAlgorithm.h:81
bool isValid() const
Whether the script loaded successfully and has a calculateZones function.
bool supportsMasterCount() const override
Check if algorithm supports variable master count.
QString id() const
Optional algorithm ID from metadata.
void setUserScript(bool isUser)
Mark whether this script was loaded from a user directory.
bool supportsSplitRatio() const override
Check if algorithm supports split ratio adjustment.
QVector< QRect > calculateZones(const TilingParams &params) const override
Calculate zone geometries for N windows.
QString description() const override
Description of the algorithm behavior.
bool supportsMinSizes() const noexcept override
Whether this algorithm supports per-window minimum size constraints.
bool producesOverlappingZones() const override
Whether this algorithm intentionally produces overlapping zones.
ScriptedAlgorithm(const QString &filePath, std::shared_ptr< ScriptedAlgorithmWatchdog > watchdog=nullptr, QObject *parent=nullptr)
Construct a ScriptedAlgorithm from a JavaScript file.
QString name() const override
Human-readable name of the algorithm.
int defaultMaxWindows() const override
Get default maximum number of windows for this algorithm.
int minimumWindows() const override
Get minimum number of windows for meaningful tiling.
QString filePath() const
Absolute path to the source script file.
qreal defaultSplitRatio() const override
Get default split ratio for this algorithm.
int masterZoneIndex() const override
Get the index of the "master" zone (if applicable)
QString scriptId() const
Identifier derived from the filename without extension.
Abstract base class for tiling algorithms.
Definition TilingAlgorithm.h:56
Tracks tiling state for a single screen.
Definition TilingState.h:40
T jsValueTo(const QJSValue &v)
Type-converter for QJSValue results — selects toBool() for bool, toInt() for int, toNumber() for qrea...
Definition ScriptedAlgorithm.h:44
bool jsValueHasType(const QJSValue &v)
Type-checker for QJSValue results — selects isBool() for bool, isNumber() for numeric types.
Definition ScriptedAlgorithm.h:32
Definition AutotileEngine.h:71
A single node in the binary split tree.
Definition SplitTree.h:27
Parameters for zone calculation.
Definition TilingParams.h:61