Merge branch 'main' into feat/llm-chat-tab
C++ / fmt (pull_request) Failing after 3s
C++ / build (pull_request) Failing after 9s
C++ / clang-tidy (pull_request) Failing after 9s
JS/TS / fmt (pull_request) Successful in 9s
JS/TS / lint (pull_request) Successful in 10s
Python / static (pull_request) Successful in 28s
Rust / build (pull_request) Successful in 49s
Python / verify (pull_request) Successful in 1m40s
Rust / fmt (pull_request) Successful in 27s
Rust / clippy (pull_request) Successful in 46s

This commit is contained in:
2026-09-30 17:59:14 +02:00
298 changed files with 18329 additions and 6057 deletions
+20
View File
@@ -1,5 +1,6 @@
qml_module(ZShell
URI ZShell
INCLUDE_PREFIX core
SOURCES
writefile.hpp writefile.cpp
appdb.hpp appdb.cpp
@@ -23,6 +24,17 @@ target_compile_definitions(ZShell PRIVATE
GIT_REVISION="${GIT_REVISION}"
)
# Node macros expand to Q_OBJECT, automoc can't see it otherwise
list(APPEND CMAKE_AUTOMOC_MACRO_NAMES
CONFIG_NODE
CONFIG_NODE_NO_CTOR
CONFIG_LIST_TYPE
SETTINGS_NODE
SETTINGS_NODE_NO_CTOR
SETTINGS_LIST_TYPE
)
add_subdirectory(Settings)
add_subdirectory(Models)
add_subdirectory(Internal)
add_subdirectory(Services)
@@ -30,3 +42,11 @@ add_subdirectory(Components)
add_subdirectory(Blobs)
add_subdirectory(Config)
add_subdirectory(Llm)
pkg_check_modules(HYPRLAND_PROBE hyprland)
if(HYPRLAND_PROBE_FOUND)
add_subdirectory(HyprPlugins)
else()
message(STATUS
"hyprland development headers not found; skipping zshell-nightlight plugin")
endif()
+4
View File
@@ -7,7 +7,11 @@ qml_module(ZShell-components
buttonrow.hpp buttonrow.cpp
carouselview.hpp carouselview.cpp
wheelinverter.hpp
sparklinecontent.hpp sparklinecontent.cpp
sparklineitem.hpp sparklineitem.cpp
sparklinesceneitem.hpp sparklinesceneitem.cpp
LIBRARIES
Qt::Quick
Qt::QuickPrivate
ZShell-internal
)
@@ -0,0 +1,250 @@
#include "sparklinecontent.hpp"
#include <qmath.h>
#include <qpainter.h>
#include <qpen.h>
namespace ZShell::components {
SparklineContent::SparklineContent(QQuickItem* parent)
: QQuickPaintedItem(parent) {
setAntialiasing(true);
}
qreal SparklineContent::stepX() const {
if (m_historyLength < 2 || m_viewportW <= 0.0) return 0.0;
if (m_sliding) return m_viewportW / static_cast<qreal>(m_historyLength - 1);
const int len = qMax(
2,
(m_line1 && m_line1->count() >= 2) ? m_line1->count()
: (m_line2 ? m_line2->count() : 2));
return m_viewportW / static_cast<qreal>(len - 1);
}
void SparklineContent::setViewportSize(
qreal viewportWidth, qreal viewportHeight) {
if (qFuzzyCompare(m_viewportW, viewportWidth) &&
qFuzzyCompare(m_viewportH, viewportHeight))
return;
m_viewportW = viewportWidth;
m_viewportH = viewportHeight;
invalidate();
}
void SparklineContent::setLine1(internal::CircularBuffer* buffer) {
if (m_line1 == buffer) return;
if (m_line1) disconnect(m_line1, nullptr, this, nullptr);
m_line1 = buffer;
connectBuffer(buffer);
invalidate();
}
void SparklineContent::setLine2(internal::CircularBuffer* buffer) {
if (m_line2 == buffer) return;
if (m_line2) disconnect(m_line2, nullptr, this, nullptr);
m_line2 = buffer;
connectBuffer(buffer);
invalidate();
}
void SparklineContent::connectBuffer(internal::CircularBuffer* buffer) {
if (!buffer) return;
// New data, or the buffer's own smoothingRadius changed (it re-emits
// valuesChanged in that case too) -> geometry is stale.
connect(
buffer,
&internal::CircularBuffer::valuesChanged,
this,
&SparklineContent::invalidate);
connect(buffer, &QObject::destroyed, this, [this, buffer]() {
if (m_line1 == buffer) m_line1 = nullptr;
if (m_line2 == buffer) m_line2 = nullptr;
invalidate();
});
}
void SparklineContent::setLine1Color(const QColor& color) {
if (m_line1Color == color) return;
m_line1Color = color;
update(); // visual-only, no geometry rebuild needed
}
void SparklineContent::setLine2Color(const QColor& color) {
if (m_line2Color == color) return;
m_line2Color = color;
update();
}
void SparklineContent::setLine1FillAlpha(qreal alpha) {
if (qFuzzyCompare(m_line1FillAlpha, alpha)) return;
m_line1FillAlpha = alpha;
update();
}
void SparklineContent::setLine2FillAlpha(qreal alpha) {
if (qFuzzyCompare(m_line2FillAlpha, alpha)) return;
m_line2FillAlpha = alpha;
update();
}
void SparklineContent::setMaxValue(qreal value) {
if (qFuzzyCompare(m_maxValue, value)) return;
m_maxValue = value;
invalidate();
}
void SparklineContent::setLineWidth(qreal width) {
if (qFuzzyCompare(m_lineWidth, width)) return;
m_lineWidth = width;
invalidate();
}
void SparklineContent::setCurveSmoothing(bool enabled) {
if (m_curveSmoothing == enabled) return;
m_curveSmoothing = enabled;
invalidate();
}
void SparklineContent::setHistoryLength(int length) {
if (m_historyLength == length) return;
m_historyLength = length;
invalidate();
}
void SparklineContent::setSliding(bool sliding) {
if (m_sliding == sliding) return;
m_sliding = sliding;
invalidate();
}
void SparklineContent::invalidate() {
m_dirty = true;
applyGeometry();
update();
}
void SparklineContent::applyGeometry() {
const qreal pad = m_sliding ? stepX() : 0.0;
setSize(QSizeF(m_viewportW + pad, m_viewportH));
}
SparklineContent::LineGeometry SparklineContent::buildGeometry(
internal::CircularBuffer* buffer) const {
LineGeometry geom;
if (!buffer || buffer->count() < 2 || m_maxValue <= 0.0 ||
m_viewportW <= 0.0)
return geom;
if (m_historyLength < 2) return geom;
const int len = buffer->count();
const qreal step = stepX();
if (step <= 0.0) return geom;
qreal startX = 0.0;
if (m_sliding)
startX = m_viewportW - (len - 1) * step + step;
else
startX = 0.0;
const qreal h = m_viewportH;
const qreal strokePad = qCeil(m_lineWidth / 2);
const qreal curvePad = 3.0;
const qreal plotTop = strokePad + curvePad;
const qreal plotBottom = h - strokePad;
const qreal fillBottom = h;
const qreal plotH = qMax<qreal>(1.0, plotBottom - plotTop);
QVector<QPointF> points;
points.reserve(len);
for (int i = 0; i < len; ++i) {
const qreal value =
qBound<qreal>(0.0, buffer->smoothedAt(i), m_maxValue);
const qreal x = startX + i * step;
const qreal y = plotTop + (1.0 - (value / m_maxValue)) * plotH;
points.append(QPointF(x, y));
}
QPainterPath linePath;
linePath.moveTo(points[0]);
if (m_curveSmoothing && points.size() > 2) {
const qreal tension = 1.0;
const qreal k = tension / 6.0;
const int n = points.size();
for (int i = 0; i < n - 1; ++i) {
const QPointF& p0 = points[qMax(i - 1, 0)];
const QPointF& p1 = points[i];
const QPointF& p2 = points[i + 1];
const QPointF& p3 = points[qMin(i + 2, n - 1)];
QPointF c1 = p1 + (p2 - p0) * k;
QPointF c2 = p2 - (p3 - p1) * k;
const qreal segMinY = qMin(p1.y(), p2.y());
const qreal segMaxY = qMax(p1.y(), p2.y());
c1.setY(qBound(segMinY, c1.y(), segMaxY));
c2.setY(qBound(segMinY, c2.y(), segMaxY));
linePath.cubicTo(c1, c2, p2);
}
} else {
for (int i = 0; i < points.size() - 1; ++i) {
const QPointF& p0 = points[i];
const QPointF& p1 = points[i + 1];
const qreal ctrlX = (p0.x() + p1.x()) * 0.5;
linePath.cubicTo(QPointF(ctrlX, p0.y()), QPointF(ctrlX, p1.y()), p1);
}
}
geom.stroke = linePath;
QPainterPath fillPath = linePath;
fillPath.lineTo(startX + (len - 1) * step, fillBottom);
fillPath.lineTo(startX, fillBottom);
fillPath.closeSubpath();
geom.fill = fillPath;
geom.hasData = true;
return geom;
}
void SparklineContent::rebuildIfNeeded() {
if (!m_dirty) return;
m_geom1 = buildGeometry(m_line1);
m_geom2 = buildGeometry(m_line2);
m_dirty = false;
}
void SparklineContent::paint(QPainter* painter) {
rebuildIfNeeded();
if (!m_geom1.hasData && !m_geom2.hasData) return;
painter->setRenderHint(QPainter::Antialiasing, true);
auto drawOne =
[&](const LineGeometry& geom, const QColor& color, qreal fillAlpha) {
if (!geom.hasData) return;
QColor fillColor = color;
fillColor.setAlphaF(static_cast<float>(fillAlpha));
painter->setPen(Qt::NoPen);
painter->setBrush(fillColor);
painter->drawPath(geom.fill);
QPen pen(color, m_lineWidth);
pen.setCapStyle(Qt::RoundCap);
pen.setJoinStyle(Qt::RoundJoin);
painter->setPen(pen);
painter->setBrush(Qt::NoBrush);
painter->drawPath(geom.stroke);
};
drawOne(m_geom1, m_line1Color, m_line1FillAlpha);
drawOne(m_geom2, m_line2Color, m_line2FillAlpha);
}
} // namespace ZShell::components
@@ -0,0 +1,69 @@
#pragma once
#include <qcolor.h>
#include <qpainterpath.h>
#include <qquickpainteditem.h>
#include "internal/circularbuffer.hpp"
namespace ZShell::components {
class SparklineContent : public QQuickPaintedItem {
Q_OBJECT
public:
explicit SparklineContent(QQuickItem* parent = nullptr);
void paint(QPainter* painter) override;
void setLine1(internal::CircularBuffer* buffer);
void setLine2(internal::CircularBuffer* buffer);
void setLine1Color(const QColor& color);
void setLine2Color(const QColor& color);
void setLine1FillAlpha(qreal alpha);
void setLine2FillAlpha(qreal alpha);
void setMaxValue(qreal value);
void setLineWidth(qreal width);
void setCurveSmoothing(bool enabled);
void setHistoryLength(int length);
void setSliding(bool sliding);
void setViewportSize(qreal viewportWidth, qreal viewportHeight);
[[nodiscard]] qreal stepX() const;
private:
struct LineGeometry {
QPainterPath stroke;
QPainterPath fill;
bool hasData = false;
};
void invalidate();
void rebuildIfNeeded();
[[nodiscard]] LineGeometry buildGeometry(
internal::CircularBuffer* buffer) const;
void connectBuffer(internal::CircularBuffer* buffer);
void applyGeometry();
internal::CircularBuffer* m_line1 = nullptr;
internal::CircularBuffer* m_line2 = nullptr;
QColor m_line1Color;
QColor m_line2Color;
qreal m_line1FillAlpha = 0.15;
qreal m_line2FillAlpha = 0.2;
qreal m_maxValue = 1024.0;
qreal m_lineWidth = 2.0;
bool m_curveSmoothing = false;
int m_historyLength = 30;
bool m_sliding = true;
qreal m_viewportW = 0.0;
qreal m_viewportH = 0.0;
LineGeometry m_geom1;
LineGeometry m_geom2;
bool m_dirty = true;
};
} // namespace ZShell::components
+179
View File
@@ -0,0 +1,179 @@
#include "sparklineitem.hpp"
#include "sparklinecontent.hpp"
namespace ZShell::components {
SparklineItem::SparklineItem(QQuickItem* parent) : QQuickItem(parent) {
setClip(true);
m_content = new SparklineContent(this);
m_content->setHistoryLength(m_historyLength);
m_content->setSliding(m_sliding);
}
void SparklineItem::geometryChange(
const QRectF& newGeometry, const QRectF& oldGeometry) {
QQuickItem::geometryChange(newGeometry, oldGeometry);
m_content->setViewportSize(newGeometry.width(), newGeometry.height());
updateContentOffset();
}
void SparklineItem::updateContentOffset() {
m_content->setX(-m_content->stepX() * m_slideProgress);
}
void SparklineItem::applySmoothingRadius(
internal::CircularBuffer* buffer) const {
if (buffer) buffer->setSmoothingRadius(qRound(m_smoothing));
}
internal::CircularBuffer* SparklineItem::line1() const {
return m_line1;
}
void SparklineItem::setLine1(internal::CircularBuffer* buffer) {
if (m_line1 == buffer) return;
m_line1 = buffer;
m_content->setLine1(buffer);
applySmoothingRadius(buffer);
emit line1Changed();
}
internal::CircularBuffer* SparklineItem::line2() const {
return m_line2;
}
void SparklineItem::setLine2(internal::CircularBuffer* buffer) {
if (m_line2 == buffer) return;
m_line2 = buffer;
m_content->setLine2(buffer);
applySmoothingRadius(buffer);
emit line2Changed();
}
QColor SparklineItem::line1Color() const {
return m_line1Color;
}
void SparklineItem::setLine1Color(const QColor& color) {
if (m_line1Color == color) return;
m_line1Color = color;
m_content->setLine1Color(color);
emit line1ColorChanged();
}
QColor SparklineItem::line2Color() const {
return m_line2Color;
}
void SparklineItem::setLine2Color(const QColor& color) {
if (m_line2Color == color) return;
m_line2Color = color;
m_content->setLine2Color(color);
emit line2ColorChanged();
}
qreal SparklineItem::line1FillAlpha() const {
return m_line1FillAlpha;
}
void SparklineItem::setLine1FillAlpha(qreal alpha) {
if (qFuzzyCompare(m_line1FillAlpha, alpha)) return;
m_line1FillAlpha = alpha;
m_content->setLine1FillAlpha(alpha);
emit line1FillAlphaChanged();
}
qreal SparklineItem::line2FillAlpha() const {
return m_line2FillAlpha;
}
void SparklineItem::setLine2FillAlpha(qreal alpha) {
if (qFuzzyCompare(m_line2FillAlpha, alpha)) return;
m_line2FillAlpha = alpha;
m_content->setLine2FillAlpha(alpha);
emit line2FillAlphaChanged();
}
qreal SparklineItem::maxValue() const {
return m_maxValue;
}
void SparklineItem::setMaxValue(qreal value) {
if (qFuzzyCompare(m_maxValue, value)) return;
m_maxValue = value;
m_content->setMaxValue(value);
emit maxValueChanged();
}
qreal SparklineItem::slideProgress() const {
return m_slideProgress;
}
void SparklineItem::setSlideProgress(qreal progress) {
if (qFuzzyCompare(m_slideProgress, progress)) return;
m_slideProgress = progress;
updateContentOffset();
emit slideProgressChanged();
}
int SparklineItem::historyLength() const {
return m_historyLength;
}
void SparklineItem::setHistoryLength(int length) {
if (m_historyLength == length) return;
m_historyLength = length;
m_content->setHistoryLength(length);
updateContentOffset();
emit historyLengthChanged();
}
qreal SparklineItem::lineWidth() const {
return m_lineWidth;
}
void SparklineItem::setLineWidth(qreal width) {
if (qFuzzyCompare(m_lineWidth, width)) return;
m_lineWidth = width;
m_content->setLineWidth(width);
emit lineWidthChanged();
}
void SparklineItem::setSliding(bool sliding) {
if (m_sliding == sliding) return;
m_sliding = sliding;
m_content->setSliding(sliding);
updateContentOffset();
emit slidingChanged();
}
bool SparklineItem::sliding() const {
return m_sliding;
}
void SparklineItem::setSmoothing(qreal smoothing) {
smoothing = qMax<qreal>(0.0, smoothing);
if (qFuzzyCompare(m_smoothing, smoothing)) return;
m_smoothing = smoothing;
applySmoothingRadius(m_line1);
applySmoothingRadius(m_line2);
emit smoothingChanged();
}
qreal SparklineItem::smoothing() const {
return m_smoothing;
}
void SparklineItem::setCurveSmoothing(bool enabled) {
if (m_curveSmoothing == enabled) return;
m_curveSmoothing = enabled;
m_content->setCurveSmoothing(enabled);
emit curveSmoothingChanged();
}
bool SparklineItem::curveSmoothing() const {
return m_curveSmoothing;
}
} // namespace ZShell::components
@@ -3,20 +3,25 @@
#include <qcolor.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qquickpainteditem.h>
#include <qquickitem.h>
#include <qtmetamacros.h>
#include "circularbuffer.hpp"
#include "internal/circularbuffer.hpp"
namespace ZShell::internal {
namespace ZShell::components {
class SparklineItem : public QQuickPaintedItem {
class SparklineContent;
class SparklineItem : public QQuickItem {
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(
CircularBuffer* line1 READ line1 WRITE setLine1 NOTIFY line1Changed)
internal::CircularBuffer* line1 READ line1 WRITE setLine1 NOTIFY
line1Changed)
Q_PROPERTY(
CircularBuffer* line2 READ line2 WRITE setLine2 NOTIFY line2Changed)
internal::CircularBuffer* line2 READ line2 WRITE setLine2 NOTIFY
line2Changed)
Q_PROPERTY(
QColor line1Color READ line1Color WRITE setLine1Color NOTIFY
line1ColorChanged)
@@ -40,17 +45,22 @@ class SparklineItem : public QQuickPaintedItem {
Q_PROPERTY(
qreal lineWidth READ lineWidth WRITE setLineWidth NOTIFY
lineWidthChanged)
Q_PROPERTY(bool sliding READ sliding WRITE setSliding NOTIFY slidingChanged)
Q_PROPERTY(
qreal smoothing READ smoothing WRITE setSmoothing NOTIFY
smoothingChanged)
Q_PROPERTY(
bool curveSmoothing READ curveSmoothing WRITE setCurveSmoothing NOTIFY
curveSmoothingChanged)
public:
explicit SparklineItem(QQuickItem* parent = nullptr);
void paint(QPainter* painter) override;
[[nodiscard]] internal::CircularBuffer* line1() const;
void setLine1(internal::CircularBuffer* buffer);
[[nodiscard]] CircularBuffer* line1() const;
void setLine1(CircularBuffer* buffer);
[[nodiscard]] CircularBuffer* line2() const;
void setLine2(CircularBuffer* buffer);
[[nodiscard]] internal::CircularBuffer* line2() const;
void setLine2(internal::CircularBuffer* buffer);
[[nodiscard]] QColor line1Color() const;
void setLine1Color(const QColor& color);
@@ -76,6 +86,15 @@ class SparklineItem : public QQuickPaintedItem {
[[nodiscard]] qreal lineWidth() const;
void setLineWidth(qreal width);
[[nodiscard]] bool sliding() const;
void setSliding(bool sliding);
[[nodiscard]] qreal smoothing() const;
void setSmoothing(qreal smoothing);
[[nodiscard]] bool curveSmoothing() const;
void setCurveSmoothing(bool curveSmoothing);
signals:
void line1Changed();
void line2Changed();
@@ -87,17 +106,21 @@ class SparklineItem : public QQuickPaintedItem {
void slideProgressChanged();
void historyLengthChanged();
void lineWidthChanged();
void slidingChanged();
void smoothingChanged();
void curveSmoothingChanged();
protected:
void geometryChange(
const QRectF& newGeometry, const QRectF& oldGeometry) override;
private:
void drawLine(
QPainter* painter,
CircularBuffer* buffer,
const QColor& color,
qreal fillAlpha);
void connectBuffer(CircularBuffer* buffer);
void updateContentOffset();
void applySmoothingRadius(internal::CircularBuffer* buffer) const;
CircularBuffer* m_line1 = nullptr;
CircularBuffer* m_line2 = nullptr;
SparklineContent* m_content = nullptr;
internal::CircularBuffer* m_line1 = nullptr;
internal::CircularBuffer* m_line2 = nullptr;
QColor m_line1Color;
QColor m_line2Color;
qreal m_line1FillAlpha = 0.15;
@@ -106,6 +129,9 @@ class SparklineItem : public QQuickPaintedItem {
qreal m_slideProgress = 0.0;
int m_historyLength = 30;
qreal m_lineWidth = 2.0;
bool m_sliding = true;
qreal m_smoothing = 0.0;
bool m_curveSmoothing = false;
};
} // namespace ZShell::internal
} // namespace ZShell::components
@@ -0,0 +1,447 @@
#include "sparklinesceneitem.hpp"
#include <qmath.h>
#include <qpainterpath.h>
#include <qsgflatcolormaterial.h>
#include <qsggeometry.h>
namespace ZShell::components {
SparklineSceneItem::SparklineSceneItem(QQuickItem* parent)
: QQuickItem(parent) {
setClip(true);
setFlag(QQuickItem::ItemHasContents, true);
}
void SparklineSceneItem::geometryChange(
const QRectF& newGeometry, const QRectF& oldGeometry) {
QQuickItem::geometryChange(newGeometry, oldGeometry);
if (newGeometry.size() != oldGeometry.size()) invalidateGeometry();
}
void SparklineSceneItem::invalidateGeometry() {
m_geometryDirty = true;
update();
}
void SparklineSceneItem::applySmoothingRadius(
internal::CircularBuffer* buffer) const {
if (buffer) buffer->setSmoothingRadius(qRound(m_smoothing));
}
void SparklineSceneItem::connectBuffer(internal::CircularBuffer* buffer) {
if (!buffer) return;
connect(
buffer,
&internal::CircularBuffer::valuesChanged,
this,
&SparklineSceneItem::invalidateGeometry);
connect(buffer, &QObject::destroyed, this, [this, buffer]() {
if (m_line1 == buffer) {
m_line1 = nullptr;
emit line1Changed();
}
if (m_line2 == buffer) {
m_line2 = nullptr;
emit line2Changed();
}
invalidateGeometry();
});
}
qreal SparklineSceneItem::stepX() const {
if (m_historyLength < 2 || width() <= 0.0) return 0.0;
if (m_sliding) return width() / static_cast<qreal>(m_historyLength - 1);
const int len = qMax(
2,
(m_line1 && m_line1->count() >= 2) ? m_line1->count()
: (m_line2 ? m_line2->count() : 2));
return width() / static_cast<qreal>(len - 1);
}
SparklineSceneItem::LineSample SparklineSceneItem::buildSample(
internal::CircularBuffer* buffer) const {
LineSample sample;
if (!buffer || buffer->count() < 2 || m_maxValue <= 0.0 || width() <= 0.0)
return sample;
if (m_historyLength < 2) return sample;
const int len = buffer->count();
const qreal step = stepX();
if (step <= 0.0) return sample;
qreal startX = m_sliding ? (width() - (len - 1) * step + step) : 0.0;
const qreal h = height();
const qreal strokePad = qCeil(m_lineWidth / 2);
const qreal curvePad = 3.0;
const qreal plotTop = strokePad + curvePad;
const qreal plotBottom = h - strokePad;
const qreal plotH = qMax<qreal>(1.0, plotBottom - plotTop);
QVector<QPointF> points;
points.reserve(len);
for (int i = 0; i < len; ++i) {
// Already smoothed once, incrementally, by the buffer itself.
const qreal value =
qBound<qreal>(0.0, buffer->smoothedAt(i), m_maxValue);
const qreal x = startX + i * step;
const qreal y = plotTop + (1.0 - (value / m_maxValue)) * plotH;
points.append(QPointF(x, y));
}
QPainterPath path;
path.moveTo(points[0]);
if (m_curveSmoothing && points.size() > 2) {
const qreal tension = 1.0;
const qreal k = tension / 6.0;
const int n = points.size();
for (int i = 0; i < n - 1; ++i) {
const QPointF& p0 = points[qMax(i - 1, 0)];
const QPointF& p1 = points[i];
const QPointF& p2 = points[i + 1];
const QPointF& p3 = points[qMin(i + 2, n - 1)];
QPointF c1 = p1 + (p2 - p0) * k;
QPointF c2 = p2 - (p3 - p1) * k;
const qreal segMinY = qMin(p1.y(), p2.y());
const qreal segMaxY = qMax(p1.y(), p2.y());
c1.setY(qBound(segMinY, c1.y(), segMaxY));
c2.setY(qBound(segMinY, c2.y(), segMaxY));
path.cubicTo(c1, c2, p2);
}
} else {
for (int i = 0; i < points.size() - 1; ++i) {
const QPointF& p0 = points[i];
const QPointF& p1 = points[i + 1];
const qreal ctrlX = (p0.x() + p1.x()) * 0.5;
path.cubicTo(QPointF(ctrlX, p0.y()), QPointF(ctrlX, p1.y()), p1);
}
}
// Flatten the curve to a polyline once, here, on data change only.
// This is CPU math only -- no QPainter draw calls, no image, no
// texture upload happen as a result of this.
const auto subpaths = path.toSubpathPolygons();
if (subpaths.isEmpty()) return sample;
sample.polyline = subpaths.first();
sample.fillBottom = h;
sample.hasData = sample.polyline.size() >= 2;
return sample;
}
void SparklineSceneItem::writeFillGeometry(
QSGGeometryNode* node, const LineSample& sample) {
const int n = sample.polyline.size();
auto* geometry = node->geometry();
geometry->allocate(n * 2);
geometry->setDrawingMode(QSGGeometry::DrawTriangleStrip);
auto* vertices = geometry->vertexDataAsPoint2D();
for (int i = 0; i < n; ++i) {
const QPointF& p = sample.polyline[i];
vertices[i * 2].set(p.x(), p.y());
vertices[i * 2 + 1].set(p.x(), sample.fillBottom);
}
node->markDirty(QSGNode::DirtyGeometry);
}
void SparklineSceneItem::writeStrokeGeometry(
QSGGeometryNode* node, const LineSample& sample, qreal lineWidth) {
const int n = sample.polyline.size();
auto* geometry = node->geometry();
geometry->allocate(n * 2);
geometry->setDrawingMode(QSGGeometry::DrawTriangleStrip);
auto* vertices = geometry->vertexDataAsPoint2D();
const qreal half = lineWidth * 0.5;
for (int i = 0; i < n; ++i) {
QPointF dir;
if (i == 0)
dir = sample.polyline[1] - sample.polyline[0];
else if (i == n - 1)
dir = sample.polyline[n - 1] - sample.polyline[n - 2];
else
dir = sample.polyline[i + 1] - sample.polyline[i - 1];
const qreal len = std::hypot(dir.x(), dir.y());
QPointF normal = len > 1e-6 ? QPointF(-dir.y() / len, dir.x() / len)
: QPointF(0, 1);
const QPointF& p = sample.polyline[i];
const QPointF offset = normal * half;
vertices[i * 2].set(p.x() + offset.x(), p.y() + offset.y());
vertices[i * 2 + 1].set(p.x() - offset.x(), p.y() - offset.y());
}
node->markDirty(QSGNode::DirtyGeometry);
}
void SparklineSceneItem::setMaterialColor(
QSGGeometryNode* node, const QColor& color, qreal alpha) {
auto* material = static_cast<QSGFlatColorMaterial*>(node->material());
QColor c = color;
c.setAlphaF(static_cast<float>(alpha));
material->setColor(c);
node->markDirty(QSGNode::DirtyMaterial);
}
QSGNode* SparklineSceneItem::updatePaintNode(
QSGNode* node, UpdatePaintNodeData*) {
bool samplesRebuilt = false;
if (m_geometryDirty) {
m_sample1 = buildSample(m_line1);
m_sample2 = buildSample(m_line2);
m_geometryDirty = false;
samplesRebuilt = true;
}
const bool hasAny = m_sample1.hasData || m_sample2.hasData;
if (!hasAny) {
delete node;
m_transformNode = nullptr;
m_line1FillNode = m_line1StrokeNode = nullptr;
m_line2FillNode = m_line2StrokeNode = nullptr;
return nullptr;
}
auto* transformNode = static_cast<QSGTransformNode*>(node);
bool justCreated = false;
if (!transformNode) {
transformNode = new QSGTransformNode();
m_transformNode = transformNode;
auto makeNode = [] {
auto* n = new QSGGeometryNode();
n->setGeometry(
new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 0));
n->setFlag(QSGNode::OwnsGeometry);
n->setMaterial(new QSGFlatColorMaterial());
n->setFlag(QSGNode::OwnsMaterial);
return n;
};
m_line1FillNode = makeNode();
m_line1StrokeNode = makeNode();
m_line2FillNode = makeNode();
m_line2StrokeNode = makeNode();
transformNode->appendChildNode(m_line1FillNode);
transformNode->appendChildNode(m_line1StrokeNode);
transformNode->appendChildNode(m_line2FillNode);
transformNode->appendChildNode(m_line2StrokeNode);
justCreated = true;
}
QMatrix4x4 matrix;
matrix.translate(-stepX() * m_slideProgress, 0);
transformNode->setMatrix(matrix);
const bool needsGeometryWrite = samplesRebuilt || justCreated;
const bool needsColorWrite = m_colorsDirty || justCreated;
auto writeLine = [&](QSGGeometryNode* fillNode,
QSGGeometryNode* strokeNode,
const LineSample& sample,
const QColor& color,
qreal fillAlpha) {
if (needsGeometryWrite) {
if (sample.hasData) {
writeFillGeometry(fillNode, sample);
writeStrokeGeometry(strokeNode, sample, m_lineWidth);
} else {
fillNode->geometry()->allocate(0);
strokeNode->geometry()->allocate(0);
fillNode->markDirty(QSGNode::DirtyGeometry);
strokeNode->markDirty(QSGNode::DirtyGeometry);
}
}
if (needsColorWrite) {
setMaterialColor(fillNode, color, fillAlpha);
setMaterialColor(strokeNode, color, 1.0);
}
};
writeLine(
m_line1FillNode,
m_line1StrokeNode,
m_sample1,
m_line1Color,
m_line1FillAlpha);
writeLine(
m_line2FillNode,
m_line2StrokeNode,
m_sample2,
m_line2Color,
m_line2FillAlpha);
m_colorsDirty = false;
return transformNode;
}
internal::CircularBuffer* SparklineSceneItem::line1() const {
return m_line1;
}
void SparklineSceneItem::setLine1(internal::CircularBuffer* buffer) {
if (m_line1 == buffer) return;
if (m_line1) disconnect(m_line1, nullptr, this, nullptr);
m_line1 = buffer;
connectBuffer(buffer);
applySmoothingRadius(buffer);
invalidateGeometry();
emit line1Changed();
}
internal::CircularBuffer* SparklineSceneItem::line2() const {
return m_line2;
}
void SparklineSceneItem::setLine2(internal::CircularBuffer* buffer) {
if (m_line2 == buffer) return;
if (m_line2) disconnect(m_line2, nullptr, this, nullptr);
m_line2 = buffer;
connectBuffer(buffer);
applySmoothingRadius(buffer);
invalidateGeometry();
emit line2Changed();
}
QColor SparklineSceneItem::line1Color() const {
return m_line1Color;
}
void SparklineSceneItem::setLine1Color(const QColor& color) {
if (m_line1Color == color) return;
m_line1Color = color;
m_colorsDirty = true;
update();
emit line1ColorChanged();
}
QColor SparklineSceneItem::line2Color() const {
return m_line2Color;
}
void SparklineSceneItem::setLine2Color(const QColor& color) {
if (m_line2Color == color) return;
m_line2Color = color;
m_colorsDirty = true;
update();
emit line2ColorChanged();
}
qreal SparklineSceneItem::line1FillAlpha() const {
return m_line1FillAlpha;
}
void SparklineSceneItem::setLine1FillAlpha(qreal alpha) {
if (qFuzzyCompare(m_line1FillAlpha, alpha)) return;
m_line1FillAlpha = alpha;
m_colorsDirty = true;
update();
emit line1FillAlphaChanged();
}
qreal SparklineSceneItem::line2FillAlpha() const {
return m_line2FillAlpha;
}
void SparklineSceneItem::setLine2FillAlpha(qreal alpha) {
if (qFuzzyCompare(m_line2FillAlpha, alpha)) return;
m_line2FillAlpha = alpha;
m_colorsDirty = true;
update();
emit line2FillAlphaChanged();
}
qreal SparklineSceneItem::maxValue() const {
return m_maxValue;
}
void SparklineSceneItem::setMaxValue(qreal value) {
if (qFuzzyCompare(m_maxValue, value)) return;
m_maxValue = value;
invalidateGeometry();
emit maxValueChanged();
}
qreal SparklineSceneItem::slideProgress() const {
return m_slideProgress;
}
void SparklineSceneItem::setSlideProgress(qreal progress) {
if (qFuzzyCompare(m_slideProgress, progress)) return;
m_slideProgress = progress;
update();
emit slideProgressChanged();
}
int SparklineSceneItem::historyLength() const {
return m_historyLength;
}
void SparklineSceneItem::setHistoryLength(int length) {
if (m_historyLength == length) return;
m_historyLength = length;
invalidateGeometry();
emit historyLengthChanged();
}
qreal SparklineSceneItem::lineWidth() const {
return m_lineWidth;
}
void SparklineSceneItem::setLineWidth(qreal width) {
if (qFuzzyCompare(m_lineWidth, width)) return;
m_lineWidth = width;
invalidateGeometry();
emit lineWidthChanged();
}
bool SparklineSceneItem::sliding() const {
return m_sliding;
}
void SparklineSceneItem::setSliding(bool sliding) {
if (m_sliding == sliding) return;
m_sliding = sliding;
invalidateGeometry();
emit slidingChanged();
}
qreal SparklineSceneItem::smoothing() const {
return m_smoothing;
}
void SparklineSceneItem::setSmoothing(qreal smoothing) {
smoothing = qMax<qreal>(0.0, smoothing);
if (qFuzzyCompare(m_smoothing, smoothing)) return;
m_smoothing = smoothing;
applySmoothingRadius(m_line1);
applySmoothingRadius(m_line2);
emit smoothingChanged();
}
bool SparklineSceneItem::curveSmoothing() const {
return m_curveSmoothing;
}
void SparklineSceneItem::setCurveSmoothing(bool enabled) {
if (m_curveSmoothing == enabled) return;
m_curveSmoothing = enabled;
invalidateGeometry();
emit curveSmoothingChanged();
}
} // namespace ZShell::components
@@ -0,0 +1,167 @@
#pragma once
#include <qcolor.h>
#include <qmatrix4x4.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qquickitem.h>
#include <qtmetamacros.h>
#include <qvector.h>
#include "internal/circularbuffer.hpp"
class QSGGeometryNode;
class QSGTransformNode;
namespace ZShell::components {
class SparklineSceneItem : public QQuickItem {
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(
internal::CircularBuffer* line1 READ line1 WRITE setLine1 NOTIFY
line1Changed)
Q_PROPERTY(
internal::CircularBuffer* line2 READ line2 WRITE setLine2 NOTIFY
line2Changed)
Q_PROPERTY(
QColor line1Color READ line1Color WRITE setLine1Color NOTIFY
line1ColorChanged)
Q_PROPERTY(
QColor line2Color READ line2Color WRITE setLine2Color NOTIFY
line2ColorChanged)
Q_PROPERTY(
qreal line1FillAlpha READ line1FillAlpha WRITE setLine1FillAlpha NOTIFY
line1FillAlphaChanged)
Q_PROPERTY(
qreal line2FillAlpha READ line2FillAlpha WRITE setLine2FillAlpha NOTIFY
line2FillAlphaChanged)
Q_PROPERTY(
qreal maxValue READ maxValue WRITE setMaxValue NOTIFY maxValueChanged)
Q_PROPERTY(
qreal slideProgress READ slideProgress WRITE setSlideProgress NOTIFY
slideProgressChanged)
Q_PROPERTY(
int historyLength READ historyLength WRITE setHistoryLength NOTIFY
historyLengthChanged)
Q_PROPERTY(
qreal lineWidth READ lineWidth WRITE setLineWidth NOTIFY
lineWidthChanged)
Q_PROPERTY(bool sliding READ sliding WRITE setSliding NOTIFY slidingChanged)
Q_PROPERTY(
qreal smoothing READ smoothing WRITE setSmoothing NOTIFY
smoothingChanged)
Q_PROPERTY(
bool curveSmoothing READ curveSmoothing WRITE setCurveSmoothing NOTIFY
curveSmoothingChanged)
public:
explicit SparklineSceneItem(QQuickItem* parent = nullptr);
[[nodiscard]] internal::CircularBuffer* line1() const;
void setLine1(internal::CircularBuffer* buffer);
[[nodiscard]] internal::CircularBuffer* line2() const;
void setLine2(internal::CircularBuffer* buffer);
[[nodiscard]] QColor line1Color() const;
void setLine1Color(const QColor& color);
[[nodiscard]] QColor line2Color() const;
void setLine2Color(const QColor& color);
[[nodiscard]] qreal line1FillAlpha() const;
void setLine1FillAlpha(qreal alpha);
[[nodiscard]] qreal line2FillAlpha() const;
void setLine2FillAlpha(qreal alpha);
[[nodiscard]] qreal maxValue() const;
void setMaxValue(qreal value);
[[nodiscard]] qreal slideProgress() const;
void setSlideProgress(qreal progress);
[[nodiscard]] int historyLength() const;
void setHistoryLength(int length);
[[nodiscard]] qreal lineWidth() const;
void setLineWidth(qreal width);
[[nodiscard]] bool sliding() const;
void setSliding(bool sliding);
[[nodiscard]] qreal smoothing() const;
void setSmoothing(qreal smoothing);
[[nodiscard]] bool curveSmoothing() const;
void setCurveSmoothing(bool curveSmoothing);
signals:
void line1Changed();
void line2Changed();
void line1ColorChanged();
void line2ColorChanged();
void line1FillAlphaChanged();
void line2FillAlphaChanged();
void maxValueChanged();
void slideProgressChanged();
void historyLengthChanged();
void lineWidthChanged();
void slidingChanged();
void smoothingChanged();
void curveSmoothingChanged();
protected:
QSGNode* updatePaintNode(QSGNode* node, UpdatePaintNodeData* data) override;
void geometryChange(
const QRectF& newGeometry, const QRectF& oldGeometry) override;
private:
struct LineSample {
QVector<QPointF> polyline; // local coords, slideProgress baked to 0
qreal fillBottom = 0.0;
bool hasData = false;
};
void connectBuffer(internal::CircularBuffer* buffer);
void invalidateGeometry();
void applySmoothingRadius(internal::CircularBuffer* buffer) const;
[[nodiscard]] qreal stepX() const;
[[nodiscard]] LineSample buildSample(internal::CircularBuffer* buffer) const;
static void writeFillGeometry(
QSGGeometryNode* node, const LineSample& sample);
static void writeStrokeGeometry(
QSGGeometryNode* node, const LineSample& sample, qreal lineWidth);
static void setMaterialColor(
QSGGeometryNode* node, const QColor& color, qreal alpha);
internal::CircularBuffer* m_line1 = nullptr;
internal::CircularBuffer* m_line2 = nullptr;
QColor m_line1Color;
QColor m_line2Color;
qreal m_line1FillAlpha = 0.15;
qreal m_line2FillAlpha = 0.2;
qreal m_maxValue = 1024.0;
qreal m_slideProgress = 0.0;
int m_historyLength = 30;
qreal m_lineWidth = 2.0;
bool m_sliding = true;
qreal m_smoothing = 0.0;
bool m_curveSmoothing = false;
LineSample m_sample1;
LineSample m_sample2;
bool m_geometryDirty = true;
bool m_colorsDirty = true;
QSGTransformNode* m_transformNode = nullptr;
QSGGeometryNode* m_line1FillNode = nullptr;
QSGGeometryNode* m_line1StrokeNode = nullptr;
QSGGeometryNode* m_line2FillNode = nullptr;
QSGGeometryNode* m_line2StrokeNode = nullptr;
};
} // namespace ZShell::components
+16 -1
View File
@@ -1,19 +1,27 @@
qml_module(ZShell-config
URI ZShell.Config
INCLUDE_PREFIX config
DEPENDENCIES
ZShell.Settings
SOURCES
confignode.hpp confignode.cpp
configobject.hpp configobject.cpp
configlist.hpp configlist.cpp
config.hpp config.cpp
migration.hpp migration.cpp
anim.hpp anim.cpp
fontbuilder.hpp fontbuilder.cpp
font.hpp font.cpp
tokens.hpp
appearance.hpp
appearance.hpp appearance.cpp
background.hpp
bar.hpp
clipboard.hpp
colors.hpp
dashboard.hpp
display.hpp
dock.hpp
enums.hpp
general.hpp
launcher.hpp
llm.hpp
@@ -25,3 +33,10 @@ qml_module(ZShell-config
sidebar.hpp
utilities.hpp
)
target_link_libraries(ZShell-config PUBLIC
Qt::Gui
zshell-util
ZShell
ZShell-settings
)
+83 -16
View File
@@ -1,32 +1,82 @@
#include "anim.hpp"
#include "tokens.hpp"
#include <qpoint.h>
#include "appearance.hpp"
#include "tokens.hpp"
namespace ZShell::config {
QEasingCurve AnimEasing::buildCurve(const QList<qreal>& points) {
AnimTokens::AnimTokens(QObject* parent) : QObject(parent) {}
QEasingCurve AnimTokens::emphasized() const {
return m_emphasized;
}
QEasingCurve AnimTokens::emphasizedAccel() const {
return m_emphasizedAccel;
}
QEasingCurve AnimTokens::emphasizedDecel() const {
return m_emphasizedDecel;
}
QEasingCurve AnimTokens::standard() const {
return m_standard;
}
QEasingCurve AnimTokens::standardAccel() const {
return m_standardAccel;
}
QEasingCurve AnimTokens::standardDecel() const {
return m_standardDecel;
}
QEasingCurve AnimTokens::expressiveFastSpatial() const {
return m_expressiveFastSpatial;
}
QEasingCurve AnimTokens::expressiveDefaultSpatial() const {
return m_expressiveDefaultSpatial;
}
QEasingCurve AnimTokens::expressiveSlowSpatial() const {
return m_expressiveSlowSpatial;
}
QEasingCurve AnimTokens::expressiveFastEffects() const {
return m_expressiveFastEffects;
}
QEasingCurve AnimTokens::expressiveDefaultEffects() const {
return m_expressiveDefaultEffects;
}
QEasingCurve AnimTokens::expressiveSlowEffects() const {
return m_expressiveSlowEffects;
}
AnimDurations* AnimTokens::durations() const {
return m_durations;
}
QEasingCurve AnimTokens::buildCurve(const QList<qreal>& points) {
QEasingCurve curve(QEasingCurve::BezierSpline);
for (int i = 0; i + 5 < points.size(); i += 6) {
QPointF c1(points[i], points[i + 1]);
QPointF c2(points[i + 2], points[i + 3]);
QPointF end(points[i + 4], points[i + 5]);
const QPointF c1(points[i], points[i + 1]);
const QPointF c2(points[i + 2], points[i + 3]);
const QPointF end(points[i + 4], points[i + 5]);
curve.addCubicBezierSegment(c1, c2, end);
}
return curve;
}
void AnimEasing::bindCurves(AnimCurves* curves) {
m_curves = curves;
connect(
curves,
&AnimCurves::propertiesChanged,
this,
&AnimEasing::rebuildCurves);
rebuildCurves();
}
void AnimEasing::rebuildCurves() {
void AnimTokens::rebuildCurves() {
if (!m_curves) return;
m_emphasized = buildCurve(m_curves->emphasized());
m_emphasizedAccel = buildCurve(m_curves->emphasizedAccel());
m_emphasizedDecel = buildCurve(m_curves->emphasizedDecel());
@@ -41,7 +91,24 @@ void AnimEasing::rebuildCurves() {
m_expressiveDefaultEffects =
buildCurve(m_curves->expressiveDefaultEffects());
m_expressiveSlowEffects = buildCurve(m_curves->expressiveSlowEffects());
emit curvesChanged();
}
void AnimTokens::bindCurves(AnimCurves* curves) {
m_curves = curves;
connect(
curves, &AnimCurves::optionChanged, this, &AnimTokens::rebuildCurves);
rebuildCurves();
}
void AnimTokens::bindDurations(AnimDurations* durations) {
if (m_durations == durations) return;
m_durations = durations;
emit durationsChanged();
}
} // namespace ZShell::config
+38 -37
View File
@@ -9,8 +9,10 @@ namespace ZShell::config {
class AnimCurves;
class AnimDurations;
class AnimEasing : public QObject {
class AnimTokens : public QObject {
Q_OBJECT
Q_MOC_INCLUDE("tokens.hpp")
Q_MOC_INCLUDE("appearance.hpp")
QML_ANONYMOUS
Q_PROPERTY(QEasingCurve emphasized READ emphasized NOTIFY curvesChanged)
@@ -42,54 +44,53 @@ class AnimEasing : public QObject {
QEasingCurve expressiveSlowEffects READ expressiveSlowEffects NOTIFY
curvesChanged)
Q_PROPERTY(
ZShell::config::AnimDurations* durations READ durations NOTIFY
durationsChanged)
public:
explicit AnimEasing(QObject* parent = nullptr) : QObject(parent) {}
explicit AnimTokens(QObject* parent = nullptr);
void bindCurves(AnimCurves* curves);
void bindDurations(AnimDurations* durations);
[[nodiscard]] QEasingCurve emphasized() const { return m_emphasized; }
[[nodiscard]] QEasingCurve emphasizedAccel() const {
return m_emphasizedAccel;
}
[[nodiscard]] QEasingCurve emphasizedDecel() const {
return m_emphasizedDecel;
}
[[nodiscard]] QEasingCurve standard() const { return m_standard; }
[[nodiscard]] QEasingCurve standardAccel() const { return m_standardAccel; }
[[nodiscard]] QEasingCurve standardDecel() const { return m_standardDecel; }
[[nodiscard]] QEasingCurve expressiveFastSpatial() const {
return m_expressiveFastSpatial;
}
[[nodiscard]] QEasingCurve expressiveDefaultSpatial() const {
return m_expressiveDefaultSpatial;
}
[[nodiscard]] QEasingCurve expressiveSlowSpatial() const {
return m_expressiveSlowSpatial;
}
[[nodiscard]] QEasingCurve expressiveFastEffects() const {
return m_expressiveFastEffects;
}
[[nodiscard]] QEasingCurve expressiveDefaultEffects() const {
return m_expressiveDefaultEffects;
}
[[nodiscard]] QEasingCurve expressiveSlowEffects() const {
return m_expressiveSlowEffects;
}
[[nodiscard]] QEasingCurve emphasized() const;
[[nodiscard]] QEasingCurve emphasizedAccel() const;
[[nodiscard]] QEasingCurve emphasizedDecel() const;
[[nodiscard]] QEasingCurve standard() const;
[[nodiscard]] QEasingCurve standardAccel() const;
[[nodiscard]] QEasingCurve standardDecel() const;
[[nodiscard]] QEasingCurve expressiveFastSpatial() const;
[[nodiscard]] QEasingCurve expressiveDefaultSpatial() const;
[[nodiscard]] QEasingCurve expressiveSlowSpatial() const;
[[nodiscard]] QEasingCurve expressiveFastEffects() const;
[[nodiscard]] QEasingCurve expressiveDefaultEffects() const;
[[nodiscard]] QEasingCurve expressiveSlowEffects() const;
[[nodiscard]] AnimDurations* durations() const;
signals:
void curvesChanged();
void durationsChanged();
private:
static QEasingCurve buildCurve(const QList<qreal>& points);
void rebuildCurves();
static QEasingCurve buildCurve(const QList<qreal>& points);
AnimCurves* m_curves = nullptr;
QEasingCurve m_emphasized, m_emphasizedAccel, m_emphasizedDecel;
QEasingCurve m_standard, m_standardAccel, m_standardDecel;
QEasingCurve m_expressiveFastSpatial, m_expressiveDefaultSpatial,
m_expressiveSlowSpatial;
QEasingCurve m_expressiveFastEffects, m_expressiveDefaultEffects,
m_expressiveSlowEffects;
AnimDurations* m_durations = nullptr;
QEasingCurve m_emphasized;
QEasingCurve m_emphasizedAccel;
QEasingCurve m_emphasizedDecel;
QEasingCurve m_standard;
QEasingCurve m_standardAccel;
QEasingCurve m_standardDecel;
QEasingCurve m_expressiveFastSpatial;
QEasingCurve m_expressiveDefaultSpatial;
QEasingCurve m_expressiveSlowSpatial;
QEasingCurve m_expressiveFastEffects;
QEasingCurve m_expressiveDefaultEffects;
QEasingCurve m_expressiveSlowEffects;
};
} // namespace ZShell::config
+219
View File
@@ -0,0 +1,219 @@
#include "appearance.hpp"
#include "tokens.hpp"
namespace ZShell::config {
namespace {
template <typename Source, typename Target>
void connectTokenSignals(Source* source, Target* target) {
const auto* meta = source->metaObject();
for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) {
auto prop = meta->property(i);
if (prop.hasNotifySignal())
QObject::connect(
source,
prop.notifySignal(),
target,
target->metaObject()->method(
target->metaObject()->indexOfSignal("valuesChanged()")));
}
QObject::connect(
target, &Target::scaleChanged, target, &Target::valuesChanged);
}
} // namespace
void AppearanceRounding::bindTokens(RoundingTokens* tokens) {
m_tokens = tokens;
connectTokenSignals(tokens, this);
}
int AppearanceRounding::extraSmall() const {
return m_tokens ? static_cast<int>(m_tokens->extraSmall() * m_scale) : 0;
}
int AppearanceRounding::small() const {
return m_tokens ? static_cast<int>(m_tokens->small() * m_scale) : 0;
}
int AppearanceRounding::medium() const {
return m_tokens ? static_cast<int>(m_tokens->medium() * m_scale) : 0;
}
int AppearanceRounding::large() const {
return m_tokens ? static_cast<int>(m_tokens->large() * m_scale) : 0;
}
int AppearanceRounding::largeIncreased() const {
return m_tokens ? static_cast<int>(m_tokens->largeIncreased() * m_scale)
: 0;
}
int AppearanceRounding::extraLarge() const {
return m_tokens ? static_cast<int>(m_tokens->extraLarge() * m_scale) : 0;
}
int AppearanceRounding::extraLargeIncreased() const {
return m_tokens
? static_cast<int>(m_tokens->extraLargeIncreased() * m_scale)
: 0;
}
int AppearanceRounding::extraExtraLarge() const {
return m_tokens ? static_cast<int>(m_tokens->extraExtraLarge() * m_scale)
: 0;
}
int AppearanceRounding::full() const {
return m_tokens ? m_tokens->full() : 0;
}
void AppearanceSpacing::bindTokens(SpacingTokens* tokens) {
m_tokens = tokens;
connectTokenSignals(tokens, this);
}
int AppearanceSpacing::extraSmall() const {
return m_tokens ? static_cast<int>(m_tokens->extraSmall() * m_scale) : 0;
}
int AppearanceSpacing::small() const {
return m_tokens ? static_cast<int>(m_tokens->small() * m_scale) : 0;
}
int AppearanceSpacing::medium() const {
return m_tokens ? static_cast<int>(m_tokens->medium() * m_scale) : 0;
}
int AppearanceSpacing::large() const {
return m_tokens ? static_cast<int>(m_tokens->large() * m_scale) : 0;
}
int AppearanceSpacing::largeIncreased() const {
return m_tokens ? static_cast<int>(m_tokens->largeIncreased() * m_scale)
: 0;
}
int AppearanceSpacing::extraLarge() const {
return m_tokens ? static_cast<int>(m_tokens->extraLarge() * m_scale) : 0;
}
int AppearanceSpacing::extraLargeIncreased() const {
return m_tokens
? static_cast<int>(m_tokens->extraLargeIncreased() * m_scale)
: 0;
}
int AppearanceSpacing::extraExtraLarge() const {
return m_tokens ? static_cast<int>(m_tokens->extraExtraLarge() * m_scale)
: 0;
}
void AppearancePadding::bindTokens(PaddingTokens* tokens) {
m_tokens = tokens;
connectTokenSignals(tokens, this);
}
int AppearancePadding::extraSmall() const {
return m_tokens ? static_cast<int>(m_tokens->extraSmall() * m_scale) : 0;
}
int AppearancePadding::smaller() const {
return m_tokens ? static_cast<int>(m_tokens->smaller() * m_scale) : 0;
}
int AppearancePadding::small() const {
return m_tokens ? static_cast<int>(m_tokens->small() * m_scale) : 0;
}
int AppearancePadding::medium() const {
return m_tokens ? static_cast<int>(m_tokens->medium() * m_scale) : 0;
}
int AppearancePadding::large() const {
return m_tokens ? static_cast<int>(m_tokens->large() * m_scale) : 0;
}
int AppearancePadding::largeIncreased() const {
return m_tokens ? static_cast<int>(m_tokens->largeIncreased() * m_scale)
: 0;
}
int AppearancePadding::extraLarge() const {
return m_tokens ? static_cast<int>(m_tokens->extraLarge() * m_scale) : 0;
}
int AppearancePadding::extraLargeIncreased() const {
return m_tokens
? static_cast<int>(m_tokens->extraLargeIncreased() * m_scale)
: 0;
}
int AppearancePadding::extraExtraLarge() const {
return m_tokens ? static_cast<int>(m_tokens->extraExtraLarge() * m_scale)
: 0;
}
void AnimDurations::bindTokens(AnimDurationTokens* tokens) {
m_tokens = tokens;
connectTokenSignals(tokens, this);
}
int AnimDurations::small() const {
return m_tokens ? static_cast<int>(m_tokens->small() * m_scale) : 0;
}
int AnimDurations::normal() const {
return m_tokens ? static_cast<int>(m_tokens->normal() * m_scale) : 0;
}
int AnimDurations::large() const {
return m_tokens ? static_cast<int>(m_tokens->large() * m_scale) : 0;
}
int AnimDurations::extraLarge() const {
return m_tokens ? static_cast<int>(m_tokens->extraLarge() * m_scale) : 0;
}
int AnimDurations::expressiveFastSpatial() const {
return m_tokens
? static_cast<int>(m_tokens->expressiveFastSpatial() * m_scale)
: 0;
}
int AnimDurations::expressiveDefaultSpatial() const {
return m_tokens ? static_cast<int>(
m_tokens->expressiveDefaultSpatial() * m_scale)
: 0;
}
int AnimDurations::expressiveSlowSpatial() const {
return m_tokens
? static_cast<int>(m_tokens->expressiveSlowSpatial() * m_scale)
: 0;
}
int AnimDurations::expressiveFastEffects() const {
return m_tokens
? static_cast<int>(m_tokens->expressiveFastEffects() * m_scale)
: 0;
}
int AnimDurations::expressiveDefaultEffects() const {
return m_tokens ? static_cast<int>(
m_tokens->expressiveDefaultEffects() * m_scale)
: 0;
}
int AnimDurations::expressiveSlowEffects() const {
return m_tokens
? static_cast<int>(m_tokens->expressiveSlowEffects() * m_scale)
: 0;
}
} // namespace ZShell::config
+312 -48
View File
@@ -1,83 +1,347 @@
#pragma once
#include "configobject.hpp"
#include <qqmlregistration.h>
#include <qfont.h>
#include <qstring.h>
#include <qvariantmap.h>
#include "settings/objectnode.hpp"
#include "common.hpp"
namespace ZShell::config {
class Anim : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
using Qt::StringLiterals::operator""_s;
CFG_PROPERTY(int, mediaGifSpeedAdjustment, 300)
CFG_PROPERTY(qreal, sessionGifSpeed, 0.7)
class RoundingTokens;
class SpacingTokens;
class PaddingTokens;
class AnimDurationTokens;
class AppearanceRounding : public settings::ObjectNode {
CONFIG_NODE(AppearanceRounding, settings::ObjectNode)
CONFIG_PROPERTY(qreal, scale, 1)
Q_PROPERTY(int extraSmall READ extraSmall NOTIFY valuesChanged)
Q_PROPERTY(int small READ small NOTIFY valuesChanged)
Q_PROPERTY(int medium READ medium NOTIFY valuesChanged)
Q_PROPERTY(int large READ large NOTIFY valuesChanged)
Q_PROPERTY(int largeIncreased READ largeIncreased NOTIFY valuesChanged)
Q_PROPERTY(int extraLarge READ extraLarge NOTIFY valuesChanged)
Q_PROPERTY(
int extraLargeIncreased READ extraLargeIncreased NOTIFY valuesChanged)
Q_PROPERTY(int extraExtraLarge READ extraExtraLarge NOTIFY valuesChanged)
Q_PROPERTY(int full READ full NOTIFY valuesChanged)
public:
explicit Anim(QObject* parent = nullptr) : ConfigObject(parent) {}
void bindTokens(RoundingTokens* tokens);
[[nodiscard]] int extraSmall() const;
[[nodiscard]] int small() const;
[[nodiscard]] int medium() const;
[[nodiscard]] int large() const;
[[nodiscard]] int largeIncreased() const;
[[nodiscard]] int extraLarge() const;
[[nodiscard]] int extraLargeIncreased() const;
[[nodiscard]] int extraExtraLarge() const;
[[nodiscard]] int full() const;
signals:
void valuesChanged();
private:
RoundingTokens* m_tokens = nullptr;
};
class Deform : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class AppearanceSpacing : public settings::ObjectNode {
CONFIG_NODE(AppearanceSpacing, settings::ObjectNode)
CFG_PROPERTY(qreal, scale, 1)
CONFIG_PROPERTY(qreal, scale, 1)
Q_PROPERTY(int extraSmall READ extraSmall NOTIFY valuesChanged)
Q_PROPERTY(int small READ small NOTIFY valuesChanged)
Q_PROPERTY(int medium READ medium NOTIFY valuesChanged)
Q_PROPERTY(int large READ large NOTIFY valuesChanged)
Q_PROPERTY(int largeIncreased READ largeIncreased NOTIFY valuesChanged)
Q_PROPERTY(int extraLarge READ extraLarge NOTIFY valuesChanged)
Q_PROPERTY(
int extraLargeIncreased READ extraLargeIncreased NOTIFY valuesChanged)
Q_PROPERTY(int extraExtraLarge READ extraExtraLarge NOTIFY valuesChanged)
public:
explicit Deform(QObject* parent = nullptr) : ConfigObject(parent) {}
void bindTokens(SpacingTokens* tokens);
[[nodiscard]] int extraSmall() const;
[[nodiscard]] int small() const;
[[nodiscard]] int medium() const;
[[nodiscard]] int large() const;
[[nodiscard]] int largeIncreased() const;
[[nodiscard]] int extraLarge() const;
[[nodiscard]] int extraLargeIncreased() const;
[[nodiscard]] int extraExtraLarge() const;
signals:
void valuesChanged();
private:
SpacingTokens* m_tokens = nullptr;
};
class FontFamily : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class AppearancePadding : public settings::ObjectNode {
CONFIG_NODE(AppearancePadding, settings::ObjectNode)
CFG_PROPERTY(QString, clock, QStringLiteral("Rubik"))
CFG_PROPERTY(QString, material, QStringLiteral("Material Symbols Rounded"))
CFG_PROPERTY(QString, mono, QStringLiteral("CaskaydiaCove NF"))
CFG_PROPERTY(QString, sans, QStringLiteral("Segoe UI Variable Text"))
CONFIG_PROPERTY(qreal, scale, 1)
Q_PROPERTY(int extraSmall READ extraSmall NOTIFY valuesChanged)
Q_PROPERTY(int smaller READ smaller NOTIFY valuesChanged)
Q_PROPERTY(int small READ small NOTIFY valuesChanged)
Q_PROPERTY(int medium READ medium NOTIFY valuesChanged)
Q_PROPERTY(int large READ large NOTIFY valuesChanged)
Q_PROPERTY(int largeIncreased READ largeIncreased NOTIFY valuesChanged)
Q_PROPERTY(int extraLarge READ extraLarge NOTIFY valuesChanged)
Q_PROPERTY(
int extraLargeIncreased READ extraLargeIncreased NOTIFY valuesChanged)
Q_PROPERTY(int extraExtraLarge READ extraExtraLarge NOTIFY valuesChanged)
public:
explicit FontFamily(QObject* parent = nullptr) : ConfigObject(parent) {}
void bindTokens(PaddingTokens* tokens);
[[nodiscard]] int extraSmall() const;
[[nodiscard]] int smaller() const;
[[nodiscard]] int small() const;
[[nodiscard]] int medium() const;
[[nodiscard]] int large() const;
[[nodiscard]] int largeIncreased() const;
[[nodiscard]] int extraLarge() const;
[[nodiscard]] int extraLargeIncreased() const;
[[nodiscard]] int extraExtraLarge() const;
signals:
void valuesChanged();
private:
PaddingTokens* m_tokens = nullptr;
};
class Font : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
namespace detail {
struct FontConfig {
QString family = {};
int size;
int weight = QFont::Normal;
bool italic = false;
QVariantMap vaxes = {{u"ROND"_s, 25}};
};
} // namespace detail
class FontSizeNode : public settings::ObjectNode {
CONFIG_NODE(FontSizeNode, settings::ObjectNode)
};
class FontStyleNode : public settings::ObjectNode {
CONFIG_NODE(FontStyleNode, settings::ObjectNode)
public:
virtual FontSizeNode* largeNode() const = 0;
virtual FontSizeNode* mediumNode() const = 0;
virtual FontSizeNode* smallNode() const = 0;
virtual FontSizeNode* extraLargeNode() const { return nullptr; }
};
#define ARG(...) __VA_ARGS__
#define FONT(...) ZShell::config::detail::FontConfig __VA_ARGS__
#define FONT_CONFIG(Style, Size, props) \
class FontConfig##Style##Size : public FontSizeNode { \
CONFIG_NODE(FontConfig##Style##Size, FontSizeNode) \
\
CONFIG_PROPERTY( \
QString, \
family, \
ARG(props).family) /* Empty inherits the style family */ \
CONFIG_PROPERTY(int, size, ARG(props).size) \
CONFIG_PROPERTY(int, weight, ARG(props).weight) \
CONFIG_PROPERTY(bool, italic, ARG(props).italic) \
CONFIG_PROPERTY(QVariantMap, vaxes, ARG(props).vaxes) \
};
#define FONT_STYLE(Style, family_, large_, medium_, small_) \
FONT_CONFIG(Style, Large, ARG(large_)) \
FONT_CONFIG(Style, Medium, ARG(medium_)) \
FONT_CONFIG(Style, Small, ARG(small_)) \
\
class FontStyle##Style : public FontStyleNode { \
CONFIG_NODE(FontStyle##Style, FontStyleNode) \
\
CONFIG_PROPERTY(QString, family, family_) \
CONFIG_SUBOBJECT(FontConfig##Style##Large, large) \
CONFIG_SUBOBJECT(FontConfig##Style##Medium, medium) \
CONFIG_SUBOBJECT(FontConfig##Style##Small, small) \
\
public: \
FontSizeNode* largeNode() const override { return m_large; } \
FontSizeNode* mediumNode() const override { return m_medium; } \
FontSizeNode* smallNode() const override { return m_small; } \
};
// clang-format off
FONT_STYLE(Headline, u"GoogleSansFlex"_s,
FONT({ .size = 32, .weight = QFont::Medium }),
FONT({ .size = 28, .weight = QFont::Medium }),
FONT({ .size = 24, .weight = QFont::Medium })
)
FONT_STYLE(Title, u"GoogleSansFlex"_s,
FONT({ .size = 22, .weight = QFont::Medium }),
FONT({ .size = 16, .weight = QFont::Medium }),
FONT({ .size = 14, .weight = QFont::Medium })
)
FONT_STYLE(Body, u"GoogleSansFlex"_s,
FONT({ .size = 16 }),
FONT({ .size = 14 }),
FONT({ .size = 12 })
)
FONT_STYLE(Label, u"GoogleSansFlex"_s,
FONT({ .size = 14, .weight = QFont::Medium }),
FONT({ .size = 12, .weight = QFont::Medium }),
FONT({ .size = 11 })
)
FONT_STYLE(Mono, u"CaskaydiaCove NF"_s,
FONT({ .size = 16, .vaxes = {} }),
FONT({ .size = 14, .vaxes = {} }),
FONT({ .size = 12, .vaxes = {} })
)
// clang-format on
FONT_CONFIG(Icon, ExtraLarge, FONT({.size = static_cast<int>(48 / 1.33)}))
FONT_CONFIG(Icon, Large, FONT({.size = static_cast<int>(32 / 1.33)}))
FONT_CONFIG(Icon, Medium, FONT({.size = static_cast<int>(24 / 1.33)}))
FONT_CONFIG(Icon, Small, FONT({.size = static_cast<int>(20 / 1.33)}))
class FontStyleIcon : public FontStyleNode {
CONFIG_NODE(FontStyleIcon, FontStyleNode)
CONFIG_PROPERTY(QString, family, u"Material Symbols Rounded"_s)
CONFIG_SUBOBJECT(FontConfigIconExtraLarge, extraLarge)
CONFIG_SUBOBJECT(FontConfigIconLarge, large)
CONFIG_SUBOBJECT(FontConfigIconMedium, medium)
CONFIG_SUBOBJECT(FontConfigIconSmall, small)
public:
FontSizeNode* extraLargeNode() const override { return m_extraLarge; }
FontSizeNode* largeNode() const override { return m_large; }
FontSizeNode* mediumNode() const override { return m_medium; }
FontSizeNode* smallNode() const override { return m_small; }
};
#undef ARG
#undef FONT
#undef FONT_CONFIG
#undef FONT_STYLE
class FontFamily : public settings::ObjectNode {
CONFIG_NODE(FontFamily, settings::ObjectNode)
CONFIG_PROPERTY(QString, clock, QStringLiteral("Rubik"))
CONFIG_PROPERTY(
QString, material, QStringLiteral("Material Symbols Rounded"))
CONFIG_PROPERTY(QString, mono, QStringLiteral("CaskaydiaCove NF"))
CONFIG_PROPERTY(QString, sans, QStringLiteral("Segoe UI Variable Text"))
};
class AppearanceFont : public settings::ObjectNode {
CONFIG_NODE(AppearanceFont, settings::ObjectNode)
CONFIG_PROPERTY(qreal, scale, 1)
CONFIG_SUBOBJECT(FontFamily, family)
public:
explicit Font(QObject* parent = nullptr)
: ConfigObject(parent), m_family(new FontFamily(this)) {}
CONFIG_SUBOBJECT(FontStyleHeadline, headline)
CONFIG_SUBOBJECT(FontStyleTitle, title)
CONFIG_SUBOBJECT(FontStyleBody, body)
CONFIG_SUBOBJECT(FontStyleLabel, label)
CONFIG_SUBOBJECT(FontStyleMono, mono)
CONFIG_SUBOBJECT(FontStyleIcon, icon)
CONFIG_PROPERTY(QString, clock, QStringLiteral("Rubik"))
CONFIG_PROPERTY(QString, workspaces, QStringLiteral("Rubik"))
};
class Transparency : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class AnimDurations : public settings::ObjectNode {
CONFIG_NODE(AnimDurations, settings::ObjectNode)
CFG_PROPERTY(qreal, base, 0.85)
CFG_PROPERTY(bool, enabled, false)
CFG_PROPERTY(qreal, layers, 0.4)
CONFIG_GLOBAL_PROPERTY(qreal, scale, 1)
Q_PROPERTY(int small READ small NOTIFY valuesChanged)
Q_PROPERTY(int normal READ normal NOTIFY valuesChanged)
Q_PROPERTY(int large READ large NOTIFY valuesChanged)
Q_PROPERTY(int extraLarge READ extraLarge NOTIFY valuesChanged)
Q_PROPERTY(
int expressiveFastSpatial READ expressiveFastSpatial NOTIFY
valuesChanged)
Q_PROPERTY(
int expressiveDefaultSpatial READ expressiveDefaultSpatial NOTIFY
valuesChanged)
Q_PROPERTY(
int expressiveSlowSpatial READ expressiveSlowSpatial NOTIFY
valuesChanged)
Q_PROPERTY(
int expressiveFastEffects READ expressiveFastEffects NOTIFY
valuesChanged)
Q_PROPERTY(
int expressiveDefaultEffects READ expressiveDefaultEffects NOTIFY
valuesChanged)
Q_PROPERTY(
int expressiveSlowEffects READ expressiveSlowEffects NOTIFY
valuesChanged)
public:
explicit Transparency(QObject* parent = nullptr) : ConfigObject(parent) {}
void bindTokens(AnimDurationTokens* tokens);
[[nodiscard]] int small() const;
[[nodiscard]] int normal() const;
[[nodiscard]] int large() const;
[[nodiscard]] int extraLarge() const;
[[nodiscard]] int expressiveFastSpatial() const;
[[nodiscard]] int expressiveDefaultSpatial() const;
[[nodiscard]] int expressiveSlowSpatial() const;
[[nodiscard]] int expressiveFastEffects() const;
[[nodiscard]] int expressiveDefaultEffects() const;
[[nodiscard]] int expressiveSlowEffects() const;
signals:
void valuesChanged();
private:
AnimDurationTokens* m_tokens = nullptr;
};
class Appearance : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class Anim : public settings::ObjectNode {
CONFIG_NODE(Anim, settings::ObjectNode)
CONFIG_PROPERTY(int, mediaGifSpeedAdjustment, 300)
CONFIG_PROPERTY(qreal, sessionGifSpeed, 0.7)
CONFIG_SUBOBJECT(AnimDurations, durations)
};
class Deform : public settings::ObjectNode {
CONFIG_NODE(Deform, settings::ObjectNode)
CONFIG_PROPERTY(qreal, scale, 1)
};
class AppearanceTransparency : public settings::ObjectNode {
CONFIG_NODE(AppearanceTransparency, settings::ObjectNode)
CONFIG_GLOBAL_PROPERTY(bool, enabled, false)
CONFIG_GLOBAL_PROPERTY(qreal, base, 0.85)
CONFIG_GLOBAL_PROPERTY(qreal, layers, 0.4)
};
class Appearance : public settings::ObjectNode {
CONFIG_NODE(Appearance, settings::ObjectNode)
CONFIG_SUBOBJECT(Anim, anim)
CONFIG_SUBOBJECT(Deform, deform)
CONFIG_SUBOBJECT(Font, font)
CONFIG_SUBOBJECT(Transparency, transparency)
public:
explicit Appearance(QObject* parent = nullptr)
: ConfigObject(parent)
, m_anim(new Anim(this))
, m_deform(new Deform(this))
, m_font(new Font(this))
, m_transparency(new Transparency(this)) {}
CONFIG_SUBOBJECT(AppearanceFont, font)
CONFIG_SUBOBJECT(AppearanceRounding, rounding)
CONFIG_SUBOBJECT(AppearanceSpacing, spacing)
CONFIG_SUBOBJECT(AppearancePadding, padding)
CONFIG_SUBOBJECT(AppearanceTransparency, transparency)
};
} // namespace ZShell::config
+5 -9
View File
@@ -1,18 +1,14 @@
#pragma once
#include "configobject.hpp"
#include "common.hpp"
#include <qqmlintegration.h>
namespace ZShell::config {
class Background : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class Background : public settings::ObjectNode {
CONFIG_NODE(Background, settings::ObjectNode)
CFG_PROPERTY(bool, enabled, true)
CFG_PROPERTY(int, wallFadeDuration, 300)
public:
explicit Background(QObject* parent = nullptr) : ConfigObject(parent) {}
CONFIG_PROPERTY(bool, enabled, true)
CONFIG_PROPERTY(int, wallFadeDuration, 300)
};
} // namespace ZShell::config
+28 -47
View File
@@ -1,7 +1,6 @@
#pragma once
#include "configlist.hpp"
#include "configobject.hpp"
#include "common.hpp"
#include <qcontainerfwd.h>
#include <qhashfunctions.h>
#include <qqmlregistration.h>
@@ -9,66 +8,53 @@
namespace ZShell::config {
class BarEntry : public ListEntry {
Q_OBJECT
QML_ANONYMOUS
public:
explicit BarEntry(QObject* parent = nullptr) : ListEntry(parent) {}
CONFIG_NODE(BarEntry, ListEntry)
};
class Popouts : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class Popouts : public settings::ObjectNode {
CONFIG_NODE(Popouts, settings::ObjectNode)
CFG_PROPERTY(bool, statusIcons, true)
CFG_PROPERTY(bool, tray, true)
public:
explicit Popouts(QObject* parent = nullptr) : ConfigObject(parent) {}
CONFIG_PROPERTY(bool, statusIcons, true)
CONFIG_PROPERTY(bool, tray, true)
};
class Tray : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class Tray : public settings::ObjectNode {
CONFIG_NODE(Tray, settings::ObjectNode)
CFG_PROPERTY(bool, recolorIcons, false)
CFG_PROPERTY(bool, showOnHover, true)
CFG_PROPERTY(int, trayIconSize, 24)
CONFIG_PROPERTY(bool, recolorIcons, false)
CONFIG_PROPERTY(bool, showOnHover, true)
CONFIG_PROPERTY(int, trayIconSize, 24)
CONFIG_LIST(
EntryList,
statusIcons,
{
DEFAULT_ARG({
LIST_ENTRY(audio, true),
LIST_ENTRY(microphone, true),
LIST_ENTRY(bluetooth, false),
LIST_ENTRY(network, false),
LIST_ENTRY(wifi, false),
LIST_ENTRY(power, true),
})
public:
explicit Tray(QObject* parent = nullptr) : ConfigObject(parent) {}
}))
};
class Bar : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class Bar : public settings::ObjectNode {
CONFIG_NODE(Bar, settings::ObjectNode)
CFG_PROPERTY(bool, autoHide, false)
CFG_PROPERTY(int, border, 4)
CFG_PROPERTY(bool, fullscreenReveal, true)
CFG_PROPERTY(int, height, 24)
CFG_PROPERTY(bool, hideWhenNotif, true)
CONFIG_PROPERTY(bool, autoHide, false)
CONFIG_PROPERTY(int, border, 4)
CONFIG_PROPERTY(bool, fullscreenReveal, true)
CONFIG_PROPERTY(int, height, 24)
CONFIG_PROPERTY(bool, hideWhenNotif, true)
CONFIG_SUBOBJECT(Popouts, popouts)
CFG_PROPERTY(int, revealDelay, 100)
CFG_PROPERTY(int, rounding, 14)
CFG_PROPERTY(int, smoothing, 32)
CFG_PROPERTY(QString, position, QStringLiteral("top"))
CONFIG_PROPERTY(int, revealDelay, 100)
CONFIG_PROPERTY(int, rounding, 14)
CONFIG_PROPERTY(int, smoothing, 32)
CONFIG_PROPERTY(QString, position, QStringLiteral("top"))
CONFIG_SUBOBJECT(Tray, tray)
CONFIG_LIST(
EntryList,
entries,
{
DEFAULT_ARG({
LIST_ENTRY(workspaces, true),
LIST_ENTRY(audio, true),
LIST_ENTRY(media, true),
@@ -78,20 +64,15 @@ class Bar : public ConfigObject {
LIST_ENTRY(spacer, true),
LIST_ENTRY(activeWindow, true),
LIST_ENTRY(spacer, true),
LIST_ENTRY(hyprsunset, true),
LIST_ENTRY(nightlight, true),
LIST_ENTRY(tray, true),
LIST_ENTRY(statusIcons, true),
LIST_ENTRY(network, false),
LIST_ENTRY(upower, true),
LIST_ENTRY(clock, true),
LIST_ENTRY(notifBell, true),
})
public:
explicit Bar(QObject* parent = nullptr)
: ConfigObject(parent)
, m_popouts(new Popouts(this))
, m_tray(new Tray(this)) {}
}),
.identityKey = QStringLiteral("id"))
};
} // namespace ZShell::config
+11 -20
View File
@@ -1,33 +1,24 @@
#pragma once
#include "configobject.hpp"
#include "common.hpp"
#include <qqmlintegration.h>
namespace ZShell::config {
class ClipboardSizes : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class ClipboardSizes : public settings::ObjectNode {
CONFIG_NODE(ClipboardSizes, settings::ObjectNode)
CFG_PROPERTY(int, itemHeight, 60)
CFG_PROPERTY(int, minPreviewWidth, 200)
CFG_PROPERTY(int, previewWidth, 800)
CFG_PROPERTY(int, width, 500)
public:
explicit ClipboardSizes(QObject* parent = nullptr) : ConfigObject(parent) {}
CONFIG_PROPERTY(int, itemHeight, 60)
CONFIG_PROPERTY(int, minPreviewWidth, 200)
CONFIG_PROPERTY(int, previewWidth, 800)
CONFIG_PROPERTY(int, width, 500)
};
class Clipboard : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class Clipboard : public settings::ObjectNode {
CONFIG_NODE(Clipboard, settings::ObjectNode)
CFG_PROPERTY(bool, enabled, true)
CFG_PROPERTY(int, maxEntriesShown, 10)
CONFIG_PROPERTY(bool, enabled, true)
CONFIG_PROPERTY(int, maxEntriesShown, 10)
CONFIG_SUBOBJECT(ClipboardSizes, sizes)
public:
explicit Clipboard(QObject* parent = nullptr)
: ConfigObject(parent), m_sizes(new ClipboardSizes(this)) {}
};
} // namespace ZShell::config
+16 -18
View File
@@ -1,31 +1,29 @@
#pragma once
#include "configobject.hpp"
#include "common.hpp"
#include <qqmlintegration.h>
namespace ZShell::config {
class Presets : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class Presets : public settings::ObjectNode {
CONFIG_NODE(Presets, settings::ObjectNode)
CFG_PROPERTY(QString, accent, QString())
CFG_PROPERTY(QString, name, QStringLiteral("Catppuccin"))
CFG_PROPERTY(QString, variant, QStringLiteral("latte"))
public:
explicit Presets(QObject* parent = nullptr) : ConfigObject(parent) {}
CONFIG_PROPERTY(QString, accent, QString())
CONFIG_PROPERTY(QString, name, QStringLiteral("Catppuccin"))
CONFIG_PROPERTY(QString, variant, QStringLiteral("latte"))
};
class Colors : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class Colors : public settings::ObjectNode {
CONFIG_NODE(Colors, settings::ObjectNode)
CONFIG_SUBOBJECT(Presets, presets)
CFG_PROPERTY(QString, schemeType, QStringLiteral("fidelity"))
public:
explicit Colors(QObject* parent = nullptr)
: ConfigObject(parent), m_presets(new Presets(this)) {}
CONFIG_PROPERTY(QString, schemeType, QStringLiteral("fidelity"))
CONFIG_PROPERTY(bool, scheduleDark, false)
CONFIG_PROPERTY(int, scheduleDarkEnd, 600)
CONFIG_PROPERTY(int, scheduleDarkStart, 1140)
CONFIG_PROPERTY(bool, schemeGen, true)
CONFIG_PROPERTY(bool, neovimColors, false)
CONFIG_PROPERTY(QString, mode, QStringLiteral("dark"))
CONFIG_PROPERTY(bool, smart, false)
};
} // namespace ZShell::config
+22
View File
@@ -0,0 +1,22 @@
#include "common.hpp"
#include <QDir>
#include <QStandardPaths>
namespace ZShell::config {
Q_LOGGING_CATEGORY(lcConfig, "zshell.config", QtInfoMsg)
QString configDir() {
const QString dir =
QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) +
QStringLiteral("/zshell");
QDir().mkpath(dir);
return dir;
}
QString monitorConfigDir() {
return configDir() + QStringLiteral("/monitors");
}
} // namespace ZShell::config
+54
View File
@@ -0,0 +1,54 @@
#pragma once
#include <qloggingcategory.h>
#include <qstring.h>
#include "settings/macros.hpp"
#include "settings/objectnode.hpp"
#define CONFIG_NODE_NO_CTOR SETTINGS_NODE_NO_CTOR
#define CONFIG_NODE SETTINGS_NODE
#define CONFIG_PROPERTY SETTINGS_PROPERTY
#define CONFIG_GLOBAL_PROPERTY SETTINGS_GLOBAL_PROPERTY
#define CONFIG_ENUM_PROPERTY(Type, name, defaultVal) \
CONFIG_PROPERTY(ZShell::config::Type::Enum, name, defaultVal)
#define CONFIG_GLOBAL_ENUM_PROPERTY(Type, name, defaultVal) \
CONFIG_GLOBAL_PROPERTY(ZShell::config::Type::Enum, name, defaultVal)
#define CONFIG_SUBOBJECT(Type, name) \
SETTINGS_SUBOBJECT(ZShell::config::Type, name)
#define CONFIG_GLOBAL_SUBOBJECT(Type, name) \
SETTINGS_GLOBAL_SUBOBJECT(ZShell::config::Type, name)
#define CONFIG_LIST_TYPE SETTINGS_LIST_TYPE
#define CONFIG_LIST(Type, name, defaultVal, ...) \
SETTINGS_LIST( \
ZShell::config::Type, name, DEFAULT_ARG(defaultVal), __VA_ARGS__)
#define CONFIG_GLOBAL_LIST(Type, name, defaultVal, ...) \
SETTINGS_GLOBAL_LIST( \
ZShell::config::Type, name, DEFAULT_ARG(defaultVal), __VA_ARGS__)
namespace ZShell::config {
Q_DECLARE_LOGGING_CATEGORY(lcConfig)
QString configDir();
QString monitorConfigDir();
class ListEntry : public settings::ObjectNode {
CONFIG_NODE(ListEntry, settings::ObjectNode)
CONFIG_PROPERTY(QString, id, {})
CONFIG_PROPERTY(bool, enabled, true)
};
CONFIG_LIST_TYPE(ListEntry, EntryList)
} // namespace ZShell::config
// Shorthand for declaring an ID'd entry (bar entries/status icons, quick toggles, etc)
#define LIST_ENTRY(id, enabled) \
settings::vmap({ \
{QStringLiteral("id"), QStringLiteral(#id)}, \
{QStringLiteral("enabled"), enabled}, \
})
-288
View File
@@ -1,288 +0,0 @@
#include "config.hpp"
#include "appearance.hpp"
#include "background.hpp"
#include "bar.hpp"
#include "clipboard.hpp"
#include "colors.hpp"
#include "dashboard.hpp"
#include "dock.hpp"
#include "general.hpp"
#include "launcher.hpp"
#include "llm.hpp"
#include "lock.hpp"
#include "notifs.hpp"
#include "osd.hpp"
#include "screenshot.hpp"
#include "services.hpp"
#include "sidebar.hpp"
#include "utilities.hpp"
#include <QFile>
#include <QSaveFile>
#include <QJsonDocument>
#include <QStandardPaths>
#include <QDir>
#include <QDebug>
#include <QFutureWatcher>
#include <QFileInfo>
#include <QFileSystemWatcher>
#include <qcontainerfwd.h>
#include <qfileinfo.h>
#include <qfilesystemwatcher.h>
#include <qthreadpool.h>
namespace ZShell::config {
Config* Config::s_instance = nullptr;
Config::Config(QObject* parent)
: ConfigObject(parent)
, m_appearance(new Appearance(this))
, m_background(new Background(this))
, m_bar(new Bar(this))
, m_clipboard(new Clipboard(this))
, m_colors(new Colors(this))
, m_dashboard(new Dashboard(this))
, m_dock(new Dock(this))
, m_general(new General(this))
, m_launcher(new Launcher(this))
, m_llm(new Llm(this))
, m_lock(new Lock(this))
, m_notifs(new Notifs(this))
, m_osd(new Osd(this))
, m_screenshot(new Screenshot(this))
, m_services(new Services(this))
, m_sidebar(new Sidebar(this))
, m_utilities(new Utilities(this)) {
s_instance = this;
connect(this, &ConfigObject::propertiesChanged, this, &Config::scheduleSave);
m_saveTimer.setSingleShot(true);
m_saveTimer.setInterval(300);
connect(&m_saveTimer, &QTimer::timeout, this, &Config::flushAsync);
m_reloadTimer.setSingleShot(true);
m_reloadTimer.setInterval(50);
connect(&m_reloadTimer, &QTimer::timeout, this, &Config::reloadAsync);
connectAutoSave(this);
connect(
&m_watcher,
&QFileSystemWatcher::directoryChanged,
this,
&Config::onWatcherEvent);
connect(
&m_watcher,
&QFileSystemWatcher::fileChanged,
this,
&Config::onWatcherEvent);
loadSync();
updateWatch();
m_firstLoadDone = true;
}
Config* Config::instance() {
return s_instance;
}
Config* Config::create(QQmlEngine*, QJSEngine*) {
if (!s_instance) s_instance = new Config();
return s_instance;
}
QString Config::filePath() const {
const QString dir =
QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) +
QStringLiteral("/zshell");
QDir().mkpath(dir);
return dir + QStringLiteral("/config.json");
}
void Config::loadSync() {
m_loading = true;
QFile f(filePath());
QJsonObject before;
bool existed = false;
if (f.open(QIODevice::ReadOnly)) {
const auto doc = QJsonDocument::fromJson(f.readAll());
if (doc.isObject()) {
before = doc.object();
existed = true;
} else {
qInfo() << "Config: existing config at" << filePath()
<< "is empty or not a valid JSON object - using defaults";
}
} else {
qInfo() << "Config: no existing config at" << filePath()
<< "- using defaults";
}
loadFromJson(QJsonValue(before));
m_loading = false;
const auto after = toJson().toObject();
if (!existed || after != before) saveNow();
}
void Config::updateWatch() {
const QFileInfo info(filePath());
const QString dir = info.absolutePath();
const QString file = info.absoluteFilePath();
if (!m_watcher.directories().contains(dir)) m_watcher.addPath(dir);
if (info.exists() && !m_watcher.files().contains(file))
m_watcher.addPath(file);
}
void Config::onWatcherEvent(const QString&) {
updateWatch();
const QFileInfo info(filePath());
if (!info.exists()) return;
QFile file(info.absoluteFilePath());
if (!file.open(QIODevice::ReadOnly)) return;
const QByteArray hash =
QCryptographicHash::hash(file.readAll(), QCryptographicHash::Sha256);
{
QMutexLocker lock(&m_writeMutex);
if (hash == m_lastWriteHash) return;
}
m_reloadPending = true;
if (!m_loading) m_reloadTimer.start();
}
void Config::loadAsync() {
m_loading = true;
auto future = QtConcurrent::run([this]() {
QFile f(filePath());
if (f.open(QIODevice::ReadOnly)) {
const auto doc = QJsonDocument::fromJson(f.readAll());
const bool valid = doc.isObject();
const QJsonObject before = valid ? doc.object() : QJsonObject();
QMetaObject::invokeMethod(
this,
[this, valid, before]() {
loadFromJson(QJsonValue(before));
m_loading = false;
const auto after = toJson().toObject();
if (!valid || after != before) saveNow();
if (m_reloadPending) m_reloadTimer.start();
},
Qt::QueuedConnection);
} else {
qInfo() << "Config: failed to reload from" << filePath()
<< "- using in-memory values";
QMetaObject::invokeMethod(
this,
[this]() {
m_loading = false;
if (m_reloadPending) m_reloadTimer.start();
},
Qt::QueuedConnection);
}
});
m_loadFuture = future;
}
void Config::load() {
if (!m_firstLoadDone) {
loadSync();
} else {
loadAsync();
}
}
void Config::reloadAsync() {
if (m_loading) return;
m_reloadPending = false;
loadAsync();
}
void Config::scheduleSave() {
if (m_loading) return;
m_saveTimer.start();
}
void Config::flushAsync() {
const QByteArray data =
QJsonDocument(toJson().toObject()).toJson(QJsonDocument::Indented);
const QByteArray hash =
QCryptographicHash::hash(data, QCryptographicHash::Sha256);
QThreadPool::globalInstance()->start([this, data, hash]() {
const bool success = writeAtomically(data);
QMetaObject::invokeMethod(
this,
[this, success, hash]() {
if (!success) return;
QMutexLocker lock(&m_writeMutex);
m_lastWriteHash = hash;
},
Qt::QueuedConnection);
});
}
void Config::saveNow() {
m_saveTimer.stop();
flushAsync();
}
void Config::connectAutoSave(ConfigNode* node) {
connect(node, &ConfigNode::propertiesChanged, this, [this] {
scheduleSave();
});
for (auto* child : node->childNodes())
connectAutoSave(child);
}
bool Config::writeAtomically(const QByteArray& data) {
QSaveFile f(filePath());
if (!f.open(QIODevice::WriteOnly)) {
qWarning() << "Config: failed to open for atomic write:"
<< f.errorString();
return false;
}
if (f.write(data) != data.size()) {
qWarning() << "Config: failed to write:" << f.errorString();
return false;
}
if (!f.commit()) {
qWarning() << "Config: commit failed:" << f.errorString();
return false;
}
return true;
}
}; // namespace ZShell::config
-114
View File
@@ -1,114 +0,0 @@
#pragma once
#include "configobject.hpp"
#include <QByteArray>
#include <QFileSystemWatcher>
#include <QFuture>
#include <QMutex>
#include <QTimer>
#include <qqmlregistration.h>
#include <QtConcurrent/QtConcurrent>
class QQmlEngine;
class QJSEngine;
namespace ZShell::config {
class Appearance;
class Background;
class Bar;
class Clipboard;
class Colors;
class Dashboard;
class Dock;
class General;
class Launcher;
class Llm;
class Lock;
class Notifs;
class Osd;
class Screenshot;
class Services;
class Sidebar;
class Utilities;
class Config : public ConfigObject {
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
Q_MOC_INCLUDE("appearance.hpp")
Q_MOC_INCLUDE("background.hpp")
Q_MOC_INCLUDE("bar.hpp")
Q_MOC_INCLUDE("clipboard.hpp")
Q_MOC_INCLUDE("colors.hpp")
Q_MOC_INCLUDE("dashboard.hpp")
Q_MOC_INCLUDE("dock.hpp")
Q_MOC_INCLUDE("general.hpp")
Q_MOC_INCLUDE("launcher.hpp")
Q_MOC_INCLUDE("llm.hpp")
Q_MOC_INCLUDE("lock.hpp")
Q_MOC_INCLUDE("notifs.hpp")
Q_MOC_INCLUDE("osd.hpp")
Q_MOC_INCLUDE("screenshot.hpp")
Q_MOC_INCLUDE("services.hpp")
Q_MOC_INCLUDE("sidebar.hpp")
Q_MOC_INCLUDE("utilities.hpp")
CONFIG_SUBOBJECT(Appearance, appearance)
CONFIG_SUBOBJECT(Background, background)
CONFIG_SUBOBJECT(Bar, bar)
CONFIG_SUBOBJECT(Clipboard, clipboard)
CONFIG_SUBOBJECT(Colors, colors)
CONFIG_SUBOBJECT(Dashboard, dashboard)
CONFIG_SUBOBJECT(Dock, dock)
CONFIG_SUBOBJECT(General, general)
CONFIG_SUBOBJECT(Launcher, launcher)
CONFIG_SUBOBJECT(Llm, llm)
CONFIG_SUBOBJECT(Lock, lock)
CONFIG_SUBOBJECT(Notifs, notifs)
CONFIG_SUBOBJECT(Osd, osd)
CONFIG_SUBOBJECT(Screenshot, screenshot)
CONFIG_SUBOBJECT(Services, services)
CONFIG_SUBOBJECT(Sidebar, sidebar)
CONFIG_SUBOBJECT(Utilities, utilities)
public:
explicit Config(QObject* parent = nullptr);
static Config* create(QQmlEngine*, QJSEngine*);
[[nodiscard]] static Config* instance();
Q_INVOKABLE void load();
Q_INVOKABLE void saveNow();
Q_INVOKABLE void reloadAsync();
private Q_SLOTS:
void scheduleSave();
void flushAsync();
void onWatcherEvent(const QString& path);
private:
QString filePath() const;
bool writeAtomically(const QByteArray& data);
void updateWatch();
void loadSync();
void loadAsync();
void connectAutoSave(ConfigNode* node);
QTimer m_saveTimer;
QTimer m_reloadTimer;
QFileSystemWatcher m_watcher;
QMutex m_writeMutex;
QByteArray m_lastWriteHash;
bool m_reloadPending = false;
bool m_loading = false;
bool m_firstLoadDone = false;
QFuture<void> m_loadFuture;
static Config* s_instance;
};
} // namespace ZShell::config
-314
View File
@@ -1,314 +0,0 @@
#include "configlist.hpp"
#include <QJsonObject>
#include <QMetaObject>
#include <QQmlEngine>
#include <QString>
namespace ZShell::config {
ConfigList::ConfigList(QObject* parent, const QVariantList& defaults)
: ConfigNode(parent), m_defaults(QJsonArray::fromVariantList(defaults)) {}
int ConfigList::count() const {
return static_cast<int>(m_items.size());
}
QVariantList ConfigList::values() const {
QVariantList vals;
vals.reserve(m_items.size());
for (auto* const item : m_items)
vals.append(QVariant::fromValue(item));
return vals;
}
ConfigObject* ConfigList::itemAt(int index) const {
if (index < 0 || index >= m_items.size()) return nullptr;
return m_items.at(index);
}
const QList<ConfigObject*>& ConfigList::items() const {
return m_items;
}
void ConfigList::remove(int index) {
if (index < 0 || index >= m_items.size()) {
qCWarning(lcConfig)
<< metaObject()->className() << "cannot remove index" << index
<< "of" << count();
return;
}
destroyItem(m_items.takeAt(index));
m_loaded = true;
emit countChanged();
emit valuesChanged();
notifyChanged();
}
void ConfigList::move(int from, int to) {
if (from < 0 || from >= m_items.size() || to < 0 || to >= m_items.size()) {
qCWarning(lcConfig) << metaObject()->className() << "cannot move"
<< from << "to" << to << "of" << count();
return;
}
if (from == to) return;
m_items.move(from, to);
m_loaded = true;
emit valuesChanged();
notifyChanged();
}
void ConfigList::clear() {
if (m_items.isEmpty()) return;
destroyItems();
m_loaded = true;
emit countChanged();
emit valuesChanged();
notifyChanged();
}
void ConfigList::loadFromJson(const QJsonValue& json) {
if (!json.isArray()) {
qCWarning(
lcConfig,
"Option '%s' must be a list, resetting to default",
qUtf8Printable(propertyPath()));
resetToDefaults();
materializeDefaults();
return;
}
populate(json.toArray());
m_loaded = true;
}
QJsonValue ConfigList::toJson() const {
if (m_loaded) return elementsToJson();
return QJsonValue::Undefined;
}
void ConfigList::clearLoadedKeys() {
m_loaded = false;
}
QStringList ConfigList::unknownKeys() const {
QStringList keys;
for (int i = 0; i < m_items.size(); ++i) {
const auto childKeys = m_items.at(i)->unknownKeys();
for (const auto& childKey : childKeys)
keys.append(joinPath(QString("[%1]").arg(i), childKey));
}
return keys;
}
void ConfigList::materializeDefaults() {
for (auto* const item : m_items)
item->materializeDefaults();
if (m_global) return;
m_loaded = true;
}
void ConfigList::resyncFromGlobal() {
syncValuesFromGlobal();
}
ConfigObject* ConfigList::insertItem(const QVariantMap& props, int index) {
const auto pos = index < 0 || index > m_items.size() ? m_items.size()
: index;
appendItem(QJsonObject::fromVariantMap(props));
m_items.move(m_items.size() - 1, pos);
auto* const item = m_items.at(pos);
const auto unknown = item->unknownKeys();
for (const auto& key : unknown)
qCWarning(lcConfig) << "Unknown option" << key << "for"
<< item->metaObject()->className();
m_loaded = true;
emit countChanged();
emit valuesChanged();
notifyChanged();
return item;
}
QJsonArray ConfigList::elementsToJson() const {
QJsonArray arr;
for (auto* const item : m_items)
arr.append(item->toJson().toObject());
return arr;
}
void ConfigList::resetToDefaults() {
loadFromJsonQuietly(m_defaults);
m_loaded = false;
}
void ConfigList::syncValuesFromGlobal() {
if (m_loaded) return;
if (auto* const global = qobject_cast<ConfigList*>(m_global))
populate(global->elementsToJson());
}
void ConfigList::onGlobalPropertiesChanged(const QMap<QString, QVariant>&) {
syncValuesFromGlobal();
}
QString ConfigList::childPath(const ConfigNode* child) const {
for (int i = 0; i < m_items.size(); ++i)
if (m_items.at(i) == child) return QString("[%1]").arg(i);
return {};
}
void ConfigList::populate(const QJsonArray& arr) {
const auto current = elementsToJson();
if (current == arr) return;
const auto old = m_items;
QList<bool> reused(old.size(), false);
m_items.clear();
m_items.reserve(arr.size());
for (int i = 0; i < arr.size(); ++i) {
auto match =
i < old.size() && !reused.at(i) && current.at(i) == arr.at(i) ? i
: -1;
for (int j = 0; match < 0 && j < old.size(); ++j)
if (!reused.at(j) && current.at(j) == arr.at(i)) match = j;
auto update = false;
for (int j = 0; match < 0 && j < old.size(); ++j) {
if (!reused.at(j) &&
sameElement(old.at(j), current.at(j), arr.at(i))) {
match = j;
update = true;
}
}
if (match < 0) {
appendItem(arr.at(i));
continue;
}
reused[match] = true;
if (update) old.at(match)->loadFromJsonQuietly(arr.at(i));
m_items.append(old.at(match));
}
for (int i = 0; i < old.size(); ++i)
if (!reused.at(i)) destroyItem(old.at(i));
if (m_items.size() != old.size()) emit countChanged();
emit valuesChanged();
notifyChanged();
}
bool ConfigList::sameElement(
const ConfigObject* item,
const QJsonValue& current,
const QJsonValue& json) {
const auto keys = item->identityKeys();
if (keys.isEmpty()) return false;
const auto currentObj = current.toObject();
const auto newObj = json.toObject();
if (currentObj.keys() != newObj.keys()) return false;
for (const auto& key : keys)
if (!newObj.contains(key) || currentObj.value(key) != newObj.value(key))
return false;
return true;
}
void ConfigList::appendItem(const QJsonValue& json) {
auto* const item = createItem();
QQmlEngine::setObjectOwnership(item, QQmlEngine::CppOwnership);
if (m_items.isEmpty()) rejectGlobalOnlyElement(item);
item->loadFromJsonQuietly(json);
connectItem(item);
m_items.append(item);
}
void ConfigList::rejectGlobalOnlyElement(const ConfigObject* item) const {
const auto keys = item->globalOnlyKeys();
if (keys.isEmpty()) return;
qCCritical(
lcConfig,
"%s declares global-only options (%s) which do not work inside a list, "
"use CONFIG_GLOBAL_LIST on %s instead",
item->metaObject()->className(),
qUtf8Printable(keys.join(", ")),
metaObject()->className());
}
void ConfigList::connectItem(ConfigNode* node) {
connect(
node, &ConfigNode::propertiesChanged, this, &ConfigList::onItemChanged);
const auto children = node->childNodes();
for (auto* const child : children)
connectItem(child);
}
void ConfigList::disconnectItem(ConfigNode* node) {
node->disconnect(this);
const auto children = node->childNodes();
for (auto* const child : children)
disconnectItem(child);
}
void ConfigList::destroyItems() {
for (auto* const item : std::as_const(m_items))
destroyItem(item);
m_items.clear();
}
void ConfigList::destroyItem(ConfigObject* item) {
disconnectItem(item);
item->deleteLater();
}
void ConfigList::onItemChanged() {
m_loaded = true;
notifyChanged();
}
void ConfigList::notifyChanged() {
notifyPropertyChanged("values", count());
}
} // namespace ZShell::config
-139
View File
@@ -1,139 +0,0 @@
#pragma once
#include "configobject.hpp"
#include <QJsonArray>
#include <QJsonObject>
#include <QQmlEngine>
#include <QVariantList>
namespace ZShell::config {
class ConfigList : public ConfigNode {
Q_OBJECT
QML_ANONYMOUS
Q_PROPERTY(int count READ count NOTIFY countChanged)
Q_PROPERTY(QVariantList values READ values NOTIFY valuesChanged)
public:
explicit ConfigList(
QObject* parent = nullptr, const QVariantList& defaults = {});
[[nodiscard]] int count() const;
[[nodiscard]] QVariantList values() const;
[[nodiscard]] ConfigObject* itemAt(int index) const;
[[nodiscard]] const QList<ConfigObject*>& items() const;
Q_INVOKABLE void remove(int index);
Q_INVOKABLE void move(int from, int to);
Q_INVOKABLE void clear();
void loadFromJson(const QJsonValue& json) override;
[[nodiscard]] QJsonValue toJson() const override;
void clearLoadedKeys() override;
[[nodiscard]] QStringList unknownKeys() const override;
void materializeDefaults() override;
void resyncFromGlobal() override;
signals:
void countChanged();
void valuesChanged();
protected:
virtual ConfigObject* createItem() = 0;
void resetToDefaults();
ConfigObject* insertItem(const QVariantMap& props, int index = -1);
[[nodiscard]] QJsonArray elementsToJson() const;
void syncValuesFromGlobal() override;
void onGlobalPropertiesChanged(
const QMap<QString, QVariant>& changed) override;
[[nodiscard]] QString childPath(const ConfigNode* child) const override;
private:
void populate(const QJsonArray& arr);
static bool sameElement(
const ConfigObject* item,
const QJsonValue& current,
const QJsonValue& json);
void appendItem(const QJsonValue& json);
void destroyItems();
void destroyItem(ConfigObject* item);
void rejectGlobalOnlyElement(const ConfigObject* item) const;
void connectItem(ConfigNode* node);
void disconnectItem(ConfigNode* node);
void onItemChanged();
void notifyChanged();
QJsonArray m_defaults;
QList<ConfigObject*> m_items;
bool m_loaded = false;
};
} // namespace ZShell::config
#define CONFIG_LIST_TYPE(Element, Name) \
class Name : public ZShell::config::ConfigList { \
Q_OBJECT \
QML_ANONYMOUS \
\
public: \
explicit Name( \
QObject* parent = nullptr, const QVariantList& defaults = {}) \
: ZShell::config::ConfigList(parent, defaults) { \
resetToDefaults(); \
} \
\
Q_INVOKABLE ZShell::config::Element* at(int index) const { \
return static_cast<ZShell::config::Element*>(itemAt(index)); \
} \
Q_INVOKABLE ZShell::config::Element* insert( \
const QVariantMap& props, int index = -1) { \
return static_cast<ZShell::config::Element*>( \
insertItem(props, index)); \
} \
\
protected: \
[[nodiscard]] ZShell::config::ConfigObject* createItem() override { \
return new ZShell::config::Element(this); \
} \
};
#define CONFIG_LIST(Type, name, ...) \
Q_PROPERTY(ZShell::config::Type* name READ name CONSTANT) \
\
public: \
[[nodiscard]] Type* name() const { \
return m_##name; \
} \
\
private: \
Type* m_##name = new Type(this __VA_OPT__(, __VA_ARGS__));
#define LIST_ENTRY(id, enabled) \
vmap({{"id", QString::fromUtf8(#id)}, {"enabled", enabled}})
namespace ZShell::config {
class ListEntry : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
CFG_PROPERTY(QString, id)
CFG_PROPERTY(bool, enabled, true)
public:
explicit ListEntry(QObject* parent = nullptr) : ConfigObject(parent) {}
[[nodiscard]] QStringList identityKeys() const override {
return {QStringLiteral("id")};
}
};
CONFIG_LIST_TYPE(ListEntry, EntryList)
} // namespace ZShell::config
-89
View File
@@ -1,89 +0,0 @@
#include "confignode.hpp"
#include <qjsonvalue.h>
#include <qloggingcategory.h>
#include <qmetaobject.h>
#include <qstring.h>
namespace ZShell::config {
Q_LOGGING_CATEGORY(lcConfig, "zshell.config")
ConfigNode::ConfigNode(QObject* parent) : QObject(parent) {
m_batchTimer = new QTimer(this);
m_batchTimer->setSingleShot(true);
m_batchTimer->setInterval(0);
connect(
m_batchTimer, &QTimer::timeout, this, &ConfigNode::emitBatchedChanges);
}
void ConfigNode::loadFromJsonQuietly(const QJsonValue& json) {
setNotifySuppressed(true);
loadFromJson(json);
setNotifySuppressed(false);
}
void ConfigNode::setNotifySuppressed(bool suppressed) {
m_suppressNotify = suppressed;
const auto children = childNodes();
for (auto* const child : children)
child->setNotifySuppressed(suppressed);
}
QList<ConfigNode*> ConfigNode::childNodes() const {
return {};
}
void ConfigNode::syncFromGlobal(ConfigNode* global) {
m_global = global;
resyncFromGlobal();
}
bool ConfigNode::isOverlay() const {
return m_global != nullptr && this != m_global;
}
QString ConfigNode::propertyPath(const QString& name) const {
if (name.isEmpty()) return objectName();
QObject* const parentObj = parent();
if (parentObj) {
if (auto* parentNode = qobject_cast<ConfigNode*>(parentObj)) {
const auto childName = parentNode->childPath(this);
if (!childName.isEmpty()) {
return joinPath(parentNode->propertyPath(childName), name);
}
}
}
return name;
}
int ConfigNode::basePropertyOffset() {
return QObject::staticMetaObject.propertyCount();
}
void ConfigNode::notifyPropertyChanged(
const QString& name, const QVariant& value) {
if (m_suppressNotify) return;
m_pendingChanges[name] = value;
if (m_batchTimer && !m_batchTimer->isActive()) m_batchTimer->start();
}
QString ConfigNode::joinPath(const QString& parent, const QString& child) {
// List elements are addressed as parent[i], everything else as parent.child
if (child.startsWith(QLatin1Char('['))) return parent + child;
return parent + QLatin1Char('.') + child;
}
void ConfigNode::emitBatchedChanges() {
if (m_pendingChanges.isEmpty()) return;
QMap<QString, QVariant> changes = m_pendingChanges;
m_pendingChanges.clear();
emit propertiesChanged(changes);
}
} // namespace ZShell::config
-65
View File
@@ -1,65 +0,0 @@
#pragma once
#include <QJsonValue>
#include <QLoggingCategory>
#include <QMap>
#include <QObject>
#include <QStringList>
#include <QTimer>
#include <QVariant>
namespace ZShell::config {
Q_DECLARE_LOGGING_CATEGORY(lcConfig)
class ConfigNode : public QObject {
Q_OBJECT
public:
explicit ConfigNode(QObject* parent = nullptr);
virtual void loadFromJson(const QJsonValue& json) = 0;
void loadFromJsonQuietly(const QJsonValue& json);
[[nodiscard]] virtual QJsonValue toJson() const = 0;
virtual void clearLoadedKeys() = 0;
[[nodiscard]] virtual QStringList unknownKeys() const = 0;
[[nodiscard]] virtual QList<ConfigNode*> childNodes() const;
virtual void materializeDefaults() = 0;
void syncFromGlobal(ConfigNode* global);
virtual void resyncFromGlobal() = 0;
[[nodiscard]] bool isOverlay() const;
[[nodiscard]] QString propertyPath(const QString& name = {}) const;
[[nodiscard]] static int basePropertyOffset();
signals:
void propertiesChanged(const QMap<QString, QVariant>& changed);
protected:
virtual void syncValuesFromGlobal() = 0;
virtual void onGlobalPropertiesChanged(
const QMap<QString, QVariant>& changed) = 0;
[[nodiscard]] virtual QString childPath(const ConfigNode* child) const = 0;
void notifyPropertyChanged(const QString& name, const QVariant& value);
[[nodiscard]] static QString joinPath(
const QString& parent, const QString& child);
ConfigNode* m_global = nullptr;
private:
void setNotifySuppressed(bool suppressed);
void emitBatchedChanges();
QMap<QString, QVariant> m_pendingChanges;
QTimer* m_batchTimer = nullptr;
bool m_suppressNotify = false;
};
} // namespace ZShell::config
-344
View File
@@ -1,344 +0,0 @@
#include "configobject.hpp"
#include <QJsonArray>
#include <QMetaObject>
#include <QStringList>
#include <QVariant>
namespace ZShell::config {
namespace {
bool isStringArray(const QJsonArray& arr) {
for (const auto& v : arr) {
if (!v.isString()) return false;
}
return true;
}
bool jsonValueMatchesType(const QJsonValue& val, QMetaType type) {
switch (val.type()) {
case QJsonValue::Bool:
return type.id() == QMetaType::Bool;
case QJsonValue::Double:
switch (type.id()) {
case QMetaType::Int:
case QMetaType::UInt:
case QMetaType::LongLong:
case QMetaType::ULongLong:
case QMetaType::Double:
case QMetaType::Float:
return true;
default:
return false;
}
case QJsonValue::String:
return type.id() == QMetaType::QString;
case QJsonValue::Array:
if (type.id() == QMetaType::QStringList)
return isStringArray(val.toArray());
return type.id() == QMetaType::QVariantList;
case QJsonValue::Object:
case QJsonValue::Null:
case QJsonValue::Undefined:
default:
return false;
}
}
} // namespace
ConfigObject::ConfigObject(QObject* parent) : ConfigNode(parent) {}
void ConfigObject::loadFromJson(const QJsonValue& json) {
const auto obj = json.toObject();
const auto* meta = metaObject();
QSet<QString> known;
QSet<QString> invalid;
for (int i = basePropertyOffset(); i < meta->propertyCount(); ++i) {
auto prop = meta->property(i);
const auto key = QString::fromUtf8(prop.name());
known.insert(key);
if (!obj.contains(key)) continue;
const auto jsonVal = obj.value(key);
if (prop.metaType().flags().testFlag(QMetaType::PointerToQObject)) {
if (auto* const node = prop.read(this).value<ConfigNode*>()) {
node->loadFromJson(jsonVal);
continue;
}
}
if (!prop.isWritable()) continue;
if (!jsonValueMatchesType(jsonVal, prop.metaType())) {
qWarning() << "Config: type mismatch for" << key << "in"
<< meta->className() << "- expected"
<< prop.metaType().name() << "got value" << jsonVal
<< "- resetting to default";
invalid.insert(key);
continue;
}
if (prop.metaType().id() == QMetaType::QStringList) {
QStringList list;
const auto jsonArr = jsonVal.toArray();
for (const auto& v : jsonArr)
list.append(v.toString());
prop.write(this, QVariant::fromValue(list));
m_loadedKeys.insert(key);
continue;
}
if (prop.metaType().id() == QMetaType::QVariantList) {
prop.write(this, jsonVal.toVariant());
m_loadedKeys.insert(key);
continue;
}
prop.write(this, jsonVal.toVariant());
m_loadedKeys.insert(key);
}
m_extras = {};
for (auto it = obj.begin(); it != obj.end(); ++it) {
if (!known.contains(it.key()) || invalid.contains(it.key())) {
m_extras.insert(it.key(), it.value());
}
}
materializeDefaults();
}
QJsonValue ConfigObject::toJson() const {
QJsonObject obj;
const auto* meta = metaObject();
for (int i = basePropertyOffset(); i < meta->propertyCount(); ++i) {
const auto prop = meta->property(i);
if (!prop.isReadable()) continue;
const auto key = QString::fromUtf8(prop.name());
const auto value = prop.read(this);
if (prop.metaType().flags().testFlag(QMetaType::PointerToQObject)) {
if (auto* const node = value.value<ConfigNode*>()) {
const auto childJson = node->toJson();
if (!childJson.isUndefined()) obj.insert(key, childJson);
continue;
}
}
if (!m_loadedKeys.contains(key)) continue;
if (!prop.isWritable()) continue;
if (prop.metaType().id() == QMetaType::QStringList) {
QJsonArray arr;
const auto strList = value.toStringList();
for (const auto& s : strList)
arr.append(s);
obj.insert(key, arr);
continue;
}
if (prop.metaType().id() == QMetaType::QVariantList) {
obj.insert(key, QJsonArray::fromVariantList(value.toList()));
continue;
}
obj.insert(key, QJsonValue::fromVariant(value));
}
if (obj.isEmpty()) return QJsonValue::Undefined;
return obj;
}
void ConfigObject::clearLoadedKeys() {
m_loadedKeys.clear();
m_extras = {};
const auto* meta = metaObject();
for (int i = basePropertyOffset(); i < meta->propertyCount(); ++i) {
auto prop = meta->property(i);
if (auto* const node = prop.read(this).value<ConfigNode*>())
node->clearLoadedKeys();
}
}
QStringList ConfigObject::identityKeys() const {
return {};
}
QStringList ConfigObject::unknownKeys() const {
auto keys = m_extras.keys();
const auto* meta = metaObject();
for (int i = basePropertyOffset(); i < meta->propertyCount(); ++i) {
const auto prop = meta->property(i);
auto* const node = prop.read(this).value<ConfigNode*>();
if (!node) continue;
const auto childKeys = node->unknownKeys();
for (const auto& childKey : childKeys)
keys.append(joinPath(QString::fromUtf8(prop.name()), childKey));
}
return keys;
}
QList<ConfigNode*> ConfigObject::childNodes() const {
QList<ConfigNode*> nodes;
const auto* meta = metaObject();
for (int i = basePropertyOffset(); i < meta->propertyCount(); ++i) {
const auto prop = meta->property(i);
if (auto* const node = prop.read(this).value<ConfigNode*>())
nodes.append(node);
}
return nodes;
}
void ConfigObject::materializeDefaults() {
const auto* meta = metaObject();
for (int i = basePropertyOffset(); i < meta->propertyCount(); ++i) {
const auto prop = meta->property(i);
const auto key = QString::fromUtf8(prop.name());
if (auto* const node = prop.read(this).value<ConfigNode*>()) {
node->materializeDefaults();
continue;
}
if (!prop.isWritable()) continue;
if (m_global) continue;
m_loadedKeys.insert(key);
}
}
void ConfigObject::syncValuesFromGlobal() {
const auto* meta = metaObject();
for (int i = basePropertyOffset(); i < meta->propertyCount(); ++i) {
auto prop = meta->property(i);
const auto key = QString::fromUtf8(prop.name());
if (auto* const node = prop.read(this).value<ConfigNode*>()) {
if (auto* const globalNode =
prop.read(m_global).value<ConfigNode*>())
node->syncFromGlobal(globalNode);
continue;
}
if (!prop.isWritable()) continue;
if (!m_loadedKeys.contains(key)) {
auto val = prop.read(m_global);
prop.write(this, val);
m_loadedKeys.remove(key);
}
}
}
void ConfigObject::resyncFromGlobal() {
if (!m_global) return;
const auto* meta = metaObject();
for (int i = basePropertyOffset(); i < meta->propertyCount(); ++i) {
auto prop = meta->property(i);
const auto key = QString::fromUtf8(prop.name());
if (auto* const node = prop.read(this).value<ConfigNode*>()) {
node->resyncFromGlobal();
continue;
}
if (!prop.isWritable()) continue;
if (!m_loadedKeys.contains(key)) {
prop.write(this, prop.read(m_global));
m_loadedKeys.remove(key);
}
}
}
bool ConfigObject::isPropertyLoaded(const QString& name) const {
return m_loadedKeys.contains(name);
}
bool ConfigObject::isGlobalOnly(const QString& name) const {
return m_globalOnlyKeys.contains(name);
}
QStringList ConfigObject::globalOnlyKeys() const {
return {m_globalOnlyKeys.begin(), m_globalOnlyKeys.end()};
}
void ConfigObject::resetOption(const QString& name) {
m_loadedKeys.remove(name);
const int idx = metaObject()->indexOfProperty(name.toUtf8().constData());
if (idx < 0) return;
const auto prop = metaObject()->property(idx);
if (auto* const node = prop.read(this).value<ConfigNode*>()) {
node->clearLoadedKeys();
node->resyncFromGlobal();
return;
}
if (m_global && prop.isWritable()) prop.write(this, prop.read(m_global));
}
void ConfigObject::onGlobalPropertiesChanged(
const QMap<QString, QVariant>& changed) {
for (auto it = changed.begin(); it != changed.end(); ++it) {
if (m_loadedKeys.contains(it.key()) || isGlobalOnly(it.key())) continue;
int idx = metaObject()->indexOfProperty(it.key().toUtf8().constData());
if (idx >= 0) {
metaObject()->property(idx).write(this, it.value());
m_loadedKeys.remove(it.key());
}
}
}
QString ConfigObject::childPath(const ConfigNode* child) const {
const auto* meta = metaObject();
for (int i = basePropertyOffset(); i < meta->propertyCount(); ++i) {
const auto prop = meta->property(i);
if (prop.read(this).value<ConfigNode*>() == child)
return QString::fromUtf8(prop.name());
}
return {};
}
void ConfigObject::markPropertyLoaded(const QString& name) {
m_loadedKeys.insert(name);
}
void ConfigObject::markGlobalOnly(const QString& name) {
m_globalOnlyKeys.insert(name);
}
} // namespace ZShell::config
-101
View File
@@ -1,101 +0,0 @@
#pragma once
#include "confignode.hpp"
#include <QJsonObject>
#include <QMetaProperty>
#include <QMetaType>
#include <QObject>
#include <QSet>
#include <QStringList>
#include <QVariant>
#include <QVariantList>
namespace ZShell::config {
inline QVariantMap vmap(
std::initializer_list<std::pair<QString, QVariant>> entries) {
QVariantMap map;
for (const auto& [key, value] : entries)
map.insert(key, value);
return map;
}
#define CFG_PROPERTY(Type, name, ...) \
Q_PROPERTY(Type name READ name WRITE set_##name NOTIFY name##Changed) \
public: \
[[nodiscard]] Type name() const { \
return m_##name; \
} \
void set_##name(const Type& value) { \
if (ConfigObject::updateMember(m_##name, value)) { \
markPropertyLoaded(QStringLiteral(#name)); \
Q_EMIT name##Changed(); \
notifyPropertyChanged( \
QStringLiteral(#name), QVariant::fromValue(m_##name)); \
} \
} \
Q_SIGNAL void name##Changed(); \
\
private: \
Type m_##name __VA_OPT__(= __VA_ARGS__);
#define CONFIG_SUBOBJECT(Type, name) \
Q_PROPERTY(ZShell::config::Type* name READ name CONSTANT) \
\
public: \
[[nodiscard]] Type* name() const { \
return m_##name; \
} \
\
private: \
Type* m_##name = nullptr;
class ConfigObject : public ConfigNode {
Q_OBJECT
public:
explicit ConfigObject(QObject* parent = nullptr);
void loadFromJson(const QJsonValue& json) override;
[[nodiscard]] QJsonValue toJson() const override;
void clearLoadedKeys() override;
[[nodiscard]] QStringList unknownKeys() const override;
[[nodiscard]] QList<ConfigNode*> childNodes() const override;
void materializeDefaults() override;
void resyncFromGlobal() override;
[[nodiscard]] virtual QStringList identityKeys() const;
[[nodiscard]] bool isPropertyLoaded(const QString& name) const;
[[nodiscard]] bool isGlobalOnly(const QString& name) const;
[[nodiscard]] QStringList globalOnlyKeys() const;
Q_INVOKABLE void resetOption(const QString& name);
template <typename T> static bool updateMember(T& member, const T& value) {
if constexpr (std::is_floating_point_v<T>) {
if (qFuzzyCompare(member + 1.0, value + 1.0)) return false;
} else {
if (member == value) return false;
}
member = value;
return true;
}
protected:
void syncValuesFromGlobal() override;
void onGlobalPropertiesChanged(
const QMap<QString, QVariant>& changed) override;
[[nodiscard]] QString childPath(const ConfigNode* child) const override;
void markPropertyLoaded(const QString& name);
void markGlobalOnly(const QString& name);
private:
QSet<QString> m_loadedKeys;
QSet<QString> m_globalOnlyKeys;
QJsonObject m_extras;
};
} // namespace ZShell::config
+37 -47
View File
@@ -1,63 +1,53 @@
#pragma once
#include "configobject.hpp"
#include "common.hpp"
#include <qqmlintegration.h>
namespace ZShell::config {
class Performance : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class Performance : public settings::ObjectNode {
CONFIG_NODE(Performance, settings::ObjectNode)
CFG_PROPERTY(bool, enabled, true)
CFG_PROPERTY(bool, showBattery, false)
CFG_PROPERTY(bool, showCpu, true)
CFG_PROPERTY(bool, showGpu, true)
CFG_PROPERTY(bool, showMemory, true)
CFG_PROPERTY(bool, showNetwork, true)
CFG_PROPERTY(bool, showStorage, true)
CFG_PROPERTY(bool, showVram, true)
public:
explicit Performance(QObject* parent = nullptr) : ConfigObject(parent) {}
CONFIG_PROPERTY(bool, enabled, true)
CONFIG_PROPERTY(bool, showBattery, false)
CONFIG_PROPERTY(bool, showCpu, true)
CONFIG_PROPERTY(bool, showGpu, true)
CONFIG_PROPERTY(bool, showMemory, true)
CONFIG_PROPERTY(bool, showNetwork, true)
CONFIG_PROPERTY(bool, showStorage, true)
CONFIG_PROPERTY(bool, showVram, true)
};
class DashboardSizes : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class ProcessColumnEntry : public settings::ObjectNode {
CONFIG_NODE(ProcessColumnEntry, settings::ObjectNode)
CFG_PROPERTY(int, dateTimeWidth, 110)
CFG_PROPERTY(int, infoIconSize, 25)
CFG_PROPERTY(int, infoWidth, 200)
CFG_PROPERTY(int, mediaCoverArtSize, 150)
CFG_PROPERTY(int, mediaProgressSweep, 180)
CFG_PROPERTY(int, mediaProgressThickness, 8)
CFG_PROPERTY(int, mediaVisualizerSize, 200)
CFG_PROPERTY(int, mediaWidth, 200)
CFG_PROPERTY(int, resourceProgessThickness, 10)
CFG_PROPERTY(int, resourceSize, 200)
CFG_PROPERTY(int, tabIndicatorHeight, 3)
CFG_PROPERTY(int, tabIndicatorSpacing, 5)
CFG_PROPERTY(int, weatherWidth, 250)
public:
explicit DashboardSizes(QObject* parent = nullptr) : ConfigObject(parent) {}
CONFIG_PROPERTY(QString, id, {})
CONFIG_PROPERTY(double, width, 0)
};
class Dashboard : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
CONFIG_LIST_TYPE(ProcessColumnEntry, ProcessColumnList)
CFG_PROPERTY(bool, enabled, true)
CFG_PROPERTY(int, mediaUpdateInterval, 2000)
class Dashboard : public settings::ObjectNode {
CONFIG_NODE(Dashboard, settings::ObjectNode)
CONFIG_PROPERTY(bool, enabled, true)
CONFIG_PROPERTY(bool, showOnHover, true)
CONFIG_PROPERTY(int, dragThreshold, 50)
CONFIG_PROPERTY(int, mediaUpdateInterval, 2000)
CONFIG_PROPERTY(bool, showWeather, true)
CONFIG_PROPERTY(bool, showDashboard, true)
CONFIG_PROPERTY(bool, showMedia, true)
CONFIG_PROPERTY(bool, showProcesses, false)
CONFIG_SUBOBJECT(Performance, performance)
CFG_PROPERTY(int, resourceUpdateInterval, 1000)
CONFIG_SUBOBJECT(DashboardSizes, sizes)
public:
explicit Dashboard(QObject* parent = nullptr)
: ConfigObject(parent)
, m_performance(new Performance(this))
, m_sizes(new DashboardSizes(this)) {}
CONFIG_PROPERTY(int, resourceUpdateInterval, 1000)
CONFIG_LIST(
ProcessColumnList,
processColumns,
DEFAULT_ARG({
settings::vmap({{"id", "command"}}),
settings::vmap({{"id", "pid"}}),
settings::vmap({{"id", "cpuUsage"}}),
settings::vmap({{"id", "memoryUsage"}}),
}))
};
} // namespace ZShell::config
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include "common.hpp"
#include <qqmlintegration.h>
namespace ZShell::config {
class Nightlight : public settings::ObjectNode {
CONFIG_NODE(Nightlight, settings::ObjectNode)
CONFIG_PROPERTY(bool, schedule, true)
CONFIG_PROPERTY(int, scheduleStart, 1200)
CONFIG_PROPERTY(int, scheduleEnd, 570)
CONFIG_PROPERTY(bool, useNative, true)
CONFIG_PROPERTY(int, fadeDuration, 2000)
CONFIG_PROPERTY(int, temp, 2600)
};
class Display : public settings::ObjectNode {
CONFIG_NODE(Display, settings::ObjectNode)
CONFIG_SUBOBJECT(Nightlight, nightlight)
};
} // namespace ZShell::config
+9 -13
View File
@@ -1,29 +1,25 @@
#pragma once
#include "configobject.hpp"
#include "common.hpp"
#include <QStringList>
#include <qqmlintegration.h>
namespace ZShell::config {
class Dock : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class Dock : public settings::ObjectNode {
CONFIG_NODE(Dock, settings::ObjectNode)
CFG_PROPERTY(bool, enable, false)
CFG_PROPERTY(int, height, 80)
CFG_PROPERTY(bool, hoverToReveal, false)
CFG_PROPERTY(QStringList, ignoredAppRegexes, {})
CFG_PROPERTY(
CONFIG_PROPERTY(bool, enable, false)
CONFIG_PROPERTY(int, height, 80)
CONFIG_PROPERTY(bool, hoverToReveal, false)
CONFIG_PROPERTY(QStringList, ignoredAppRegexes, {})
CONFIG_PROPERTY(
QStringList,
pinnedApps,
(QStringList{
QStringLiteral("com.ayugram.desktop"),
QStringLiteral("com.obsproject.studio"),
QStringLiteral("librewolf")}))
CFG_PROPERTY(bool, pinnedOnStartup, false)
public:
explicit Dock(QObject* parent = nullptr) : ConfigObject(parent) {}
CONFIG_PROPERTY(bool, pinnedOnStartup, false)
};
} // namespace ZShell::config
+29
View File
@@ -0,0 +1,29 @@
#pragma once
#include <qobject.h>
#include <qqmlintegration.h>
namespace ZShell::config {
#define ENUM(Name, ...) \
namespace Name { \
\
Q_NAMESPACE \
QML_ELEMENT \
\
enum Enum : quint8 { __VA_ARGS__ }; \
Q_ENUM_NS(Enum) \
};
ENUM(BarWorkspaceDisplay, Shapes, Text, Icons)
ENUM(BarWorkspaceCapitalisation, Preserve, Upper, Lower)
ENUM(LyricsBackend, Auto, Local, LRCLIB, NetEase)
ENUM(GpuType, Auto, Nvidia, Generic, None)
ENUM(NotifsFullscreen, On, Off)
ENUM(TemperatureUnit, Auto, Celsius, Fahrenheit, Kelvin)
ENUM(DataUnit, Binary, Decimal)
ENUM(ClockFormat, Auto, TwelveHour, TwentyFourHour)
#undef ENUM
} // namespace ZShell::config
+288
View File
@@ -0,0 +1,288 @@
#include "font.hpp"
#include "appearance.hpp"
#include "rootnodes.hpp"
namespace ZShell::config {
FontStyleBase::FontStyleBase(QObject* parent) : QObject(parent) {}
QFont FontStyleBase::large() const {
return m_large;
}
QFont FontStyleBase::medium() const {
return m_medium;
}
QFont FontStyleBase::small() const {
return m_small;
}
QFont FontStyleBase::buildFont(
const FontSizeNode* cfg, const QString& fallbackFamily, qreal scale) {
const auto family = cfg->property("family").toString();
const auto size = cfg->property("size").toInt();
const auto weight = cfg->property("weight").value<QFont::Weight>();
const auto italic = cfg->property("italic").toBool();
const auto vaxes = cfg->property("vaxes").toMap();
QFont font;
font.setFamily(family.isEmpty() ? fallbackFamily : family);
const int scaledSize = static_cast<int>(size * scale);
const int cappedSize = scaledSize > 0 ? scaledSize : 1;
font.setPointSize(cappedSize);
font.setVariableAxis("opsz", static_cast<float>(cappedSize));
font.setWeight(weight);
font.setVariableAxis("wght", static_cast<float>(weight));
font.setItalic(italic);
for (auto it = vaxes.constBegin(); it != vaxes.constEnd(); ++it) {
if (auto tag = QFont::Tag::fromString(it.key()))
font.setVariableAxis(*tag, it.value().toFloat());
}
return font;
}
void FontStyleBase::bind(FontStyleNode* cfg) {
if (m_cfg == cfg) return;
if (m_cfg) {
disconnect(m_cfg, nullptr, this, nullptr);
disconnect(m_cfg->largeNode(), nullptr, this, nullptr);
disconnect(m_cfg->mediumNode(), nullptr, this, nullptr);
disconnect(m_cfg->smallNode(), nullptr, this, nullptr);
}
m_cfg = cfg;
if (cfg) {
connect(
cfg, &settings::Node::optionChanged, this, &FontStyleBase::rebuild);
connect(
cfg->largeNode(),
&settings::Node::optionChanged,
this,
&FontStyleBase::rebuild);
connect(
cfg->mediumNode(),
&settings::Node::optionChanged,
this,
&FontStyleBase::rebuild);
connect(
cfg->smallNode(),
&settings::Node::optionChanged,
this,
&FontStyleBase::rebuild);
}
rebuild();
}
void FontStyleBase::rebuild() {
if (m_cfg) {
const auto family = m_cfg->property("family").toString();
m_large = buildFont(m_cfg->largeNode(), family, m_scale);
m_medium = buildFont(m_cfg->mediumNode(), family, m_scale);
m_small = buildFont(m_cfg->smallNode(), family, m_scale);
} else {
m_large = QFont();
m_medium = QFont();
m_small = QFont();
}
emit fontsChanged();
}
void FontStyleBase::setScale(qreal scale) {
if (qFuzzyCompare(m_scale + 1.0, scale + 1.0)) return;
m_scale = scale;
rebuild();
}
FontStyle::FontStyle(QObject* parent)
: FontStyleBase(parent), m_builders(new FontBuilders(this, this)) {}
FontBuilders* FontStyle::builders() const {
return m_builders;
}
IconFontStyle::IconFontStyle(QObject* parent)
: FontStyleBase(parent), m_builders(new IconFontBuilders(this, this)) {}
FontBuilder IconFontStyle::size(int pointSize) {
return FontBuilder(m_small).size(pointSize);
}
FontBuilder IconFontStyle::pixelSize(int pixelSize) {
return FontBuilder(m_small).pixelSize(pixelSize);
}
void IconFontStyle::bind(FontStyleNode* cfg) {
if (m_cfg == cfg) return;
if (m_cfg) disconnect(m_cfg->extraLargeNode(), nullptr, this, nullptr);
FontStyleBase::bind(cfg);
if (cfg)
connect(
cfg->extraLargeNode(),
&settings::Node::optionChanged,
this,
&IconFontStyle::rebuild);
}
QFont IconFontStyle::extraLarge() const {
return m_extraLarge;
}
IconFontBuilders* IconFontStyle::builders() const {
return m_builders;
}
void IconFontStyle::rebuild() {
if (m_cfg) {
const auto family = m_cfg->property("family").toString();
m_extraLarge = buildFont(m_cfg->extraLargeNode(), family, m_scale);
} else {
m_extraLarge = QFont();
}
FontStyleBase::rebuild();
}
FontBuilders::FontBuilders(const FontStyleBase* style, QObject* parent)
: QObject(parent), m_style(style) {
connect(
style,
&FontStyleBase::fontsChanged,
this,
&FontBuilders::buildersChanged);
}
FontBuilder FontBuilders::large() const {
return FontBuilder(m_style->large());
}
FontBuilder FontBuilders::medium() const {
return FontBuilder(m_style->medium());
}
FontBuilder FontBuilders::small() const {
return FontBuilder(m_style->small());
}
IconFontBuilders::IconFontBuilders(const IconFontStyle* style, QObject* parent)
: FontBuilders(style, parent) {}
FontBuilder IconFontBuilders::extraLarge() const {
return FontBuilder(
static_cast<const IconFontStyle*>(m_style)->extraLarge());
}
FontTokens::FontTokens(QObject* parent)
: QObject(parent)
, m_headline(new FontStyle(this))
, m_title(new FontStyle(this))
, m_body(new FontStyle(this))
, m_label(new FontStyle(this))
, m_mono(new FontStyle(this))
, m_icon(new IconFontStyle(this)) {
bindFont(ConfigSingleton::instance()->appearance()->font());
}
FontStyle* FontTokens::headline() const {
return m_headline;
}
FontStyle* FontTokens::title() const {
return m_title;
}
FontStyle* FontTokens::body() const {
return m_body;
}
FontStyle* FontTokens::label() const {
return m_label;
}
FontStyle* FontTokens::mono() const {
return m_mono;
}
IconFontStyle* FontTokens::icon() const {
return m_icon;
}
FontBuilder FontTokens::clock() const {
return FontBuilder(m_clock);
}
QString FontTokens::workspaces() const {
return m_font ? m_font->workspaces() : QString();
}
void FontTokens::bindFont(AppearanceFont* font) {
if (m_font == font) return;
if (m_font) disconnect(m_font, nullptr, this, nullptr);
m_font = font;
if (font) {
rebuildScale();
m_headline->bind(font->headline());
m_title->bind(font->title());
m_body->bind(font->body());
m_label->bind(font->label());
m_mono->bind(font->mono());
m_icon->bind(font->icon());
connect(
font,
&AppearanceFont::clockChanged,
this,
&FontTokens::rebuildClock);
connect(
font,
&AppearanceFont::scaleChanged,
this,
&FontTokens::rebuildScale);
connect(
font,
&AppearanceFont::workspacesChanged,
this,
&FontTokens::workspacesChanged);
} else {
rebuildScale();
m_headline->bind(nullptr);
m_title->bind(nullptr);
m_body->bind(nullptr);
m_label->bind(nullptr);
m_mono->bind(nullptr);
m_icon->bind(nullptr);
}
rebuildClock();
emit workspacesChanged();
}
void FontTokens::rebuildScale() {
const qreal s = m_font ? m_font->scale() : 1;
m_headline->setScale(s);
m_title->setScale(s);
m_body->setScale(s);
m_label->setScale(s);
m_mono->setScale(s);
m_icon->setScale(s);
}
void FontTokens::rebuildClock() {
QFont f;
if (m_font) f.setFamily(m_font->clock());
m_clock = f;
emit clockChanged();
}
} // namespace ZShell::config
+182
View File
@@ -0,0 +1,182 @@
#pragma once
#include "fontbuilder.hpp"
#include <qfont.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qvariantmap.h>
namespace ZShell::config {
class AppearanceFont;
class FontBuilders;
class FontStyleBase;
class FontSizeNode;
class FontStyleNode;
class IconFontStyle;
class FontBuilders : public QObject {
Q_OBJECT
QML_ANONYMOUS
Q_PROPERTY(
ZShell::config::FontBuilder large READ large NOTIFY buildersChanged
FINAL)
Q_PROPERTY(
ZShell::config::FontBuilder medium READ medium NOTIFY buildersChanged
FINAL)
Q_PROPERTY(
ZShell::config::FontBuilder small READ small NOTIFY buildersChanged
FINAL)
public:
explicit FontBuilders(const FontStyleBase* style, QObject* parent = nullptr);
[[nodiscard]] FontBuilder large() const;
[[nodiscard]] FontBuilder medium() const;
[[nodiscard]] FontBuilder small() const;
signals:
void buildersChanged();
protected:
const FontStyleBase* m_style;
};
class IconFontBuilders : public FontBuilders {
Q_OBJECT
QML_ANONYMOUS
Q_PROPERTY(
ZShell::config::FontBuilder extraLarge READ extraLarge NOTIFY
buildersChanged FINAL)
public:
explicit IconFontBuilders(
const IconFontStyle* style, QObject* parent = nullptr);
[[nodiscard]] FontBuilder extraLarge() const;
};
class FontStyleBase : public QObject {
Q_OBJECT
Q_PROPERTY(QFont large READ large NOTIFY fontsChanged FINAL)
Q_PROPERTY(QFont medium READ medium NOTIFY fontsChanged FINAL)
Q_PROPERTY(QFont small READ small NOTIFY fontsChanged FINAL)
public:
explicit FontStyleBase(QObject* parent = nullptr);
virtual void bind(FontStyleNode* cfg);
void setScale(qreal scale);
[[nodiscard]] QFont large() const;
[[nodiscard]] QFont medium() const;
[[nodiscard]] QFont small() const;
signals:
void fontsChanged();
protected:
virtual void rebuild();
static QFont buildFont(
const FontSizeNode* cfg, const QString& fallbackFamily, qreal scale);
FontStyleNode* m_cfg = nullptr;
qreal m_scale = 1;
QFont m_large;
QFont m_medium;
QFont m_small;
};
class FontStyle : public FontStyleBase {
Q_OBJECT
QML_ANONYMOUS
Q_PROPERTY(FontBuilders* builders READ builders CONSTANT FINAL)
public:
explicit FontStyle(QObject* parent = nullptr);
[[nodiscard]] FontBuilders* builders() const;
private:
FontBuilders* m_builders;
};
class IconFontStyle : public FontStyleBase {
Q_OBJECT
QML_ANONYMOUS
Q_PROPERTY(QFont extraLarge READ extraLarge NOTIFY fontsChanged FINAL)
Q_PROPERTY(IconFontBuilders* builders READ builders CONSTANT FINAL)
public:
explicit IconFontStyle(QObject* parent = nullptr);
Q_INVOKABLE FontBuilder size(int pointSize);
Q_INVOKABLE FontBuilder pixelSize(int pixelSize);
void bind(FontStyleNode* cfg) override;
[[nodiscard]] QFont extraLarge() const;
[[nodiscard]] IconFontBuilders* builders() const;
protected:
void rebuild() override;
private:
QFont m_extraLarge;
IconFontBuilders* m_builders;
};
class FontTokens : public QObject {
Q_OBJECT
QML_ANONYMOUS
Q_PROPERTY(FontStyle* headline READ headline CONSTANT FINAL)
Q_PROPERTY(FontStyle* title READ title CONSTANT FINAL)
Q_PROPERTY(FontStyle* body READ body CONSTANT FINAL)
Q_PROPERTY(FontStyle* label READ label CONSTANT FINAL)
Q_PROPERTY(FontStyle* mono READ mono CONSTANT FINAL)
Q_PROPERTY(IconFontStyle* icon READ icon CONSTANT FINAL)
Q_PROPERTY(
ZShell::config::FontBuilder clock READ clock NOTIFY clockChanged FINAL)
Q_PROPERTY(QString workspaces READ workspaces NOTIFY workspacesChanged FINAL)
public:
explicit FontTokens(QObject* parent = nullptr);
void bindFont(AppearanceFont* font);
[[nodiscard]] FontStyle* headline() const;
[[nodiscard]] FontStyle* title() const;
[[nodiscard]] FontStyle* body() const;
[[nodiscard]] FontStyle* label() const;
[[nodiscard]] FontStyle* mono() const;
[[nodiscard]] IconFontStyle* icon() const;
[[nodiscard]] FontBuilder clock() const;
[[nodiscard]] QString workspaces() const;
signals:
void clockChanged();
void workspacesChanged();
private:
void rebuildClock();
void rebuildScale();
AppearanceFont* m_font = nullptr;
FontStyle* m_headline;
FontStyle* m_title;
FontStyle* m_body;
FontStyle* m_label;
FontStyle* m_mono;
IconFontStyle* m_icon;
QFont m_clock;
};
} // namespace ZShell::config
+105
View File
@@ -0,0 +1,105 @@
#include "fontbuilder.hpp"
#include <qloggingcategory.h>
namespace {
Q_LOGGING_CATEGORY(lcFontBuilder, "zshell.fontbuilder", QtInfoMsg)
} // namespace
namespace ZShell::config {
FontBuilder::FontBuilder(const QFont& font) : m_font(font) {}
FontBuilder FontBuilder::family(const QString& family) {
m_font.setFamily(family);
return *this;
}
FontBuilder FontBuilder::size(int pointSize) {
if (pointSize <= 0) pointSize = 1;
m_font.setPointSize(pointSize);
m_font.setVariableAxis("opsz", static_cast<float>(pointSize));
return *this;
}
FontBuilder FontBuilder::pixelSize(int pixelSize) {
if (pixelSize <= 0) pixelSize = 1;
m_font.setPixelSize(pixelSize);
return *this;
}
FontBuilder FontBuilder::weight(QFont::Weight weight) {
m_font.setWeight(weight);
m_font.setVariableAxis("wght", static_cast<float>(weight));
return *this;
}
FontBuilder FontBuilder::italic(bool on) {
m_font.setItalic(on);
return *this;
}
FontBuilder FontBuilder::stretch(int stretch) {
m_font.setStretch(stretch);
return *this;
}
FontBuilder FontBuilder::letterSpacing(qreal spacing, bool absolute) {
m_font.setLetterSpacing(
absolute ? QFont::AbsoluteSpacing : QFont::PercentageSpacing, spacing);
return *this;
}
FontBuilder FontBuilder::capitalisation(QFont::Capitalization cap) {
m_font.setCapitalization(cap);
return *this;
}
FontBuilder FontBuilder::vaxis(const QString& tag, float value) {
if (auto t = QFont::Tag::fromString(tag))
m_font.setVariableAxis(*t, value);
else
qCWarning(lcFontBuilder)
<< "Unable to convert tag" << tag << "to QFont::Tag";
return *this;
}
FontBuilder FontBuilder::vaxes(const QVariantMap& axes) {
for (auto it = axes.constBegin(); it != axes.constEnd(); ++it) {
if (it.value().canConvert<float>()) {
if (auto tag = QFont::Tag::fromString(it.key()))
m_font.setVariableAxis(*tag, it.value().toFloat());
else
qCWarning(lcFontBuilder)
<< "Unable to convert tag" << it.key() << "to QFont::Tag";
} else {
qCWarning(lcFontBuilder)
<< "Unable to convert value" << it.value() << "to float";
}
}
return *this;
}
QFont FontBuilder::build() const {
return m_font;
}
FontBuilder FontBuilder::fill(float value) {
return vaxis(QStringLiteral("FILL"), value);
}
FontBuilder FontBuilder::grade(float value) {
return vaxis(QStringLiteral("GRAD"), value);
}
FontBuilder FontBuilder::width(float value) {
return vaxis(QStringLiteral("wdth"), value);
}
FontBuilder FontBuilder::scale(qreal factor) {
return size(static_cast<int>(m_font.pointSize() * factor));
}
} // namespace ZShell::config
+41
View File
@@ -0,0 +1,41 @@
#pragma once
#include <qfont.h>
#include <qqmlintegration.h>
#include <qvariant.h>
namespace ZShell::config {
class FontBuilder {
Q_GADGET
QML_ANONYMOUS
public:
FontBuilder() = default;
explicit FontBuilder(const QFont& font);
[[nodiscard]] Q_INVOKABLE FontBuilder family(const QString& family);
[[nodiscard]] Q_INVOKABLE FontBuilder size(int pointSize);
[[nodiscard]] Q_INVOKABLE FontBuilder pixelSize(int pixelSize);
[[nodiscard]] Q_INVOKABLE FontBuilder weight(QFont::Weight weight);
[[nodiscard]] Q_INVOKABLE FontBuilder italic(bool on = true);
[[nodiscard]] Q_INVOKABLE FontBuilder stretch(int stretch);
[[nodiscard]] Q_INVOKABLE FontBuilder
letterSpacing(qreal spacing, bool absolute = true);
[[nodiscard]] Q_INVOKABLE FontBuilder
capitalisation(QFont::Capitalization cap);
[[nodiscard]] Q_INVOKABLE FontBuilder vaxis(const QString& tag, float value);
[[nodiscard]] Q_INVOKABLE FontBuilder vaxes(const QVariantMap& axes);
[[nodiscard]] Q_INVOKABLE QFont build() const;
[[nodiscard]] Q_INVOKABLE FontBuilder fill(float value);
[[nodiscard]] Q_INVOKABLE FontBuilder grade(float value);
[[nodiscard]] Q_INVOKABLE FontBuilder width(float value);
[[nodiscard]] Q_INVOKABLE FontBuilder scale(qreal factor);
private:
QFont m_font;
};
} // namespace ZShell::config
+48 -103
View File
@@ -1,158 +1,103 @@
#pragma once
#include "configlist.hpp"
#include "configobject.hpp"
#include "common.hpp"
#include <QStandardPaths>
#include <QStringList>
namespace ZShell::config {
class BatteryThresholdEntry : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class BatteryThresholdEntry : public settings::ObjectNode {
CONFIG_NODE(BatteryThresholdEntry, settings::ObjectNode)
CFG_PROPERTY(QString, icon)
CFG_PROPERTY(QString, message)
CFG_PROPERTY(QString, name)
CFG_PROPERTY(int, perc)
public:
explicit BatteryThresholdEntry(QObject* parent = nullptr)
: ConfigObject(parent) {}
[[nodiscard]] QStringList identityKeys() const override {
return {QStringLiteral("perc")};
}
CONFIG_PROPERTY(QString, icon, {})
CONFIG_PROPERTY(QString, message, {})
CONFIG_PROPERTY(QString, name, {})
CONFIG_PROPERTY(int, perc, {})
};
CONFIG_LIST_TYPE(BatteryThresholdEntry, BatteryThresholdList)
class IdleTimeoutEntry : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class IdleTimeoutEntry : public settings::ObjectNode {
CONFIG_NODE(IdleTimeoutEntry, settings::ObjectNode)
CFG_PROPERTY(QString, idleAction)
CFG_PROPERTY(QString, name)
CFG_PROPERTY(int, timeout)
public:
explicit IdleTimeoutEntry(QObject* parent = nullptr)
: ConfigObject(parent) {}
[[nodiscard]] QStringList identityKeys() const override {
return {QStringLiteral("name")};
}
CONFIG_PROPERTY(QString, idleAction, {})
CONFIG_PROPERTY(QString, name, {})
CONFIG_PROPERTY(int, timeout, {})
};
CONFIG_LIST_TYPE(IdleTimeoutEntry, IdleTimeoutList)
class Apps : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class Apps : public settings::ObjectNode {
CONFIG_NODE(Apps, settings::ObjectNode)
CFG_PROPERTY(QStringList, archiver, {QStringLiteral("ark")})
CFG_PROPERTY(QStringList, audio, {QStringLiteral("pavucontrol")})
CFG_PROPERTY(QStringList, document, {QStringLiteral("libreoffice")})
CFG_PROPERTY(QStringList, editor, {QStringLiteral("kate")})
CFG_PROPERTY(QStringList, explorer, {QStringLiteral("dolphin")})
CFG_PROPERTY(QStringList, image, {QStringLiteral("imv")})
CFG_PROPERTY(QStringList, playback, {QStringLiteral("vlc")})
CFG_PROPERTY(QStringList, terminal, {QStringLiteral("wezterm")})
public:
explicit Apps(QObject* parent = nullptr) : ConfigObject(parent) {}
CONFIG_PROPERTY(QStringList, archiver, {QStringLiteral("ark")})
CONFIG_PROPERTY(QStringList, audio, {QStringLiteral("pavucontrol")})
CONFIG_PROPERTY(QStringList, document, {QStringLiteral("libreoffice")})
CONFIG_PROPERTY(QStringList, editor, {QStringLiteral("kate")})
CONFIG_PROPERTY(QStringList, explorer, {QStringLiteral("dolphin")})
CONFIG_PROPERTY(QStringList, image, {QStringLiteral("imv")})
CONFIG_PROPERTY(QStringList, playback, {QStringLiteral("vlc")})
CONFIG_PROPERTY(QStringList, terminal, {QStringLiteral("wezterm")})
};
class Battery : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class Battery : public settings::ObjectNode {
CONFIG_NODE(Battery, settings::ObjectNode)
CFG_PROPERTY(int, critPerc, 5)
CONFIG_PROPERTY(int, critPerc, 5)
CONFIG_LIST(
BatteryThresholdList,
popupThresholds,
{
vmap(
DEFAULT_ARG({
settings::vmap(
{{"icon", "battery_android_frame_2"},
{"message", "Battery low"},
{"name", "Low battery"},
{"perc", 20}}),
vmap(
settings::vmap(
{{"icon", "battery_android_frame_2"},
{"message", "Battery lower"},
{"name", "Low battery"},
{"perc", 15}}),
vmap(
settings::vmap(
{{"icon", "battery_android_frame_2"},
{"message", "Battery lowest"},
{"name", "Low battery"},
{"perc", 10}}),
})
public:
explicit Battery(QObject* parent = nullptr) : ConfigObject(parent) {}
}))
};
class ColorSettings : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
CFG_PROPERTY(int, hyprsunsetTemp, 2600)
CFG_PROPERTY(QString, mode, QStringLiteral("dark"))
CFG_PROPERTY(bool, neovimColors, false)
CFG_PROPERTY(bool, scheduleDark, false)
CFG_PROPERTY(int, scheduleDarkEnd, 600)
CFG_PROPERTY(int, scheduleDarkStart, 1140)
CFG_PROPERTY(bool, scheduleHyprsunset, true)
CFG_PROPERTY(int, scheduleHyprsunsetEnd, 570)
CFG_PROPERTY(int, scheduleHyprsunsetStart, 1200)
CFG_PROPERTY(bool, schemeGeneration, true)
CFG_PROPERTY(bool, smart, false)
public:
explicit ColorSettings(QObject* parent = nullptr) : ConfigObject(parent) {}
};
class Idle : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class Idle : public settings::ObjectNode {
CONFIG_NODE(Idle, settings::ObjectNode)
CONFIG_LIST(
IdleTimeoutList,
timeouts,
{
vmap(
DEFAULT_ARG({
settings::vmap(
{{"idleAction", QStringLiteral("lock")},
{"name", QStringLiteral("Lock")},
{"timeout", 180}}),
})
public:
explicit Idle(QObject* parent = nullptr) : ConfigObject(parent) {}
}))
};
class General : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class General : public settings::ObjectNode {
CONFIG_NODE(General, settings::ObjectNode)
CONFIG_SUBOBJECT(Apps, apps)
CONFIG_SUBOBJECT(Battery, battery)
CONFIG_SUBOBJECT(ColorSettings, color)
CFG_PROPERTY(QString, dateFormat, QStringLiteral("ddd d MMM - hh:mm:ss"))
CFG_PROPERTY(bool, desktopIcons, true)
CONFIG_PROPERTY(QString, dateFormat, QStringLiteral("ddd d MMM - hh:mm:ss"))
CONFIG_PROPERTY(bool, desktopIcons, true)
CONFIG_SUBOBJECT(Idle, idle)
CFG_PROPERTY(QString, logo, QString())
CFG_PROPERTY(bool, showOverFullscreen, true)
CFG_PROPERTY(
CONFIG_PROPERTY(
QString,
lyricsDir,
QStandardPaths::writableLocation(QStandardPaths::MusicLocation) +
QStringLiteral("/Lyrics/"))
CONFIG_PROPERTY(QString, logo, QString())
CONFIG_PROPERTY(bool, showOverFullscreen, true)
CONFIG_PROPERTY(
QString,
wallpaperPath,
QStringLiteral("/mnt/IronWolf/SDImages/SWWW_Wals/"))
public:
explicit General(QObject* parent = nullptr)
: ConfigObject(parent)
, m_apps(new Apps(this))
, m_battery(new Battery(this))
, m_color(new ColorSettings(this))
, m_idle(new Idle(this)) {}
};
} // namespace ZShell::config
+39 -80
View File
@@ -1,68 +1,53 @@
#pragma once
#include "configlist.hpp"
#include "configobject.hpp"
#include "common.hpp"
#include <QStringList>
namespace ZShell::config {
class LauncherActionEntry : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
using Qt::StringLiterals::operator""_s;
using settings::vmap;
CFG_PROPERTY(QVariantList, command)
CFG_PROPERTY(bool, dangerous, false)
CFG_PROPERTY(QString, description)
CFG_PROPERTY(bool, enabled, true)
CFG_PROPERTY(QString, icon)
CFG_PROPERTY(QString, name)
class LauncherActionEntry : public settings::ObjectNode {
CONFIG_NODE(LauncherActionEntry, settings::ObjectNode)
public:
explicit LauncherActionEntry(QObject* parent = nullptr)
: ConfigObject(parent) {}
[[nodiscard]] QStringList identityKeys() const override {
return {QStringLiteral("name")};
}
CONFIG_PROPERTY(QVariantList, command, {})
CONFIG_PROPERTY(bool, dangerous, false)
CONFIG_PROPERTY(QString, description, {})
CONFIG_PROPERTY(bool, enabled, true)
CONFIG_PROPERTY(QString, icon, {})
CONFIG_PROPERTY(QString, name, {})
};
CONFIG_LIST_TYPE(LauncherActionEntry, LauncherActionList)
class LauncherSizes : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class LauncherSizes : public settings::ObjectNode {
CONFIG_NODE(LauncherSizes, settings::ObjectNode)
CFG_PROPERTY(int, itemHeight, 50)
CFG_PROPERTY(int, itemWidth, 600)
CFG_PROPERTY(int, wallpaperHeight, 200)
CFG_PROPERTY(int, wallpaperWidth, 280)
public:
explicit LauncherSizes(QObject* parent = nullptr) : ConfigObject(parent) {}
CONFIG_PROPERTY(int, itemHeight, 50)
CONFIG_PROPERTY(int, itemWidth, 600)
CONFIG_PROPERTY(int, wallpaperHeight, 200)
CONFIG_PROPERTY(int, wallpaperWidth, 280)
};
class UseFuzzy : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class UseFuzzy : public settings::ObjectNode {
CONFIG_NODE(UseFuzzy, settings::ObjectNode)
CFG_PROPERTY(bool, actions, true)
CFG_PROPERTY(bool, apps, true)
CFG_PROPERTY(bool, schemes, true)
CFG_PROPERTY(bool, variants, true)
CFG_PROPERTY(bool, wallpapers, true)
public:
explicit UseFuzzy(QObject* parent = nullptr) : ConfigObject(parent) {}
CONFIG_PROPERTY(bool, actions, true)
CONFIG_PROPERTY(bool, apps, true)
CONFIG_PROPERTY(bool, schemes, true)
CONFIG_PROPERTY(bool, variants, true)
CONFIG_PROPERTY(bool, wallpapers, true)
};
class Launcher : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class Launcher : public settings::ObjectNode {
CONFIG_NODE(Launcher, settings::ObjectNode)
CFG_PROPERTY(QString, actionPrefix, QStringLiteral("<"))
CONFIG_PROPERTY(QString, actionPrefix, QStringLiteral("<"))
CONFIG_LIST(
LauncherActionList,
actions,
{
DEFAULT_ARG({
vmap(
{{"command", QVariantList{"autocomplete", "calc"}},
{"dangerous", false},
@@ -134,44 +119,18 @@ class Launcher : public ConfigObject {
{"enabled", true},
{"icon", "bedtime"},
{"name", "Sleep"}}),
})
CFG_PROPERTY(int, dragThreshold, 150)
CFG_PROPERTY(bool, enableDangerousActions, true)
CFG_PROPERTY(bool, enabled, true)
CFG_PROPERTY(
QStringList,
favoriteApps,
{QStringLiteral("Balatro"),
QStringLiteral("Slay the Spire 2"),
QStringLiteral("com.obsproject.Studio")})
CFG_PROPERTY(
QStringList,
hiddenApps,
{QStringLiteral("zellij"),
QStringLiteral("vim"),
QStringLiteral("jshell-java-openjdk"),
QStringLiteral("jconsole-java-openjdk"),
QStringLiteral("jshell-java21-openjdk"),
QStringLiteral("jconsole-java21-openjdk"),
QStringLiteral("jshell-java17-openjdk"),
QStringLiteral("jconsole-java17-openjdk"),
QStringLiteral("org.neovim.nvim"),
QStringLiteral("avahi-discover"),
QStringLiteral("bvnc"),
QStringLiteral("bssh"),
QStringLiteral("nm-connection-editor")})
CFG_PROPERTY(int, maxAppsShown, 18)
CFG_PROPERTY(int, maxWallpapers, 7)
}))
CONFIG_PROPERTY(int, dragThreshold, 150)
CONFIG_PROPERTY(bool, enableDangerousActions, true)
CONFIG_PROPERTY(bool, enabled, true)
CONFIG_PROPERTY(QStringList, favoriteApps, {})
CONFIG_PROPERTY(QStringList, hiddenApps, {})
CONFIG_PROPERTY(int, maxAppsShown, 18)
CONFIG_PROPERTY(int, maxWallpapers, 7)
CONFIG_SUBOBJECT(LauncherSizes, sizes)
CFG_PROPERTY(QString, specialPrefix, QStringLiteral("@"))
CONFIG_PROPERTY(QString, specialPrefix, QStringLiteral("@"))
CONFIG_SUBOBJECT(UseFuzzy, useFuzzy)
CFG_PROPERTY(bool, uwsm, true)
public:
explicit Launcher(QObject* parent = nullptr)
: ConfigObject(parent)
, m_sizes(new LauncherSizes(this))
, m_useFuzzy(new UseFuzzy(this)) {}
CONFIG_PROPERTY(bool, uwsm, true)
};
} // namespace ZShell::config
+14 -23
View File
@@ -1,36 +1,27 @@
#pragma once
#include "configobject.hpp"
#include "common.hpp"
#include <qqmlintegration.h>
namespace ZShell::config {
class LockSizes : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class LockSizes : public settings::ObjectNode {
CONFIG_NODE(LockSizes, settings::ObjectNode)
CFG_PROPERTY(int, centerWidth, 600)
CFG_PROPERTY(qreal, heightMult, 0.7)
CFG_PROPERTY(qreal, ratio, 1.78)
public:
explicit LockSizes(QObject* parent = nullptr) : ConfigObject(parent) {}
CONFIG_PROPERTY(int, centerWidth, 600)
CONFIG_PROPERTY(qreal, heightMult, 0.7)
CONFIG_PROPERTY(qreal, ratio, 1.78)
};
class Lock : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class Lock : public settings::ObjectNode {
CONFIG_NODE(Lock, settings::ObjectNode)
CFG_PROPERTY(int, blurAmount, 10)
CFG_PROPERTY(bool, enableFprint, true)
CFG_PROPERTY(int, maxFprintTries, 3)
CFG_PROPERTY(bool, recolorLogo, false)
CFG_PROPERTY(bool, showNotifContent, true)
CFG_PROPERTY(bool, showNotifIcon, true)
CONFIG_PROPERTY(int, blurAmount, 10)
CONFIG_PROPERTY(bool, enableFprint, true)
CONFIG_PROPERTY(int, maxFprintTries, 3)
CONFIG_PROPERTY(bool, recolorLogo, false)
CONFIG_PROPERTY(bool, showNotifContent, true)
CONFIG_PROPERTY(bool, showNotifIcon, true)
CONFIG_SUBOBJECT(LockSizes, sizes)
public:
explicit Lock(QObject* parent = nullptr)
: ConfigObject(parent), m_sizes(new LockSizes(this)) {}
};
} // namespace ZShell::config
+123
View File
@@ -0,0 +1,123 @@
#include "migration.hpp"
#include <QDebug>
namespace ZShell::config {
namespace {
// A stored value is "missing" if the path does not resolve to an entry.
// Note: default-constructed QJsonValue is Null, not Undefined.
bool isMissing(const QJsonValue& v) {
return v.isUndefined() || v.isNull();
}
QJsonValue valueAt(const QJsonObject& obj, const QStringList& path) {
QJsonObject cur = obj;
for (int i = 0; i < path.size() - 1; ++i) {
if (!cur.value(path.at(i)).isObject()) return QJsonValue::Undefined;
cur = cur.value(path.at(i)).toObject();
}
const QString key = path.last();
if (!cur.contains(key)) return QJsonValue::Undefined;
return cur.value(key);
}
QJsonObject placeAt(QJsonObject obj, QStringList path, const QJsonValue& value) {
const QString head = path.takeFirst();
if (path.isEmpty()) {
obj[head] = value;
return obj;
}
QJsonObject child = obj.value(head).isObject() ? obj.value(head).toObject()
: QJsonObject();
obj[head] = placeAt(child, path, value);
return obj;
}
// Removes `path` from `obj`. Returns true if a value was removed.
// Parent objects left empty are removed as well.
bool removeAt(QJsonObject& obj, const QStringList& path) {
const QString head = path.first();
if (path.size() == 1) {
if (!obj.contains(head)) return false;
obj.remove(head);
return true;
}
if (!obj.value(head).isObject()) return false;
QJsonObject child = obj.value(head).toObject();
const bool removed = removeAt(child, path.mid(1));
if (child.isEmpty())
obj.remove(head);
else
obj[head] = child;
return removed;
}
} // namespace
const QList<ConfigMigrationRule>& ConfigMigrations::rules() {
static const QList<ConfigMigrationRule> s_rules = {
migrate(
"general.color.scheduleHyprsunset", "display.nightlight.schedule"),
migrate(
"general.color.scheduleHyprsunsetStart",
"display.nightlight.scheduleStart"),
migrate(
"general.color.scheduleHyprsunsetEnd",
"display.nightlight.scheduleEnd"),
migrate("general.color.scheduleDark", "colors.scheduleDark"),
migrate("general.color.scheduleDarkStart", "colors.scheduleDarkStart"),
migrate("general.color.scheduleDarkEnd", "colors.scheduleDarkEnd"),
migrate("general.color.schemeGeneration", "colors.schemeGen"),
migrate(
"general.color.useNativeNightlight",
"display.nightlight.useNative"),
migrate(
"general.color.nativeFadeDuration",
"display.nightlight.fadeDuration"),
migrate("general.color.hyprsunsetTemp", "display.nightlight.temp"),
migrate("general.color.neovimColors", "colors.neovimColors"),
migrate("general.color.mode", "colors.mode"),
migrate("general.color.smart", "colors.smart"),
};
return s_rules;
}
QJsonObject ConfigMigrations::apply(const QJsonObject& json) {
QJsonObject obj = json;
for (const auto& rule : rules()) {
const QStringList from =
rule.from.split(QLatin1Char('.'), Qt::SkipEmptyParts);
const QStringList to =
rule.to.split(QLatin1Char('.'), Qt::SkipEmptyParts);
if (from.isEmpty() || to.isEmpty()) continue;
const QJsonValue oldValue = valueAt(obj, from);
if (isMissing(oldValue)) continue;
const bool targetExists = !isMissing(valueAt(obj, to));
if (!targetExists) obj = placeAt(obj, to, oldValue);
if (removeAt(obj, from)) {
qInfo() << "Config migration:" << rule.from << "->" << rule.to
<< (targetExists ? "(target already set, old value dropped)"
: "(value moved)");
}
}
return obj;
}
} // namespace ZShell::config
+38
View File
@@ -0,0 +1,38 @@
#pragma once
#include <QJsonObject>
#include <QStringList>
namespace ZShell::config {
// A single migration rule: move the value stored at the dotted `from` path
// to the dotted `to` path, then delete the old key (and any parent objects
// left empty).
//
// Rules are applied to the raw JSON file on every load, before the schema
// loads it. They are idempotent: once the old key is gone the rule never
// fires again, so no version stamping is needed. Old rules can stay in the
// table forever.
//
// WARNING: the target path must exist in the schema (i.e. a ConfigObject
// property somewhere in the tree). If it does not, the migrated value is
// loaded as an unknown key and silently dropped on the next save.
struct ConfigMigrationRule {
QString from;
QString to;
};
// Helper for writing rules: migrate("general.color.foo", "display.bar.foo")
inline ConfigMigrationRule migrate(const QString& from, const QString& to) {
return {from, to};
}
class ConfigMigrations {
public:
[[nodiscard]] static const QList<ConfigMigrationRule>& rules();
// Applies all rules to `json`, returning the migrated object.
[[nodiscard]] static QJsonObject apply(const QJsonObject& json);
};
} // namespace ZShell::config
+17 -26
View File
@@ -1,39 +1,30 @@
#pragma once
#include "configobject.hpp"
#include "common.hpp"
#include <qqmlintegration.h>
namespace ZShell::config {
class NotifsSizes : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class NotifsSizes : public settings::ObjectNode {
CONFIG_NODE(NotifsSizes, settings::ObjectNode)
CFG_PROPERTY(int, badge, 20)
CFG_PROPERTY(int, image, 41)
CFG_PROPERTY(int, width, 400)
public:
explicit NotifsSizes(QObject* parent = nullptr) : ConfigObject(parent) {}
CONFIG_PROPERTY(int, badge, 20)
CONFIG_PROPERTY(int, image, 41)
CONFIG_PROPERTY(int, width, 400)
};
class Notifs : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class Notifs : public settings::ObjectNode {
CONFIG_NODE(Notifs, settings::ObjectNode)
CFG_PROPERTY(bool, actionOnClick, false)
CFG_PROPERTY(int, appNotifCooldown, 0)
CFG_PROPERTY(qreal, clearThreshold, 0.3)
CFG_PROPERTY(int, defaultExpireTimeout, 5000)
CFG_PROPERTY(int, expandThreshold, 20)
CFG_PROPERTY(bool, expire, true)
CFG_PROPERTY(int, groupPreviewNum, 5)
CFG_PROPERTY(bool, openExpanded, false)
CFG_PROPERTY(bool, showInFullscreen, false)
CONFIG_PROPERTY(bool, actionOnClick, false)
CONFIG_PROPERTY(int, appNotifCooldown, 0)
CONFIG_PROPERTY(qreal, clearThreshold, 0.3)
CONFIG_PROPERTY(int, defaultExpireTimeout, 5000)
CONFIG_PROPERTY(int, expandThreshold, 20)
CONFIG_PROPERTY(bool, expire, true)
CONFIG_PROPERTY(int, groupPreviewNum, 5)
CONFIG_PROPERTY(bool, openExpanded, false)
CONFIG_PROPERTY(bool, showInFullscreen, false)
CONFIG_SUBOBJECT(NotifsSizes, sizes)
public:
explicit Notifs(QObject* parent = nullptr)
: ConfigObject(parent), m_sizes(new NotifsSizes(this)) {}
};
} // namespace ZShell::config
+12 -21
View File
@@ -1,34 +1,25 @@
#pragma once
#include "configobject.hpp"
#include "common.hpp"
#include <qqmlintegration.h>
namespace ZShell::config {
class OsdSizes : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class OsdSizes : public settings::ObjectNode {
CONFIG_NODE(OsdSizes, settings::ObjectNode)
CFG_PROPERTY(int, sliderHeight, 150)
CFG_PROPERTY(int, sliderWidth, 30)
public:
explicit OsdSizes(QObject* parent = nullptr) : ConfigObject(parent) {}
CONFIG_PROPERTY(int, sliderHeight, 150)
CONFIG_PROPERTY(int, sliderWidth, 30)
};
class Osd : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class Osd : public settings::ObjectNode {
CONFIG_NODE(Osd, settings::ObjectNode)
CFG_PROPERTY(bool, allMonBrightness, true)
CFG_PROPERTY(bool, enableBrightness, true)
CFG_PROPERTY(bool, enableMicrophone, true)
CFG_PROPERTY(bool, enabled, true)
CFG_PROPERTY(int, hideDelay, 3000)
CONFIG_PROPERTY(bool, allMonBrightness, true)
CONFIG_PROPERTY(bool, enableBrightness, true)
CONFIG_PROPERTY(bool, enableMicrophone, true)
CONFIG_PROPERTY(bool, enabled, true)
CONFIG_PROPERTY(int, hideDelay, 3000)
CONFIG_SUBOBJECT(OsdSizes, sizes)
public:
explicit Osd(QObject* parent = nullptr)
: ConfigObject(parent), m_sizes(new OsdSizes(this)) {}
};
} // namespace ZShell::config
+191
View File
@@ -0,0 +1,191 @@
#include "rootnodes.hpp"
#include <qjsondocument.h>
#include <qsavefile.h>
#include "../toaster.hpp"
#include "common.hpp"
#include "migration.hpp"
namespace ZShell::config {
using Qt::StringLiterals::operator""_s;
namespace {
QString nameFor(const QString& key) {
return key.isEmpty() ? u"global"_s : key;
}
} // namespace
namespace detail {
void loaded(ConfigKind kind, settings::RootNode* layer, const QString& screen) {
if (kind != ConfigKind::Shell || !screen.isEmpty()) return;
auto* const config = static_cast<ConfigRoot*>(layer);
if (!config->utilities()->toasts()->configLoaded()) return;
const auto issues = static_cast<int>(config->diagnostics().count());
const auto message = issues > 0
? QStringLiteral("Config loaded with %1 issue%2")
.arg(issues)
.arg(issues == 1 ? "" : "s")
: QStringLiteral("Config loaded successfully!");
Toaster::instance()->toast(
QStringLiteral("Config loaded"),
message,
issues > 0 ? QStringLiteral("settings_alert")
: QStringLiteral("rule_settings"),
issues > 0 ? Toast::Type::Warning : Toast::Type::Info);
}
void loadFailed(ConfigKind kind, const QString& error, const QString& screen) {
QString title;
if (kind == ConfigKind::Tokens)
title = screen.isEmpty()
? QStringLiteral("Failed to parse token config")
: QStringLiteral("Failed to parse token config for %1")
.arg(screen);
else
title =
screen.isEmpty()
? QStringLiteral("Failed to parse config")
: QStringLiteral("Failed to parse config for %1").arg(screen);
Toaster::instance()->toast(
title, error, QStringLiteral("settings_alert"), Toast::Type::Warning);
}
void saveFailed(ConfigKind kind, const QString& error, const QString& screen) {
if (kind != ConfigKind::Shell) return;
const auto title =
screen.isEmpty()
? QStringLiteral("Failed to save config")
: QStringLiteral("Failed to save config for %1").arg(screen);
Toaster::instance()->toast(
title, error, QStringLiteral("settings_alert"), Toast::Type::Error);
}
} // namespace detail
ConfigRoot::ConfigRoot(
const QString& path, ConfigRoot* fallback, QObject* parent)
: RootNode(path, fallback, parent) {
migrateFile(path);
bindTokens();
qCDebug(lcConfig) << "Created config root for" << nameFor(key());
}
void ConfigRoot::migrateFile(const QString& path) {
QFile file(path);
if (!file.exists() || !file.open(QIODevice::ReadOnly)) return;
const auto doc = QJsonDocument::fromJson(file.readAll());
if (!doc.isObject()) return;
const QJsonObject raw = doc.object();
const QJsonObject migrated = ConfigMigrations::apply(raw);
if (migrated == raw) return;
file.close();
QSaveFile save(path);
if (!save.open(QIODevice::WriteOnly)) {
qCWarning(lcConfig) << "Failed to open" << path
<< "for migration write:" << save.errorString();
return;
}
if (save.write(QJsonDocument(migrated).toJson(QJsonDocument::Indented)) !=
save.size()) {
qCWarning(lcConfig)
<< "Failed to write migrated config:" << save.errorString();
return;
}
if (!save.commit()) {
qCWarning(lcConfig)
<< "Failed to commit migrated config:" << save.errorString();
return;
}
qCDebug(lcConfig) << "Migrated config file" << path;
}
void ConfigRoot::bindTokens() {
qCDebug(lcConfig) << "Binding appearance to token values for"
<< nameFor(key());
auto* const tokens = TokensSingleton::instance()->appearance();
m_appearance->rounding()->bindTokens(tokens->rounding());
m_appearance->spacing()->bindTokens(tokens->spacing());
m_appearance->padding()->bindTokens(tokens->padding());
m_appearance->anim()->durations()->bindTokens(tokens->animDurations());
}
TokensRoot::TokensRoot(
const QString& path, TokensRoot* fallback, QObject* parent)
: RootNode(path, fallback, parent) {
qCDebug(lcConfig) << "Created tokens root for" << nameFor(key());
}
#define SINGLETON_IMPL(Type, Root, file, kind) \
Type* Type::instance() { \
static Type instance; \
return &instance; \
} \
\
Type* Type::create(QQmlEngine*, QJSEngine*) { \
QQmlEngine::setObjectOwnership(instance(), QQmlEngine::CppOwnership); \
return instance(); \
} \
\
Root* Type::forScreen(const QString& screen) { \
bool created; \
auto* const layer = m_layers.get(screen, this, &created); \
if (created) initLayer(layer); \
return layer; \
} \
\
Type::Type(QObject* parent) \
: Root(configDir() + u'/' + file, nullptr, parent) \
, m_layers(monitorConfigDir(), file, this) { \
initLayer(this); \
} \
\
void Type::onTreeLoaded(settings::RootNode* layer) { \
detail::loaded( \
kind, layer, m_layers.nameFor(static_cast<Root*>(layer))); \
} \
\
void Type::onTreeLoadFailed( \
settings::RootNode* layer, const QString& error) { \
detail::loadFailed( \
kind, error, m_layers.nameFor(static_cast<Root*>(layer))); \
} \
\
void Type::onTreeSaveFailed( \
settings::RootNode* layer, const QString& error) { \
detail::saveFailed( \
kind, error, m_layers.nameFor(static_cast<Root*>(layer))); \
} \
\
void Type::initLayer(Root* layer) { \
QObject::connect(layer, &Root::treeLoaded, this, &Type::onTreeLoaded); \
QObject::connect( \
layer, &Root::treeLoadFailed, this, &Type::onTreeLoadFailed); \
QObject::connect( \
layer, &Root::treeSaveFailed, this, &Type::onTreeSaveFailed); \
layer->load(); \
}
SINGLETON_IMPL(
ConfigSingleton, ConfigRoot, u"config.json"_s, detail::ConfigKind::Shell)
SINGLETON_IMPL(
TokensSingleton, TokensRoot, u"tokens.json"_s, detail::ConfigKind::Tokens)
#undef SINGLETON_IMPL
} // namespace ZShell::config
+168
View File
@@ -0,0 +1,168 @@
#pragma once
#include <qjsengine.h>
#include <qqmlengine.h>
#include "settings/layerregistry.hpp"
#include "settings/rootnode.hpp"
#include "appearance.hpp"
#include "background.hpp"
#include "bar.hpp"
#include "clipboard.hpp"
#include "colors.hpp"
#include "common.hpp"
#include "dashboard.hpp"
#include "display.hpp"
#include "dock.hpp"
#include "general.hpp"
#include "launcher.hpp"
#include "llm.hpp"
#include "lock.hpp"
#include "notifs.hpp"
#include "osd.hpp"
#include "screenshot.hpp"
#include "services.hpp"
#include "sidebar.hpp"
#include "tokens.hpp"
#include "utilities.hpp"
namespace ZShell::config {
class ConfigRoot : public settings::RootNode {
CONFIG_NODE_NO_CTOR(ConfigRoot, settings::RootNode)
QML_ANONYMOUS
CONFIG_PROPERTY(bool, enabled, true)
CONFIG_SUBOBJECT(Appearance, appearance)
CONFIG_SUBOBJECT(Background, background)
CONFIG_SUBOBJECT(Bar, bar)
CONFIG_SUBOBJECT(Clipboard, clipboard)
CONFIG_SUBOBJECT(Colors, colors)
CONFIG_SUBOBJECT(Dashboard, dashboard)
CONFIG_SUBOBJECT(Display, display)
CONFIG_SUBOBJECT(Dock, dock)
CONFIG_SUBOBJECT(General, general)
CONFIG_SUBOBJECT(Launcher, launcher)
CONFIG_SUBOBJECT(Llm, llm)
CONFIG_SUBOBJECT(Lock, lock)
CONFIG_SUBOBJECT(Notifs, notifs)
CONFIG_SUBOBJECT(Osd, osd)
CONFIG_SUBOBJECT(Screenshot, screenshot)
CONFIG_SUBOBJECT(Services, services)
CONFIG_SUBOBJECT(Sidebar, sidebar)
CONFIG_SUBOBJECT(Utilities, utilities)
public:
explicit ConfigRoot(
const QString& path,
ConfigRoot* fallback = nullptr,
QObject* parent = nullptr);
private:
// Binds the computed appearance values to the global token base values
void bindTokens();
// Applies ConfigMigrations to the file before the first load
void migrateFile(const QString& path);
};
class TokensRoot : public settings::RootNode {
CONFIG_NODE_NO_CTOR(TokensRoot, settings::RootNode)
QML_ANONYMOUS
CONFIG_SUBOBJECT(AppearanceTokens, appearance)
CONFIG_SUBOBJECT(SizeTokens, sizes)
public:
explicit TokensRoot(
const QString& path,
TokensRoot* fallback = nullptr,
QObject* parent = nullptr);
};
namespace detail {
enum class ConfigKind : quint8 { Shell, Tokens };
void loaded(ConfigKind kind, settings::RootNode* layer, const QString& screen);
void loadFailed(ConfigKind kind, const QString& error, const QString& screen);
void saveFailed(ConfigKind kind, const QString& error, const QString& screen);
} // namespace detail
#define SINGLETON_DECL(Type, Root, QmlName) \
class Type : public Root { \
Q_OBJECT \
QML_NAMED_ELEMENT(QmlName) \
QML_SINGLETON \
\
public: \
static Type* instance(); \
static Type* create(QQmlEngine*, QJSEngine*); \
\
[[nodiscard]] Q_INVOKABLE Root* forScreen(const QString& screen); \
\
private: \
explicit Type(QObject* parent = nullptr); \
\
void onTreeLoaded(settings::RootNode* layer); \
void onTreeLoadFailed(settings::RootNode* layer, const QString& error); \
void onTreeSaveFailed(settings::RootNode* layer, const QString& error); \
\
void initLayer(Root* layer); \
\
settings::LayerRegistry<Root> m_layers; \
};
SINGLETON_DECL(ConfigSingleton, ConfigRoot, Config)
SINGLETON_DECL(TokensSingleton, TokensRoot, TokenConfig)
#undef SINGLETON_DECL
class Tokens : public QObject {
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
Q_PROPERTY(
ZShell::config::AppearanceRounding* rounding READ rounding CONSTANT)
Q_PROPERTY(ZShell::config::AppearanceSpacing* spacing READ spacing CONSTANT)
Q_PROPERTY(ZShell::config::AppearancePadding* padding READ padding CONSTANT)
Q_PROPERTY(ZShell::config::SizeTokens* sizes READ sizes CONSTANT)
Q_PROPERTY(ZShell::config::FontTokens* font READ font CONSTANT)
Q_PROPERTY(ZShell::config::AnimTokens* anim READ anim CONSTANT)
public:
explicit Tokens(QObject* parent = nullptr)
: QObject(parent)
, m_font(new FontTokens(this))
, m_anim(new AnimTokens(this)) {
m_anim->bindCurves(TokensSingleton::instance()->appearance()->curves());
m_anim->bindDurations(
ConfigSingleton::instance()->appearance()->anim()->durations());
m_font->bindFont(ConfigSingleton::instance()->appearance()->font());
}
[[nodiscard]] AppearanceRounding* rounding() const {
return ConfigSingleton::instance()->appearance()->rounding();
}
[[nodiscard]] AppearancePadding* padding() const {
return ConfigSingleton::instance()->appearance()->padding();
}
[[nodiscard]] AppearanceSpacing* spacing() const {
return ConfigSingleton::instance()->appearance()->spacing();
}
[[nodiscard]] SizeTokens* sizes() const {
return TokensSingleton::instance()->sizes();
}
[[nodiscard]] FontTokens* font() const { return m_font; }
[[nodiscard]] AnimTokens* anim() const { return m_anim; }
static Tokens* create(QQmlEngine*, QJSEngine*) { return new Tokens(); }
private:
FontTokens* m_font;
AnimTokens* m_anim;
};
} // namespace ZShell::config
+13 -17
View File
@@ -1,27 +1,23 @@
#pragma once
#include "configobject.hpp"
#include "common.hpp"
#include <QVariantList>
#include <qqmlintegration.h>
namespace ZShell::config {
class Screenshot : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class Screenshot : public settings::ObjectNode {
CONFIG_NODE(Screenshot, settings::ObjectNode)
CFG_PROPERTY(bool, enable_pp, false)
CFG_PROPERTY(QString, mode, QStringLiteral("manual"))
CFG_PROPERTY(int, radius, 49)
CFG_PROPERTY(bool, rounding, false)
CFG_PROPERTY(bool, saveOnCopy, false)
CFG_PROPERTY(bool, shadow, true)
CFG_PROPERTY(int, shadow_blur, 0)
CFG_PROPERTY(QVariantList, shadow_color, (QVariantList{0, 0, 0, 160}))
CFG_PROPERTY(int, shadow_offset_x, 30)
CFG_PROPERTY(int, shadow_offset_y, 50)
public:
explicit Screenshot(QObject* parent = nullptr) : ConfigObject(parent) {}
CONFIG_PROPERTY(bool, enable_pp, false)
CONFIG_PROPERTY(QString, mode, QStringLiteral("manual"))
CONFIG_PROPERTY(int, radius, 49)
CONFIG_PROPERTY(bool, rounding, false)
CONFIG_PROPERTY(bool, saveOnCopy, false)
CONFIG_PROPERTY(bool, shadow, true)
CONFIG_PROPERTY(int, shadow_blur, 0)
CONFIG_PROPERTY(QVariantList, shadow_color, (QVariantList{0, 0, 0, 160}))
CONFIG_PROPERTY(int, shadow_offset_x, 30)
CONFIG_PROPERTY(int, shadow_offset_y, 50)
};
} // namespace ZShell::config
+25 -36
View File
@@ -1,54 +1,43 @@
#pragma once
#include "configlist.hpp"
#include "configobject.hpp"
#include "common.hpp"
#include "enums.hpp"
namespace ZShell::config {
class PlayerAliasEntry : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class PlayerAliasEntry : public settings::ObjectNode {
CONFIG_NODE(PlayerAliasEntry, settings::ObjectNode)
CFG_PROPERTY(QString, from)
CFG_PROPERTY(QString, to)
public:
explicit PlayerAliasEntry(QObject* parent = nullptr)
: ConfigObject(parent) {}
[[nodiscard]] QStringList identityKeys() const override {
return {QStringLiteral("from")};
}
CONFIG_PROPERTY(QString, from, {})
CONFIG_PROPERTY(QString, to, {})
};
CONFIG_LIST_TYPE(PlayerAliasEntry, PlayerAliasList)
class Services : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class Services : public settings::ObjectNode {
CONFIG_NODE(Services, settings::ObjectNode)
CFG_PROPERTY(qreal, audioIncrement, 0.1)
CFG_PROPERTY(qreal, brightnessIncrement, 0.1)
CFG_PROPERTY(bool, ddcutilService, true)
CFG_PROPERTY(QString, defaultPlayer, QStringLiteral("Spotify"))
CFG_PROPERTY(QString, gpuType, QStringLiteral("NVIDIA"))
CFG_PROPERTY(qreal, maxVolume, 1)
CFG_PROPERTY(qreal, minBrightness, 0.01)
CONFIG_PROPERTY(qreal, audioIncrement, 0.1)
CONFIG_PROPERTY(qreal, brightnessIncrement, 0.1)
CONFIG_PROPERTY(bool, ddcutilService, true)
CONFIG_PROPERTY(QString, defaultPlayer, QStringLiteral("Spotify"))
CONFIG_ENUM_PROPERTY(GpuType, gpuType, GpuType::Auto)
CONFIG_PROPERTY(LyricsBackend::Enum, lyricsBackend, LyricsBackend::Auto)
CONFIG_PROPERTY(qreal, maxVolume, 1)
CONFIG_PROPERTY(qreal, minBrightness, 0.01)
CONFIG_LIST(
PlayerAliasList,
playerAliases,
{
vmap(
DEFAULT_ARG({
settings::vmap(
{{"from", QStringLiteral("com.github.th_ch.youtube_music")},
{"to", QStringLiteral("YT Music")}}),
})
CFG_PROPERTY(bool, updates, true)
CFG_PROPERTY(bool, useFahrenheit, false)
CFG_PROPERTY(bool, useTwelveHourClock, false)
CFG_PROPERTY(int, visualizerBars, 50)
CFG_PROPERTY(QString, weatherLocation, QString())
public:
explicit Services(QObject* parent = nullptr) : ConfigObject(parent) {}
}))
CONFIG_PROPERTY(bool, updates, true)
CONFIG_PROPERTY(bool, useFahrenheit, false)
CONFIG_PROPERTY(bool, useTwelveHourClock, false)
CONFIG_PROPERTY(int, visualizerBars, 50)
CONFIG_PROPERTY(QString, weatherLocation, QString())
};
} // namespace ZShell::config
+8 -17
View File
@@ -1,30 +1,21 @@
#pragma once
#include "configobject.hpp"
#include "common.hpp"
#include <qqmlintegration.h>
namespace ZShell::config {
class SidebarSizes : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class SidebarSizes : public settings::ObjectNode {
CONFIG_NODE(SidebarSizes, settings::ObjectNode)
CFG_PROPERTY(int, width, 520)
public:
explicit SidebarSizes(QObject* parent = nullptr) : ConfigObject(parent) {}
CONFIG_PROPERTY(int, width, 520)
};
class Sidebar : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class Sidebar : public settings::ObjectNode {
CONFIG_NODE(Sidebar, settings::ObjectNode)
CFG_PROPERTY(int, dragThreshold, 30)
CFG_PROPERTY(bool, enabled, true)
CONFIG_PROPERTY(int, dragThreshold, 30)
CONFIG_PROPERTY(bool, enabled, true)
CONFIG_SUBOBJECT(SidebarSizes, sizes)
public:
explicit Sidebar(QObject* parent = nullptr)
: ConfigObject(parent), m_sizes(new SidebarSizes(this)) {}
};
} // namespace ZShell::config
+136 -254
View File
@@ -1,292 +1,174 @@
#pragma once
#include "configobject.hpp"
#include "anim.hpp"
#include <QVariantList>
#include <limits>
#include <qlist.h>
#include <qobject.h>
#include <qqmlengine.h>
#include <qqmlintegration.h>
#include <qqmlregistration.h>
#include <qtypes.h>
#include <limits>
#include "settings/objectnode.hpp"
#include "anim.hpp"
#include "common.hpp"
#include "font.hpp"
namespace ZShell::config {
class AppearanceRounding : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class AnimCurves : public settings::ObjectNode {
CONFIG_NODE(AnimCurves, settings::ObjectNode)
CFG_PROPERTY(qreal, extraSmall, 4)
CFG_PROPERTY(qreal, full, std::numeric_limits<int>::max())
CFG_PROPERTY(qreal, large, 16)
CFG_PROPERTY(qreal, largeIncreased, 20)
CFG_PROPERTY(qreal, extraLarge, 28)
CFG_PROPERTY(qreal, extraLargeIncreased, 32)
CFG_PROPERTY(qreal, extraExtraLarge, 48)
CFG_PROPERTY(qreal, medium, 12)
CFG_PROPERTY(qreal, scale, 1)
CFG_PROPERTY(qreal, small, 8)
public:
explicit AppearanceRounding(QObject* parent = nullptr)
: ConfigObject(parent) {}
CONFIG_GLOBAL_PROPERTY(
QList<qreal>,
emphasized,
DEFAULT_ARG(
{0.05,
0,
2.0 / 15.0,
0.06,
1.0 / 6.0,
0.4,
5.0 / 24.0,
0.82,
0.25,
1,
1,
1}))
CONFIG_GLOBAL_PROPERTY(
QList<qreal>, emphasizedAccel, DEFAULT_ARG({0.3, 0, 0.8, 0.15, 1, 1}))
CONFIG_GLOBAL_PROPERTY(
QList<qreal>, emphasizedDecel, DEFAULT_ARG({0.05, 0.7, 0.1, 1, 1, 1}))
CONFIG_GLOBAL_PROPERTY(
QList<qreal>, standard, DEFAULT_ARG({0.2, 0, 0, 1, 1, 1}))
CONFIG_GLOBAL_PROPERTY(
QList<qreal>, standardAccel, DEFAULT_ARG({0.3, 0, 1, 1, 1, 1}))
CONFIG_GLOBAL_PROPERTY(
QList<qreal>, standardDecel, DEFAULT_ARG({0, 0, 0, 1, 1, 1}))
CONFIG_GLOBAL_PROPERTY(
QList<qreal>,
expressiveFastSpatial,
DEFAULT_ARG({0.42, 1.67, 0.21, 0.9, 1, 1}))
CONFIG_GLOBAL_PROPERTY(
QList<qreal>,
expressiveDefaultSpatial,
DEFAULT_ARG({0.38, 1.21, 0.22, 1, 1, 1}))
CONFIG_GLOBAL_PROPERTY(
QList<qreal>,
expressiveSlowSpatial,
DEFAULT_ARG({0.39, 1.29, 0.35, 0.98, 1, 1}))
CONFIG_GLOBAL_PROPERTY(
QList<qreal>,
expressiveFastEffects,
DEFAULT_ARG({0.31, 0.94, 0.34, 1, 1, 1}))
CONFIG_GLOBAL_PROPERTY(
QList<qreal>,
expressiveDefaultEffects,
DEFAULT_ARG({0.34, 0.8, 0.34, 1, 1, 1}))
CONFIG_GLOBAL_PROPERTY(
QList<qreal>,
expressiveSlowEffects,
DEFAULT_ARG({0.34, 0.88, 0.34, 1, 1, 1}))
};
class AppearancePadding : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class RoundingTokens : public settings::ObjectNode {
CONFIG_NODE(RoundingTokens, settings::ObjectNode)
CFG_PROPERTY(int, extraSmall, 4)
CFG_PROPERTY(int, smaller, 5)
CFG_PROPERTY(int, small, 8)
CFG_PROPERTY(int, medium, 12)
CFG_PROPERTY(int, large, 16)
CFG_PROPERTY(int, largeIncreased, 20)
CFG_PROPERTY(int, extraLarge, 28)
CFG_PROPERTY(int, extraLargeIncreased, 32)
CFG_PROPERTY(int, extraExtraLarge, 48)
public:
explicit AppearancePadding(QObject* parent = nullptr)
: ConfigObject(parent) {}
CONFIG_PROPERTY(int, extraSmall, 4)
CONFIG_PROPERTY(int, small, 8)
CONFIG_PROPERTY(int, medium, 12)
CONFIG_PROPERTY(int, large, 16)
CONFIG_PROPERTY(int, largeIncreased, 20)
CONFIG_PROPERTY(int, extraLarge, 28)
CONFIG_PROPERTY(int, extraLargeIncreased, 32)
CONFIG_PROPERTY(int, extraExtraLarge, 48)
CONFIG_PROPERTY(int, full, std::numeric_limits<int>::max())
};
class AppearanceSpacing : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class SpacingTokens : public settings::ObjectNode {
CONFIG_NODE(SpacingTokens, settings::ObjectNode)
CFG_PROPERTY(int, extraSmall, 4)
CFG_PROPERTY(int, small, 8)
CFG_PROPERTY(int, medium, 12)
CFG_PROPERTY(int, large, 16)
CFG_PROPERTY(int, largeIncreased, 20)
CFG_PROPERTY(int, extraLarge, 28)
CFG_PROPERTY(int, extraLargeIncreased, 32)
CFG_PROPERTY(int, extraExtraLarge, 48)
public:
explicit AppearanceSpacing(QObject* parent = nullptr)
: ConfigObject(parent) {}
CONFIG_PROPERTY(int, extraSmall, 4)
CONFIG_PROPERTY(int, small, 8)
CONFIG_PROPERTY(int, medium, 12)
CONFIG_PROPERTY(int, large, 16)
CONFIG_PROPERTY(int, largeIncreased, 20)
CONFIG_PROPERTY(int, extraLarge, 28)
CONFIG_PROPERTY(int, extraLargeIncreased, 32)
CONFIG_PROPERTY(int, extraExtraLarge, 48)
};
class FontSizeTokens : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class PaddingTokens : public settings::ObjectNode {
CONFIG_NODE(PaddingTokens, settings::ObjectNode)
CFG_PROPERTY(int, small, 11)
CFG_PROPERTY(int, smaller, 12)
CFG_PROPERTY(int, normal, 13)
CFG_PROPERTY(int, larger, 15)
CFG_PROPERTY(int, large, 18)
CFG_PROPERTY(int, extraLarge, 28)
public:
explicit FontSizeTokens(QObject* parent = nullptr) : ConfigObject(parent) {}
CONFIG_PROPERTY(int, extraSmall, 4)
CONFIG_PROPERTY(int, smaller, 5)
CONFIG_PROPERTY(int, small, 8)
CONFIG_PROPERTY(int, medium, 12)
CONFIG_PROPERTY(int, large, 16)
CONFIG_PROPERTY(int, largeIncreased, 20)
CONFIG_PROPERTY(int, extraLarge, 28)
CONFIG_PROPERTY(int, extraLargeIncreased, 32)
CONFIG_PROPERTY(int, extraExtraLarge, 48)
};
class FontTokens : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class AnimDurationTokens : public settings::ObjectNode {
CONFIG_NODE(AnimDurationTokens, settings::ObjectNode)
CONFIG_SUBOBJECT(FontSizeTokens, size)
public:
explicit FontTokens(QObject* parent = nullptr)
: ConfigObject(parent), m_size(new FontSizeTokens(this)) {}
CONFIG_GLOBAL_PROPERTY(int, small, 200)
CONFIG_GLOBAL_PROPERTY(int, normal, 400)
CONFIG_GLOBAL_PROPERTY(int, large, 600)
CONFIG_GLOBAL_PROPERTY(int, extraLarge, 1000)
CONFIG_GLOBAL_PROPERTY(int, expressiveFastSpatial, 350)
CONFIG_GLOBAL_PROPERTY(int, expressiveDefaultSpatial, 500)
CONFIG_GLOBAL_PROPERTY(int, expressiveSlowSpatial, 650)
CONFIG_GLOBAL_PROPERTY(int, expressiveFastEffects, 150)
CONFIG_GLOBAL_PROPERTY(int, expressiveDefaultEffects, 200)
CONFIG_GLOBAL_PROPERTY(int, expressiveSlowEffects, 300)
};
class AnimCurves : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class AppearanceTokens : public settings::ObjectNode {
CONFIG_NODE(AppearanceTokens, settings::ObjectNode)
CFG_PROPERTY(QList<qreal>, emphasized)
CFG_PROPERTY(QList<qreal>, emphasizedAccel)
CFG_PROPERTY(QList<qreal>, emphasizedDecel)
CFG_PROPERTY(QList<qreal>, standard)
CFG_PROPERTY(QList<qreal>, standardAccel)
CFG_PROPERTY(QList<qreal>, standardDecel)
CFG_PROPERTY(QList<qreal>, expressiveFastSpatial)
CFG_PROPERTY(QList<qreal>, expressiveDefaultSpatial)
CFG_PROPERTY(QList<qreal>, expressiveSlowSpatial)
CFG_PROPERTY(QList<qreal>, expressiveFastEffects)
CFG_PROPERTY(QList<qreal>, expressiveDefaultEffects)
CFG_PROPERTY(QList<qreal>, expressiveSlowEffects)
public:
explicit AnimCurves(QObject* parent = nullptr)
: ConfigObject(parent)
, m_emphasized(
{0.05,
0,
2.0 / 15.0,
0.06,
1.0 / 6.0,
0.4,
5.0 / 24.0,
0.82,
0.25,
1,
1,
1})
, m_emphasizedAccel({0.3, 0, 0.8, 0.15, 1, 1})
, m_emphasizedDecel({0.05, 0.7, 0.1, 1, 1, 1})
, m_standard({0.2, 0, 0, 1, 1, 1})
, m_standardAccel({0.3, 0, 1, 1, 1, 1})
, m_standardDecel({0, 0, 0, 1, 1, 1})
, m_expressiveFastSpatial({0.42, 1.67, 0.21, 0.9, 1, 1})
, m_expressiveDefaultSpatial({0.38, 1.21, 0.22, 1, 1, 1})
, m_expressiveSlowSpatial({0.39, 1.29, 0.35, 0.98, 1, 1})
, m_expressiveFastEffects({0.31, 0.94, 0.34, 1, 1, 1})
, m_expressiveDefaultEffects({0.34, 0.8, 0.34, 1, 1, 1})
, m_expressiveSlowEffects({0.34, 0.88, 0.34, 1, 1, 1}) {}
CONFIG_SUBOBJECT(AnimCurves, curves)
CONFIG_SUBOBJECT(RoundingTokens, rounding)
CONFIG_SUBOBJECT(SpacingTokens, spacing)
CONFIG_SUBOBJECT(PaddingTokens, padding)
CONFIG_SUBOBJECT(AnimDurationTokens, animDurations)
};
class AnimDurations : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class BarTokens : public settings::ObjectNode {
CONFIG_NODE(BarTokens, settings::ObjectNode)
CFG_PROPERTY(int, small, 200)
CFG_PROPERTY(int, scale, 1)
CFG_PROPERTY(int, normal, 400)
CFG_PROPERTY(int, large, 600)
CFG_PROPERTY(int, extraLarge, 1000)
CFG_PROPERTY(int, expressiveFastSpatial, 350)
CFG_PROPERTY(int, expressiveDefaultSpatial, 500)
CFG_PROPERTY(int, expressiveSlowSpatial, 650)
CFG_PROPERTY(int, expressiveFastEffects, 150)
CFG_PROPERTY(int, expressiveDefaultEffects, 200)
CFG_PROPERTY(int, expressiveSlowEffects, 300)
public:
explicit AnimDurations(QObject* parent = nullptr) : ConfigObject(parent) {}
CONFIG_PROPERTY(int, innerSize, 28)
};
class AnimTokens : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class DashboardTokens : public settings::ObjectNode {
CONFIG_NODE(DashboardTokens, settings::ObjectNode)
CONFIG_SUBOBJECT(AnimDurations, durations)
Q_PROPERTY(QEasingCurve emphasized READ emphasized NOTIFY curvesChanged)
Q_PROPERTY(
QEasingCurve emphasizedAccel READ emphasizedAccel NOTIFY curvesChanged)
Q_PROPERTY(
QEasingCurve emphasizedDecel READ emphasizedDecel NOTIFY curvesChanged)
Q_PROPERTY(QEasingCurve standard READ standard NOTIFY curvesChanged)
Q_PROPERTY(
QEasingCurve standardAccel READ standardAccel NOTIFY curvesChanged)
Q_PROPERTY(
QEasingCurve standardDecel READ standardDecel NOTIFY curvesChanged)
Q_PROPERTY(
QEasingCurve expressiveFastSpatial READ expressiveFastSpatial NOTIFY
curvesChanged)
Q_PROPERTY(
QEasingCurve expressiveDefaultSpatial READ expressiveDefaultSpatial
NOTIFY curvesChanged)
Q_PROPERTY(
QEasingCurve expressiveSlowSpatial READ expressiveSlowSpatial NOTIFY
curvesChanged)
Q_PROPERTY(
QEasingCurve expressiveFastEffects READ expressiveFastEffects NOTIFY
curvesChanged)
Q_PROPERTY(
QEasingCurve expressiveDefaultEffects READ expressiveDefaultEffects
NOTIFY curvesChanged)
Q_PROPERTY(
QEasingCurve expressiveSlowEffects READ expressiveSlowEffects NOTIFY
curvesChanged)
public:
explicit AnimTokens(QObject* parent = nullptr)
: ConfigObject(parent)
, m_curves(new AnimCurves(this))
, m_durations(new AnimDurations(this))
, m_easing(new AnimEasing(this)) {
connect(
m_easing,
&AnimEasing::curvesChanged,
this,
&AnimTokens::curvesChanged);
m_easing->bindCurves(m_curves);
}
[[nodiscard]] QEasingCurve emphasized() const {
return m_easing->emphasized();
}
[[nodiscard]] QEasingCurve emphasizedAccel() const {
return m_easing->emphasizedAccel();
}
[[nodiscard]] QEasingCurve emphasizedDecel() const {
return m_easing->emphasizedDecel();
}
[[nodiscard]] QEasingCurve standard() const { return m_easing->standard(); }
[[nodiscard]] QEasingCurve standardAccel() const {
return m_easing->standardAccel();
}
[[nodiscard]] QEasingCurve standardDecel() const {
return m_easing->standardDecel();
}
[[nodiscard]] QEasingCurve expressiveFastSpatial() const {
return m_easing->expressiveFastSpatial();
}
[[nodiscard]] QEasingCurve expressiveDefaultSpatial() const {
return m_easing->expressiveDefaultSpatial();
}
[[nodiscard]] QEasingCurve expressiveSlowSpatial() const {
return m_easing->expressiveSlowSpatial();
}
[[nodiscard]] QEasingCurve expressiveFastEffects() const {
return m_easing->expressiveFastEffects();
}
[[nodiscard]] QEasingCurve expressiveDefaultEffects() const {
return m_easing->expressiveDefaultEffects();
}
[[nodiscard]] QEasingCurve expressiveSlowEffects() const {
return m_easing->expressiveSlowEffects();
}
signals:
void curvesChanged();
private:
AnimCurves* m_curves;
AnimEasing* m_easing;
CONFIG_PROPERTY(int, processesWidth, 1200)
CONFIG_PROPERTY(int, processesHeight, 800)
CONFIG_PROPERTY(int, processesHeaderHeight, 40)
CONFIG_PROPERTY(int, processesRowHeight, 40)
CONFIG_PROPERTY(int, dateTimeWidth, 110)
CONFIG_PROPERTY(int, infoIconSize, 25)
CONFIG_PROPERTY(int, infoWidth, 200)
CONFIG_PROPERTY(int, mediaCoverArtSize, 150)
CONFIG_PROPERTY(int, mediaProgressSweep, 180)
CONFIG_PROPERTY(int, mediaProgressThickness, 8)
CONFIG_PROPERTY(int, mediaVisualizerSize, 200)
CONFIG_PROPERTY(int, mediaWidth, 200)
CONFIG_PROPERTY(int, resourceProgessThickness, 10)
CONFIG_PROPERTY(int, resourceSize, 200)
CONFIG_PROPERTY(int, tabIndicatorHeight, 3)
CONFIG_PROPERTY(int, tabIndicatorSpacing, 5)
CONFIG_PROPERTY(int, weatherWidth, 250)
};
class BarTokens : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class SizeTokens : public settings::ObjectNode {
CONFIG_NODE(SizeTokens, settings::ObjectNode)
CFG_PROPERTY(int, innerSize, 28)
public:
explicit BarTokens(QObject* parent = nullptr) : ConfigObject(parent) {}
};
class Tokens : public ConfigObject {
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
CONFIG_SUBOBJECT(AppearanceRounding, rounding)
CONFIG_SUBOBJECT(AppearancePadding, padding)
CONFIG_SUBOBJECT(AppearanceSpacing, spacing)
CONFIG_SUBOBJECT(FontTokens, font)
CONFIG_SUBOBJECT(AnimTokens, anim)
CONFIG_SUBOBJECT(DashboardTokens, dashboard)
CONFIG_SUBOBJECT(BarTokens, bar)
public:
explicit Tokens(QObject* parent = nullptr)
: ConfigObject(parent)
, m_rounding(new AppearanceRounding(this))
, m_padding(new AppearancePadding(this))
, m_spacing(new AppearanceSpacing(this))
, m_font(new FontTokens(this))
, m_anim(new AnimTokens(this))
, m_bar(new BarTokens(this)) {}
static Tokens* create(QQmlEngine*, QJSEngine*) { return new Tokens(); }
};
} // namespace ZShell::config
+36 -55
View File
@@ -1,7 +1,6 @@
#pragma once
#include "configobject.hpp"
#include "configlist.hpp"
#include "common.hpp"
#include <QVariantList>
#include <QStringList>
@@ -9,75 +8,57 @@
namespace ZShell::config {
class UtilitiesSizes : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class UtilitiesSizes : public settings::ObjectNode {
CONFIG_NODE(UtilitiesSizes, settings::ObjectNode)
CFG_PROPERTY(int, toastWidth, 430)
CFG_PROPERTY(int, width, 430)
public:
explicit UtilitiesSizes(QObject* parent = nullptr) : ConfigObject(parent) {}
CONFIG_PROPERTY(int, toastWidth, 430)
CONFIG_PROPERTY(int, width, 430)
};
class Toasts : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class Toasts : public settings::ObjectNode {
CONFIG_NODE(Toasts, settings::ObjectNode)
CFG_PROPERTY(bool, audioInputChanged, true)
CFG_PROPERTY(bool, audioOutputChanged, true)
CFG_PROPERTY(bool, capsLockChanged, true)
CFG_PROPERTY(bool, chargingChanged, true)
CFG_PROPERTY(bool, configLoaded, true)
CFG_PROPERTY(bool, dndChanged, true)
CFG_PROPERTY(bool, gameModeChanged, true)
CFG_PROPERTY(bool, kbLayoutChanged, true)
CFG_PROPERTY(bool, kbLimit, true)
CFG_PROPERTY(bool, nowPlaying, false)
CFG_PROPERTY(bool, numLockChanged, true)
CFG_PROPERTY(bool, vpnChanged, true)
public:
explicit Toasts(QObject* parent = nullptr) : ConfigObject(parent) {}
CONFIG_PROPERTY(bool, audioInputChanged, true)
CONFIG_PROPERTY(bool, audioOutputChanged, true)
CONFIG_PROPERTY(bool, capsLockChanged, true)
CONFIG_PROPERTY(bool, chargingChanged, true)
CONFIG_PROPERTY(bool, configLoaded, true)
CONFIG_PROPERTY(bool, dndChanged, true)
CONFIG_PROPERTY(bool, gameModeChanged, true)
CONFIG_PROPERTY(bool, kbLayoutChanged, true)
CONFIG_PROPERTY(bool, kbLimit, true)
CONFIG_PROPERTY(bool, nowPlaying, false)
CONFIG_PROPERTY(bool, numLockChanged, true)
CONFIG_PROPERTY(bool, vpnChanged, true)
};
class Vpn : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class Vpn : public settings::ObjectNode {
CONFIG_NODE(Vpn, settings::ObjectNode)
CFG_PROPERTY(bool, enabled, false)
CFG_PROPERTY(QStringList, provider, (QStringList{QStringLiteral("netbird")}))
public:
explicit Vpn(QObject* parent = nullptr) : ConfigObject(parent) {}
CONFIG_PROPERTY(bool, enabled, false)
CONFIG_PROPERTY(
QStringList, provider, (QStringList{QStringLiteral("netbird")}))
};
class Utilities : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class Utilities : public settings::ObjectNode {
CONFIG_NODE(Utilities, settings::ObjectNode)
CFG_PROPERTY(bool, enabled, true)
CFG_PROPERTY(int, maxToasts, 4)
CONFIG_PROPERTY(bool, enabled, true)
CONFIG_PROPERTY(int, maxToasts, 4)
CONFIG_SUBOBJECT(UtilitiesSizes, sizes)
CONFIG_SUBOBJECT(Toasts, toasts)
CONFIG_SUBOBJECT(Vpn, vpn)
CONFIG_LIST(
EntryList,
quickToggles,
{LIST_ENTRY(dnd, true),
LIST_ENTRY(wifi, true),
LIST_ENTRY(settings, true),
LIST_ENTRY(bluetooth, true),
LIST_ENTRY(mic, true),
LIST_ENTRY(audio, true),
LIST_ENTRY(gameMode, true)})
public:
explicit Utilities(QObject* parent = nullptr)
: ConfigObject(parent)
, m_sizes(new UtilitiesSizes(this))
, m_toasts(new Toasts(this))
, m_vpn(new Vpn(this)) {}
DEFAULT_ARG(
{LIST_ENTRY(dnd, true),
LIST_ENTRY(wifi, true),
LIST_ENTRY(settings, true),
LIST_ENTRY(bluetooth, true),
LIST_ENTRY(mic, true),
LIST_ENTRY(audio, true),
LIST_ENTRY(gameMode, true)}))
};
} // namespace ZShell::config
+12
View File
@@ -0,0 +1,12 @@
pkg_check_modules(HYPRLAND REQUIRED hyprland)
add_library(zshell-nightlight SHARED main.cpp)
target_compile_features(zshell-nightlight PRIVATE cxx_std_23)
target_include_directories(zshell-nightlight PRIVATE ${HYPRLAND_INCLUDE_DIRS})
target_link_directories(zshell-nightlight PRIVATE ${HYPRLAND_LIBRARY_DIRS})
target_compile_options(zshell-nightlight PRIVATE ${HYPRLAND_CFLAGS_OTHER})
target_compile_options(zshell-nightlight PRIVATE -fno-gnu-unique)
install(TARGETS zshell-nightlight LIBRARY DESTINATION "${INSTALL_LIBDIR}/plugins")
+32
View File
@@ -0,0 +1,32 @@
#pragma once
#include <array>
#include <cmath>
#include <algorithm>
namespace ZShell::services {
inline std::array<float, 3> kelvinToGain(int kelvin) {
const double t = std::clamp(kelvin, 1000, 40000) / 100.0;
double r, g, b;
r = t <= 66.0
? 1.0
: std::clamp(
1.29293618606 * std::pow(t - 60.0, -0.1332047592), 0.0, 1.0);
g = t <= 66.0
? std::clamp(0.39008157876 * std::log(t) - 0.63184144378, 0.0, 1.0)
: std::clamp(
1.12989086089 * std::pow(t - 60.0, -0.0755148492), 0.0, 1.0);
b = t >= 66.0
? 1.0
: (t <= 19.0
? 0.0
: std::clamp(
0.54320678911 * std::log(t - 10.0) - 1.19625408914,
0.0,
1.0));
return {static_cast<float>(r), static_cast<float>(g), static_cast<float>(b)};
}
} // namespace ZShell::services
+455
View File
@@ -0,0 +1,455 @@
#include <any>
#include <atomic>
#include <chrono>
#include <filesystem>
#include <fstream>
#include <mutex>
#include <optional>
#include <sstream>
#include <string>
#include <thread>
#include <vector>
#define private public
#include <hyprland/src/plugins/PluginAPI.hpp>
#include <hyprland/src/render/OpenGL.hpp>
#include <hyprland/src/render/Renderer.hpp>
#undef private
#include <hyprland/src/managers/EventManager.hpp>
extern "C" {
#include <lua.h>
#include <lauxlib.h>
}
#include "colortemp.hpp"
using ZShell::services::kelvinToGain;
using Render::IFramebuffer;
using Render::GL::CHyprOpenGLImpl;
using Render::GL::g_pHyprOpenGL;
inline HANDLE PHANDLE = nullptr;
static std::filesystem::path g_runtimeDir{"/tmp"};
static std::filesystem::path g_cacheBase{"/tmp/.cache"};
static void initEnvPaths() {
if (const char* env = getenv("XDG_RUNTIME_DIR"))
g_runtimeDir = env;
if (const char* env = getenv("XDG_CACHE_HOME")) {
if (*env)
g_cacheBase = env;
} else if (const char* home = getenv("HOME")) {
g_cacheBase = std::filesystem::path(home) / ".cache";
}
}
static std::ofstream& debugLog() {
static std::ofstream log(
g_runtimeDir / "zshell-nightlight-debug.log",
std::ios::app);
return log;
}
static std::mutex g_logMutex;
static std::atomic<uint64_t> g_logSeq{0};
static void dlog(const std::string& msg) {
std::lock_guard<std::mutex> lk(g_logMutex);
auto& log = debugLog();
log << "[" << g_logSeq++ << "][tid=" << std::this_thread::get_id() << "] "
<< msg << std::endl;
}
static constexpr int BAKED_STEPS = 24;
static std::string glslFloat(float v) {
std::ostringstream ss;
ss << std::fixed << std::setprecision(6) << v;
return ss.str();
}
static std::filesystem::path cacheDir() {
return g_cacheBase / "zshell" / "night-light";
}
static std::filesystem::path shaderPath() {
return cacheDir() / "nightlight.frag";
}
static std::filesystem::path bakedShaderPath(const std::array<float, 3>& gain) {
return cacheDir() /
("nightlight-" + glslFloat(gain[0]) + "-" + glslFloat(gain[1]) +
"-" + glslFloat(gain[2]) + ".frag");
}
static std::string defaultShaderSource() {
return "#version 100\n"
"precision highp float;\n"
"varying vec2 v_texcoord;\n"
"uniform sampler2D tex;\n"
"uniform vec3 tint;\n"
"void main() {\n"
" vec4 pixColor = texture2D(tex, v_texcoord);\n"
" gl_FragColor = vec4(pixColor.rgb * tint, pixColor.a);\n"
"}\n";
}
static std::string bakedShaderSource(const std::array<float, 3>& gain) {
std::ostringstream src;
src << "#version 100\n"
"precision highp float;\n"
"varying vec2 v_texcoord;\n"
"uniform sampler2D tex;\n"
"const vec3 GAIN = vec3("
<< glslFloat(gain[0]) << "," << glslFloat(gain[1]) << ","
<< glslFloat(gain[2])
<< ");\n"
"void main() {\n"
" vec4 pixColor = texture2D(tex, v_texcoord);\n"
" gl_FragColor = vec4(pixColor.rgb * GAIN, pixColor.a);\n"
"}\n";
return src.str();
}
static void ensureShaderFile(
const std::filesystem::path& path, const std::string& source) {
std::error_code ec;
if (std::filesystem::is_regular_file(path, ec)) return;
std::filesystem::create_directories(path.parent_path(), ec);
std::ofstream out(path, std::ios::trunc);
out << source;
}
static bool g_active = false;
static bool g_uniformMode = false;
static bool g_modeDecided = false;
static std::array<float, 3> g_currentGain{1.f, 1.f, 1.f};
static int g_currentKelvin = 6500;
static int g_lastKelvin = 6500;
struct STransition {
bool active = false;
std::chrono::steady_clock::time_point start;
float durationSec = 0.4f;
std::array<float, 3> fromGain{1.f, 1.f, 1.f};
std::array<float, 3> toGain{1.f, 1.f, 1.f};
int lastBakedStep = -1;
};
static STransition g_transition;
static void postStateEvent(bool enabled, bool animating);
static bool gainIsNeutral(const std::array<float, 3>& gain) {
for (float v : gain)
if (std::abs(v - 1.f) > 0.001f) return false;
return true;
}
static CFunctionHook* g_pBeginHook = nullptr;
using origBegin = void (*)(
CHyprOpenGLImpl*,
PHLMONITOR,
const CRegion&,
SP<IFramebuffer>,
std::optional<CRegion>);
static CFunctionHook* g_pSaveMirrorHook = nullptr;
using origSaveMirror = bool (*)(CHyprOpenGLImpl*, const CBox&);
static bool hkSaveBufferForMirror(CHyprOpenGLImpl* thisptr, const CBox& box) {
const bool saved = thisptr->m_applyFinalShader;
thisptr->m_applyFinalShader = false;
const bool result = (*reinterpret_cast<origSaveMirror>(
g_pSaveMirrorHook->m_original))(thisptr, box);
thisptr->m_applyFinalShader = saved;
return result;
}
static void installShader() {
auto* gl = g_pHyprOpenGL.get();
if (!gl)
return;
ensureShaderFile(shaderPath(), defaultShaderSource());
gl->applyScreenShader(shaderPath().string());
auto* shader = gl->m_finalScreenShader.get();
if (shader && shader->program() >= 1 &&
shader->getUniformLocation(SHADER_TINT) != -1) {
g_uniformMode = true;
} else {
g_uniformMode = false;
if (!g_modeDecided)
dlog(
"shader has no 'tint' uniform, falling back to "
"per-step recompiled shaders");
}
g_modeDecided = true;
}
static void setUniformGain(const std::array<float, 3>& gain) {
auto* gl = g_pHyprOpenGL.get();
auto* shader = gl ? gl->m_finalScreenShader.get() : nullptr;
if (!gl || !shader)
return;
gl->useShader(gl->m_finalScreenShader);
shader->setUniformFloat3(SHADER_TINT, gain[0], gain[1], gain[2]);
}
static void hkBegin(
CHyprOpenGLImpl* thisptr,
PHLMONITOR mon,
const CRegion& damage,
SP<IFramebuffer> fb,
std::optional<CRegion> finalDamage) {
(*reinterpret_cast<origBegin>(
g_pBeginHook->m_original))(thisptr, mon, damage, fb, finalDamage);
if (fb) {
auto* renderer = g_pHyprRenderer.get();
if (renderer)
renderer->m_renderData.blockScreenShader = true;
}
if (!g_active) return;
auto* gl = g_pHyprOpenGL.get();
if (!gl) return;
if (gl->m_finalScreenShader.get() &&
gl->m_finalScreenShader->program() < 1)
installShader();
if (!gl->m_finalScreenShader.get() ||
gl->m_finalScreenShader->program() < 1)
return;
if (g_transition.active) {
auto* monPtr = mon.get();
if (!monPtr)
return;
const float elapsed =
std::chrono::duration<float>(
std::chrono::steady_clock::now() - g_transition.start)
.count();
const float t =
std::clamp(elapsed / g_transition.durationSec, 0.f, 1.f);
for (size_t i = 0; i < 3; ++i)
g_currentGain[i] =
g_transition.fromGain[i] +
(g_transition.toGain[i] - g_transition.fromGain[i]) * t;
if (g_uniformMode) {
setUniformGain(g_currentGain);
} else {
const int step = static_cast<int>(t * BAKED_STEPS);
if (step != g_transition.lastBakedStep) {
g_transition.lastBakedStep = step;
ensureShaderFile(
bakedShaderPath(g_currentGain),
bakedShaderSource(g_currentGain));
gl->applyScreenShader(bakedShaderPath(g_currentGain).string());
}
}
if (t >= 1.f) {
g_transition.active = false;
if (gainIsNeutral(g_currentGain)) {
g_active = false;
gl->applyScreenShader("");
}
postStateEvent(g_active, false);
} else {
monPtr->m_forceFullFrames = 2;
monPtr->scheduleFrame();
}
} else if (g_uniformMode) {
setUniformGain(g_currentGain);
}
}
static constexpr int KELVIN_MIN = 2000;
static constexpr int KELVIN_MAX = 6500;
static int luaNightlightSet(lua_State* L) {
const int rawKelvin = static_cast<int>(luaL_checknumber(L, 1));
const float durationSec =
lua_gettop(L) >= 2 ? static_cast<float>(luaL_checknumber(L, 2)) : 0.4f;
const int targetKelvin = std::clamp(rawKelvin, KELVIN_MIN, KELVIN_MAX);
if (targetKelvin != rawKelvin)
dlog(
"clamped " + std::to_string(rawKelvin) + "K to " +
std::to_string(targetKelvin) + "K");
g_transition.fromGain = g_currentGain;
g_transition.toGain = kelvinToGain(targetKelvin);
g_transition.active = true;
g_transition.start = std::chrono::steady_clock::now();
g_transition.durationSec = durationSec;
g_transition.lastBakedStep = -1;
g_active = true;
g_lastKelvin = g_currentKelvin;
g_currentKelvin = targetKelvin;
postStateEvent(true, true);
lua_pushinteger(L, targetKelvin);
return 1;
}
static int luaNightlightDisable(lua_State* L) {
if (!g_active) return 0;
const float durationSec =
lua_gettop(L) >= 1 ? static_cast<float>(luaL_checknumber(L, 1)) : 0.4f;
g_transition.fromGain = g_currentGain;
g_transition.toGain = {1.f, 1.f, 1.f};
g_transition.active = true;
g_transition.start = std::chrono::steady_clock::now();
g_transition.durationSec = durationSec;
g_transition.lastBakedStep = -1;
g_lastKelvin = g_currentKelvin;
g_currentKelvin = 6500;
postStateEvent(false, true);
return 0;
}
static int luaNightlightState(lua_State* L) {
lua_pushstring(
L,
std::format(
"{{\"enabled\":{},\"animating\":{},\"last\":{},\"current\":{}}}",
g_active ? "true" : "false",
g_transition.active ? "true" : "false",
g_lastKelvin,
g_currentKelvin)
.c_str());
return 1;
}
static void postStateEvent(bool enabled, bool animating) {
if (!g_pEventManager) return;
g_pEventManager->postEvent(
SHyprIPCEvent{
.event = "zshell-nightlight",
.data = std::format(
"{},{},{},{}",
enabled ? 1 : 0,
animating ? 1 : 0,
g_lastKelvin,
g_currentKelvin)});
}
static const SFunctionMatch* findExactMethod(
const std::vector<SFunctionMatch>& methods,
const std::string& classAndMethod) {
for (const auto& m : methods) {
if (m.signature.find(classAndMethod) != std::string::npos) return &m;
}
return nullptr;
}
static std::string manglePrefix(
const std::string& className, const std::string& methodName) {
return std::to_string(className.size()) + className +
std::to_string(methodName.size()) + methodName;
}
APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) {
PHANDLE = handle;
initEnvPaths();
dlog("=== PLUGIN_INIT, build timestamp " __DATE__ " " __TIME__ " ===");
const std::string serverHash = __hyprland_api_get_hash();
const std::string clientHash = __hyprland_api_get_client_hash();
if (serverHash != clientHash) {
HyprlandAPI::addNotification(
PHANDLE,
"[zshell-nightlight] Mismatched headers! Can't proceed.",
CHyprColor{1.0f, 0.2f, 0.2f, 1.0f},
5000);
throw std::runtime_error("[zshell-nightlight] version mismatch");
}
static const auto SAVEMIRROR_METHODS =
HyprlandAPI::findFunctionsByName(PHANDLE, "saveBufferForMirror");
const SFunctionMatch* saveMirrorTarget = findExactMethod(
SAVEMIRROR_METHODS,
manglePrefix("CHyprOpenGLImpl", "saveBufferForMirror"));
if (saveMirrorTarget) {
g_pSaveMirrorHook = HyprlandAPI::createFunctionHook(
PHANDLE,
saveMirrorTarget->address,
reinterpret_cast<void*>(&hkSaveBufferForMirror));
if (g_pSaveMirrorHook && g_pSaveMirrorHook->hook())
dlog(
"saveBufferForMirror hook installed: " +
saveMirrorTarget->signature);
}
if (!g_pSaveMirrorHook)
HyprlandAPI::addNotification(
PHANDLE,
"[zshell-nightlight] WARNING: couldn't hook saveBufferForMirror — "
"captures may still show the nightlight",
CHyprColor{1.f, 0.6f, 0.2f, 1.f},
8000);
static const auto METHODS =
HyprlandAPI::findFunctionsByName(PHANDLE, "begin");
const SFunctionMatch* target =
findExactMethod(METHODS, manglePrefix("CHyprOpenGLImpl", "begin"));
if (!target)
throw std::runtime_error(
"[zshell-nightlight] couldn't uniquely resolve "
"CHyprOpenGLImpl::begin");
g_pBeginHook = HyprlandAPI::createFunctionHook(
PHANDLE, target->address, reinterpret_cast<void*>(&hkBegin));
if (!g_pBeginHook || !g_pBeginHook->hook())
throw std::runtime_error("[zshell-nightlight] failed to hook begin()");
if (!HyprlandAPI::addLuaFunction(
PHANDLE, "zshell", "nlSet", luaNightlightSet))
throw std::runtime_error(
"[zshell-nightlight] failed to register Lua function");
if (!HyprlandAPI::addLuaFunction(
PHANDLE, "zshell", "nlDisable", luaNightlightDisable))
throw std::runtime_error(
"[zshell-nightlight] failed to register Lua function");
if (!HyprlandAPI::addLuaFunction(
PHANDLE, "zshell", "nlState", luaNightlightState))
throw std::runtime_error(
"[zshell-nightlight] failed to register Lua function");
return {
"zshell-nightlight",
"Animated nightlight, excluded from screencopy",
"ZShell",
"1.2"};
}
APICALL EXPORT std::string PLUGIN_API_VERSION() {
return HYPRLAND_API_VERSION;
}
APICALL EXPORT void PLUGIN_EXIT() {}
+2 -1
View File
@@ -1,12 +1,12 @@
qml_module(ZShell-internal
URI ZShell.Internal
INCLUDE_PREFIX internal
SOURCES
hyprextras.hpp hyprextras.cpp
hyprdevices.hpp hyprdevices.cpp
cachingimagemanager.hpp cachingimagemanager.cpp
circularindicatormanager.hpp circularindicatormanager.cpp
circularbuffer.hpp circularbuffer.cpp
sparklineitem.hpp sparklineitem.cpp
arcgauge.hpp arcgauge.cpp
wallpaperimage.hpp wallpaperimage.cpp
lidwatcher.hpp lidwatcher.cpp
@@ -14,6 +14,7 @@ qml_module(ZShell-internal
linearindicatormanager.hpp linearindicatormanager.cpp
strokecanvasitem.hpp strokecanvasitem.cpp
strokecanvasrenderer.hpp strokecanvasrenderer.cpp
cursor.hpp
LIBRARIES
Qt::Gui
Qt::Quick
@@ -19,6 +19,8 @@ void CircularBuffer::setCapacity(int capacity) {
m_capacity = capacity;
m_data.resize(capacity);
m_data.fill(0.0);
m_smoothed.resize(capacity);
m_smoothed.fill(0.0);
m_head = 0;
m_count = 0;
@@ -30,6 +32,8 @@ void CircularBuffer::setCapacity(int capacity) {
m_count++;
}
recomputeAllSmoothed();
emit capacityChanged();
emit countChanged();
emit valuesChanged();
@@ -56,6 +60,19 @@ qreal CircularBuffer::maximum() const {
return maxVal;
}
int CircularBuffer::smoothingRadius() const {
return m_smoothingRadius;
}
void CircularBuffer::setSmoothingRadius(int radius) {
radius = std::max(0, radius);
if (m_smoothingRadius == radius) return;
m_smoothingRadius = radius;
recomputeAllSmoothed();
emit smoothingRadiusChanged();
emit valuesChanged();
}
void CircularBuffer::push(qreal value) {
if (m_capacity <= 0) return;
@@ -65,6 +82,9 @@ void CircularBuffer::push(qreal value) {
m_count++;
emit countChanged();
}
updateSmoothedTail();
emit valuesChanged();
}
@@ -85,4 +105,44 @@ qreal CircularBuffer::at(int index) const {
return m_data[actualIndex];
}
qreal CircularBuffer::smoothedAt(int index) const {
if (index < 0 || index >= m_count) return 0.0;
const int actualIndex =
(m_head - m_count + index + m_capacity) % m_capacity;
return m_smoothed[actualIndex];
}
void CircularBuffer::writeSmoothedAt(int logicalIndex) {
const int actualIndex =
(m_head - m_count + logicalIndex + m_capacity) % m_capacity;
if (m_smoothingRadius <= 0) {
m_smoothed[actualIndex] = m_data[actualIndex];
return;
}
qreal sum = 0.0;
int sampleCount = 0;
for (int j = -m_smoothingRadius; j <= m_smoothingRadius; ++j) {
const int idx = logicalIndex + j;
if (idx < 0 || idx >= m_count) continue;
const int a = (m_head - m_count + idx + m_capacity) % m_capacity;
sum += m_data[a];
sampleCount++;
}
m_smoothed[actualIndex] = sampleCount > 0 ? sum / sampleCount : 0.0;
}
void CircularBuffer::updateSmoothedTail() {
const int start = std::max(0, m_count - 1 - m_smoothingRadius);
for (int i = start; i < m_count; ++i)
writeSmoothedAt(i);
}
void CircularBuffer::recomputeAllSmoothed() {
for (int i = 0; i < m_count; ++i)
writeSmoothedAt(i);
}
} // namespace ZShell::internal
@@ -15,6 +15,9 @@ class CircularBuffer : public QObject {
Q_PROPERTY(int count READ count NOTIFY countChanged)
Q_PROPERTY(QList<qreal> values READ values NOTIFY valuesChanged)
Q_PROPERTY(qreal maximum READ maximum NOTIFY valuesChanged)
Q_PROPERTY(
int smoothingRadius READ smoothingRadius WRITE setSmoothingRadius NOTIFY
smoothingRadiusChanged)
public:
explicit CircularBuffer(QObject* parent = nullptr);
@@ -26,20 +29,34 @@ class CircularBuffer : public QObject {
[[nodiscard]] QList<qreal> values() const;
[[nodiscard]] qreal maximum() const;
[[nodiscard]] int smoothingRadius() const;
void setSmoothingRadius(int radius);
Q_INVOKABLE void push(qreal value);
Q_INVOKABLE void clear();
Q_INVOKABLE [[nodiscard]] qreal at(int index) const;
Q_INVOKABLE [[nodiscard]] qreal smoothedAt(int index) const;
signals:
void capacityChanged();
void countChanged();
void valuesChanged();
void smoothingRadiusChanged();
private:
void writeSmoothedAt(int logicalIndex);
void updateSmoothedTail();
void recomputeAllSmoothed();
QVector<qreal> m_data;
QVector<qreal> m_smoothed;
int m_head = 0;
int m_count = 0;
int m_capacity = 0;
int m_smoothingRadius = 0;
};
} // namespace ZShell::internal
+40
View File
@@ -0,0 +1,40 @@
#pragma once
#include <qcursor.h>
#include <qguiapplication.h>
#include <qlogging.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qqmlintegration.h>
namespace ZShell::internal {
class Cursor : public QObject {
Q_OBJECT
QML_SINGLETON
QML_ELEMENT
public:
Q_INVOKABLE void setOverride(Qt::CursorShape shape) {
if (m_shape == shape) return;
if (m_shape)
QGuiApplication::changeOverrideCursor(QCursor(shape));
else {
QGuiApplication::setOverrideCursor(QCursor(shape));
}
m_shape = shape;
}
Q_INVOKABLE void clearOverride() {
if (!m_shape) return;
QGuiApplication::restoreOverrideCursor();
m_shape.reset();
}
private:
std::optional<Qt::CursorShape> m_shape;
};
} // namespace ZShell::internal
-227
View File
@@ -1,227 +0,0 @@
#include "sparklineitem.hpp"
#include <qmath.h>
#include <qpainter.h>
#include <qpainterpath.h>
#include <qpen.h>
namespace ZShell::internal {
SparklineItem::SparklineItem(QQuickItem* parent) : QQuickPaintedItem(parent) {
setAntialiasing(true);
}
void SparklineItem::paint(QPainter* painter) {
const bool has1 = m_line1 && m_line1->count() >= 2;
const bool has2 = m_line2 && m_line2->count() >= 2;
if (!has1 && !has2) return;
painter->setRenderHint(QPainter::Antialiasing, true);
// Draw line1 first (behind), then line2 (in front)
if (has1) drawLine(painter, m_line1, m_line1Color, m_line1FillAlpha);
if (has2) drawLine(painter, m_line2, m_line2Color, m_line2FillAlpha);
}
void SparklineItem::drawLine(
QPainter* painter,
CircularBuffer* buffer,
const QColor& color,
qreal fillAlpha) {
if (m_historyLength < 2) return;
const qreal w = width();
const qreal h = height();
const int len = buffer->count();
if (len < 2 || m_maxValue <= 0.0) return;
const qreal stepX = w / static_cast<qreal>(m_historyLength - 1);
const qreal startX =
w - (len - 1) * stepX - stepX * m_slideProgress + stepX;
const qreal strokePad = qCeil(m_lineWidth / 2);
const qreal curvePad = 3.0;
const qreal topPad = strokePad + curvePad;
const qreal bottomPad = strokePad;
const qreal plotTop = topPad;
const qreal plotBottom = h - bottomPad;
const qreal fillBottom = h;
const qreal plotH = qMax<qreal>(1.0, plotBottom - plotTop);
QVector<QPointF> points;
points.reserve(len);
for (int i = 0; i < len; ++i) {
const qreal x = startX + i * stepX;
const qreal value = qBound<qreal>(0.0, buffer->at(i), m_maxValue);
const qreal y = plotTop + (1.0 - (value / m_maxValue)) * plotH;
points.append(QPointF(x, y));
}
QPainterPath linePath;
linePath.moveTo(points[0]);
for (int i = 0; i < points.size() - 1; ++i) {
const QPointF& p0 = points[i];
const QPointF& p1 = points[i + 1];
const qreal ctrlX = (p0.x() + p1.x()) * 0.5;
const QPointF c1(ctrlX, p0.y());
const QPointF c2(ctrlX, p1.y());
linePath.cubicTo(c1, c2, p1);
}
QPen pen(color, m_lineWidth);
pen.setCapStyle(Qt::RoundCap);
pen.setJoinStyle(Qt::RoundJoin);
painter->setPen(pen);
painter->setBrush(Qt::NoBrush);
painter->drawPath(linePath);
QPainterPath fillPath = linePath;
fillPath.lineTo(startX + (len - 1) * stepX, fillBottom);
fillPath.lineTo(startX, fillBottom);
fillPath.closeSubpath();
QColor fillColor = color;
fillColor.setAlphaF(static_cast<float>(fillAlpha));
painter->setPen(Qt::NoPen);
painter->setBrush(fillColor);
painter->drawPath(fillPath);
}
void SparklineItem::connectBuffer(CircularBuffer* buffer) {
if (!buffer) return;
connect(buffer, &CircularBuffer::valuesChanged, this, [this]() {
update();
});
connect(buffer, &QObject::destroyed, this, [this, buffer]() {
if (m_line1 == buffer) {
m_line1 = nullptr;
emit line1Changed();
}
if (m_line2 == buffer) {
m_line2 = nullptr;
emit line2Changed();
}
update();
});
}
CircularBuffer* SparklineItem::line1() const {
return m_line1;
}
void SparklineItem::setLine1(CircularBuffer* buffer) {
if (m_line1 == buffer) return;
if (m_line1) disconnect(m_line1, nullptr, this, nullptr);
m_line1 = buffer;
connectBuffer(buffer);
emit line1Changed();
update();
}
CircularBuffer* SparklineItem::line2() const {
return m_line2;
}
void SparklineItem::setLine2(CircularBuffer* buffer) {
if (m_line2 == buffer) return;
if (m_line2) disconnect(m_line2, nullptr, this, nullptr);
m_line2 = buffer;
connectBuffer(buffer);
emit line2Changed();
update();
}
QColor SparklineItem::line1Color() const {
return m_line1Color;
}
void SparklineItem::setLine1Color(const QColor& color) {
if (m_line1Color == color) return;
m_line1Color = color;
emit line1ColorChanged();
update();
}
QColor SparklineItem::line2Color() const {
return m_line2Color;
}
void SparklineItem::setLine2Color(const QColor& color) {
if (m_line2Color == color) return;
m_line2Color = color;
emit line2ColorChanged();
update();
}
qreal SparklineItem::line1FillAlpha() const {
return m_line1FillAlpha;
}
void SparklineItem::setLine1FillAlpha(qreal alpha) {
if (qFuzzyCompare(m_line1FillAlpha, alpha)) return;
m_line1FillAlpha = alpha;
emit line1FillAlphaChanged();
update();
}
qreal SparklineItem::line2FillAlpha() const {
return m_line2FillAlpha;
}
void SparklineItem::setLine2FillAlpha(qreal alpha) {
if (qFuzzyCompare(m_line2FillAlpha, alpha)) return;
m_line2FillAlpha = alpha;
emit line2FillAlphaChanged();
update();
}
qreal SparklineItem::maxValue() const {
return m_maxValue;
}
void SparklineItem::setMaxValue(qreal value) {
if (qFuzzyCompare(m_maxValue, value)) return;
m_maxValue = value;
emit maxValueChanged();
update();
}
qreal SparklineItem::slideProgress() const {
return m_slideProgress;
}
void SparklineItem::setSlideProgress(qreal progress) {
if (qFuzzyCompare(m_slideProgress, progress)) return;
m_slideProgress = progress;
emit slideProgressChanged();
update();
}
int SparklineItem::historyLength() const {
return m_historyLength;
}
void SparklineItem::setHistoryLength(int length) {
if (m_historyLength == length) return;
m_historyLength = length;
emit historyLengthChanged();
update();
}
qreal SparklineItem::lineWidth() const {
return m_lineWidth;
}
void SparklineItem::setLineWidth(qreal width) {
if (qFuzzyCompare(m_lineWidth, width)) return;
m_lineWidth = width;
emit lineWidthChanged();
update();
}
} // namespace ZShell::internal
+14 -1
View File
@@ -9,7 +9,7 @@ qml_module(ZShell-services
cavaprovider.hpp cavaprovider.cpp
desktopmodel.hpp desktopmodel.cpp
desktopstatemanager.hpp desktopstatemanager.cpp
hyprsunsetmanager.hpp hyprsunsetmanager.cpp
nightlightmanager.hpp nightlightmanager.cpp
tickingservice.hpp tickingservice.cpp
sensorslib.hpp sensorslib.cpp
usagefmt.hpp usagefmt.cpp
@@ -18,6 +18,10 @@ qml_module(ZShell-services
memory.hpp memory.cpp
diskinfo.hpp diskinfo.cpp
storage.hpp storage.cpp
processes.hpp processes.cpp
networkusage.hpp networkusage.cpp
lyriccandidate.hpp lyriccandidate.cpp
lyrics.hpp lyrics.cpp
LIBRARIES
Qt6::Core
Qt6::Qml
@@ -25,4 +29,13 @@ qml_module(ZShell-services
PkgConfig::Aubio
PkgConfig::Cava
Sensors::Sensors
ZShell-config
ZShell-settings
ZShell-internal
ZShell
zshell-util
DEPENDENCIES
ZShell
ZShell.Config
ZShell.Internal
)
+345 -247
View File
@@ -2,97 +2,187 @@
#include "sensorslib.hpp"
#include <cmath>
#include <qcontainerfwd.h>
#include "config/rootnodes.hpp"
#include "config/services.hpp"
#include <qdir.h>
#include <qfile.h>
#include <qobject.h>
#include <qdiriterator.h>
#include <qregularexpression.h>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QFileSystemWatcher>
#include <QTimer>
namespace ZShell::services {
using Qt::StringLiterals::operator""_s;
namespace {
constexpr const char* kTypeDetectScript =
"if command -v nvidia-smi >/dev/null 2>&1 && nvidia-smi -L >/dev/null "
"2>&1; then echo NVIDIA;"
" elif ls /sys/class/drm/card*/device/gpu_busy_percent 2>/dev/null | grep "
"-q .; then echo GENERIC;"
" else echo NONE; fi";
QStringList cardFiles(const QString& relative) {
static const QRegularExpression k_cardRe(u"^card\\d+$"_s);
constexpr const char* kNameDetectScript =
"nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null"
" || glxinfo -B 2>/dev/null | grep 'Device:' | cut -d':' -f2 | cut -d'(' "
"-f1"
" || lspci 2>/dev/null | grep -i 'vga\\|3d controller\\|display' | head -1";
QStringList files;
QDirIterator it(u"/sys/class/drm"_s, QDir::Dirs | QDir::NoDotAndDotDot);
while (it.hasNext()) {
const QString path = it.next();
if (!k_cardRe.match(it.fileName()).hasMatch()) continue;
const QString file = path + relative;
if (QFile::exists(file)) files << file;
}
return files;
}
std::optional<qreal> readNumber(const QString& path) {
QFile f(path);
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) return std::nullopt;
bool ok = false;
const qreal v = f.readAll().trimmed().toDouble(&ok);
if (!ok) return std::nullopt;
return v;
}
QString cleanName(QString s) {
static const QRegularExpression k_noise(
u"\\(R\\)|\\(TM\\)|Graphics"_s,
QRegularExpression::CaseInsensitiveOption);
static const QRegularExpression k_spaces(u"\\s+"_s);
s.replace(k_noise, {});
s.replace(k_spaces, u" "_s);
return s.trimmed();
}
QString parseNvidiaName(const QByteArray& out) {
const QString first =
QString::fromUtf8(out).split(u'\n').value(0).trimmed();
return first.isEmpty() ? QString() : cleanName(first);
}
QString parseGlxinfoName(const QByteArray& out) {
const QStringList lines = QString::fromUtf8(out).split(u'\n');
for (const QString& line : lines) {
const qsizetype idx = line.indexOf(u"Device:"_s);
if (idx < 0) {
continue;
}
QString rest = line.mid(idx + 7);
const qsizetype paren = rest.indexOf(u'(');
if (paren >= 0) {
rest = rest.left(paren);
}
const QString cleaned = cleanName(rest);
if (!cleaned.isEmpty()) {
return cleaned;
}
}
return {};
}
QString parseLspciName(const QByteArray& out) {
static const QRegularExpression k_lineRe(
u"vga|3d controller|display"_s,
QRegularExpression::CaseInsensitiveOption);
const QStringList lines = QString::fromUtf8(out).split(u'\n');
QString match;
for (const QString& line : lines) {
if (k_lineRe.match(line).hasMatch()) {
match = line;
break;
}
}
if (match.isEmpty()) {
return {};
}
static const QRegularExpression k_bracketRe(u"\\[([^\\]]+)\\][^\\[]*$"_s);
const auto bracket = k_bracketRe.match(match);
if (bracket.hasMatch()) {
return cleanName(bracket.captured(1));
}
static const QRegularExpression k_colonRe(u":\\s+(.+)"_s);
const auto colon = k_colonRe.match(match);
if (colon.hasMatch()) {
return cleanName(colon.captured(1));
}
return {};
}
struct NameSource {
QString program;
QStringList args;
QString (*parse)(const QByteArray&);
};
const std::array<NameSource, 3>& nameSources() {
static const std::array<NameSource, 3> k_sources = {{
{
.program = u"nvidia-smi"_s,
.args = {u"--query-gpu=name"_s, u"--format=csv,noheader"_s},
.parse = &parseNvidiaName,
},
{
.program = u"glxinfo"_s,
.args = {u"-B"_s},
.parse = &parseGlxinfoName,
},
{
.program = u"lspci"_s,
.args = {},
.parse = &parseLspciName,
},
}};
return k_sources;
}
constexpr int k_nvidiaSource = 0;
constexpr int k_firstGenericSource = 1;
} // namespace
Gpu::Gpu(QObject* parent) : TickingService(parent) {
QString configPath =
QDir::homePath() + QStringLiteral("/.config/zshell/config.json");
auto* svc = config::ConfigSingleton::instance()->services();
m_userType = svc->gpuType();
QObject::connect(svc, &config::Services::gpuTypeChanged, this, [this, svc] {
const GpuType value = svc->gpuType();
if (value == m_userType) return;
m_userType = value;
resolveGpu();
});
auto reloadConfig = [this, configPath]() {
QFile file(configPath);
if (file.open(QIODevice::ReadOnly)) {
QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
if (!doc.isNull()) {
QJsonObject services =
doc.object().value("services").toObject();
setUserType(parseType(services.value("gpuType").toString()));
}
}
};
resolveGpu();
reloadConfig();
m_restartTimer = new QTimer(this);
m_restartTimer->setSingleShot(true);
m_restartTimer->setInterval(500);
QObject::connect(m_restartTimer, &QTimer::timeout, this, [this] {
if (!m_nvidiaProc || m_type != GpuType::Nvidia) return;
static QFileSystemWatcher* watcher = new QFileSystemWatcher();
if (!watcher->files().contains(configPath)) {
QObject::connect(
watcher,
&QFileSystemWatcher::fileChanged,
this,
[this, configPath]() {
QTimer::singleShot(100, this, [this, configPath]() {
QFile file(configPath);
if (file.exists()) {
QJsonDocument doc =
QJsonDocument::fromJson(file.readAll());
if (!doc.isNull()) {
QJsonObject services =
doc.object().value("services").toObject();
setUserType(parseType(
services.value("gpuType").toString()));
}
}
});
});
watcher->addPath(configPath);
stopNvidiaUsage();
startNvidiaUsage();
});
QObject::connect(this, &TickingService::updateIntervalChanged, this, [this] {
if (!m_nvidiaProc) return;
m_restartTimer->start();
});
}
Gpu::~Gpu() {
if (!m_nvidiaProc) return;
m_nvidiaProc->disconnect(this);
m_nvidiaProc->terminate();
if (!m_nvidiaProc->waitForFinished(500)) {
m_nvidiaProc->kill();
m_nvidiaProc->waitForFinished(200);
}
// Detection must run before any ServiceRef appears: callers may gate the ref on
// `type !== Gpu.None`, which would otherwise deadlock the detection.
if (m_userType == Auto) {
detectTypeOnce();
}
detectNameOnce();
}
Gpu::Type Gpu::type() const {
return m_userType == Auto ? m_autoType : m_userType;
}
Gpu::Type Gpu::userType() const {
return m_userType;
}
Gpu::Type Gpu::autoType() const {
return m_autoType;
GpuType Gpu::type() const {
return m_type;
}
QString Gpu::name() const {
@@ -115,28 +205,17 @@ qreal Gpu::memoryTotal() const {
return m_memoryTotal;
}
void Gpu::setUserType(Type value) {
if (value == m_userType) {
return;
}
const Type prevDerived = type();
m_userType = value;
Q_EMIT userTypeChanged();
if (type() != prevDerived) {
Q_EMIT typeChanged();
}
bool Gpu::detecting() const {
return m_detecting;
}
void Gpu::setAutoType(Type value) {
if (value == m_userType) {
return;
}
const Type prevDerived = type();
m_autoType = value;
Q_EMIT autoTypeChanged();
if (type() != prevDerived) {
Q_EMIT typeChanged();
}
void Gpu::setType(GpuType value) {
if (value == m_type) return;
m_type = value;
if (m_type != GpuType::Nvidia) stopNvidiaUsage();
if (m_type == GpuType::None) resetUsage();
emit typeChanged();
}
void Gpu::setName(QString value) {
@@ -147,6 +226,12 @@ void Gpu::setName(QString value) {
Q_EMIT nameChanged();
}
void Gpu::setDetecting(bool value) {
if (value == m_detecting) return;
m_detecting = value;
emit detectingChanged();
}
void Gpu::setMemoryUsed(qreal value) {
if (std::abs(m_memoryUsed - value) < 0.001) return;
m_memoryUsed = value;
@@ -160,168 +245,192 @@ void Gpu::setMemoryTotal(qreal value) {
}
void Gpu::tick() {
const Type t = type();
if (t == Generic) {
if (m_type == GpuType::Generic) {
readGenericUsage();
readGenericMemory();
readGpuTemperature();
} else if (t == Nvidia) {
} else if (m_type == GpuType::Nvidia) {
startNvidiaUsage();
} else {
if (std::abs(m_percentage) > 0.0001) {
m_percentage = 0.0;
Q_EMIT percentageChanged();
}
if (std::abs(m_temperature) > 0.05) {
m_temperature = 0.0;
Q_EMIT temperatureChanged();
}
resetUsage();
}
}
void Gpu::detectTypeOnce() {
if (m_typeProc) {
return;
void Gpu::refreshSysfsCache() {
m_busyFiles = cardFiles(u"/device/gpu_busy_percent"_s);
m_vramFiles.clear();
const QStringList totals = cardFiles(u"/device/mem_info_vram_total"_s);
for (const QString& total : totals) {
QString used = total;
used.replace(u"mem_info_vram_total"_s, u"mem_info_vram_used"_s);
if (QFile::exists(used)) m_vramFiles.append({used, total});
}
m_typeProc = new QProcess(this);
QObject::connect(
m_typeProc,
&QProcess::finished,
this,
[this](int, QProcess::ExitStatus) {
const QByteArray out =
m_typeProc->readAllStandardOutput().trimmed();
if (!out.isEmpty()) {
setAutoType(parseType(QString::fromLatin1(out)));
}
m_typeProc->deleteLater();
m_typeProc = nullptr;
});
m_typeProc->start(
QStringLiteral("sh"),
{QStringLiteral("-c"), QString::fromLatin1(kTypeDetectScript)});
}
void Gpu::detectNameOnce() {
if (m_nameProc) {
void Gpu::resolveGpu() {
refreshSysfsCache();
const int generation = ++m_generation;
if (m_userType != GpuType::Auto) setType(m_userType);
if (m_userType == GpuType::None) {
setName({});
setDetecting(false);
return;
}
m_nameProc = new QProcess(this);
setName({});
setDetecting(true);
tryNameSource(
m_userType == GpuType::Generic ? k_firstGenericSource : k_nvidiaSource,
generation);
}
int Gpu::probeEnd() const {
return m_userType == GpuType::Nvidia
? k_firstGenericSource
: static_cast<int>(nameSources().size());
}
void Gpu::tryNameSource(int index, int generation) {
const NameSource& src = nameSources().at(static_cast<std::size_t>(index));
runProcess(
src.program,
src.args,
[this, index, generation, parse = src.parse](const QByteArray& out) {
finishNameSource(index, generation, parse(out));
});
}
void Gpu::finishNameSource(int index, int generation, QString name) {
if (generation != m_generation) {
return;
}
if (m_userType == GpuType::Auto && index == k_nvidiaSource) {
if (!name.isEmpty())
setType(GpuType::Nvidia);
else
setType(m_busyFiles.isEmpty() ? GpuType::None : GpuType::Generic);
if (m_type == GpuType::None) {
setName({});
setDetecting(false);
return;
}
}
if (!name.isEmpty()) {
setName(std::move(name));
setDetecting(false);
return;
}
const int next = index + 1;
if (next < probeEnd()) {
tryNameSource(next, generation);
} else {
setName({});
setDetecting(false);
}
}
void Gpu::runProcess(
const QString& program,
const QStringList& args,
std::function<void(const QByteArray&)> callback) {
auto* proc = new QProcess(this);
proc->setStandardErrorFile(QProcess::nullDevice());
const auto finish =
[proc, callback = std::move(callback)](const QByteArray& out) {
callback(out);
proc->deleteLater();
};
QObject::connect(
m_nameProc,
proc,
&QProcess::finished,
this,
[this](int, QProcess::ExitStatus) {
const QString output =
QString::fromUtf8(m_nameProc->readAllStandardOutput()).trimmed();
if (!output.isEmpty()) {
const QString lower = output.toLower();
if (lower.contains(QStringLiteral("nvidia")) ||
lower.contains(QStringLiteral("geforce")) ||
lower.contains(QStringLiteral("rtx")) ||
lower.contains(QStringLiteral("gtx")) ||
lower.contains(QStringLiteral("rx"))) {
setName(cleanName(output));
} else {
static const QRegularExpression bracketRe(
QStringLiteral("\\[([^\\]]+)\\][^\\[]*$"));
const auto bracket = bracketRe.match(output);
if (bracket.hasMatch()) {
setName(cleanName(bracket.captured(1)));
} else {
static const QRegularExpression colonRe(
QStringLiteral(":\\s*(.+)"));
const auto colon = colonRe.match(output);
if (colon.hasMatch()) {
setName(cleanName(colon.captured(1)));
}
}
}
}
m_nameProc->deleteLater();
m_nameProc = nullptr;
[finish, proc](int code, QProcess::ExitStatus status) {
const bool ok = status == QProcess::NormalExit && code == 0;
finish(ok ? proc->readAllStandardOutput() : QByteArray());
});
m_nameProc->start(
QStringLiteral("sh"),
{QStringLiteral("-c"), QString::fromLatin1(kNameDetectScript)});
QObject::connect(
proc,
&QProcess::errorOccurred,
this,
[finish](QProcess::ProcessError err) {
if (err == QProcess::FailedToStart) {
finish(QByteArray());
}
});
proc->start(program, args);
}
void Gpu::onStopped() {
stopNvidiaUsage();
}
void Gpu::readGenericMemory() {
const QStringList paths = QDir(QStringLiteral("/sys/class/drm"))
.entryList(
QStringList() << QStringLiteral("card*"),
QDir::Dirs | QDir::NoDotAndDotDot);
qreal totalMem = 0.0;
qreal usedMem = 0.0;
for (const QString& card : paths) {
QFile total(
QStringLiteral("/sys/class/drm/%1/device/mem_info_vram_total")
.arg(card));
if (!total.open(QIODevice::ReadOnly | QIODevice::Text)) {
continue;
}
bool ok = false;
const qreal v = total.readAll().trimmed().toDouble(&ok);
total.close();
if (ok) {
totalMem += v;
}
QFile used(QStringLiteral("/sys/class/drm/%1/device/mem_info_vram_used")
.arg(card));
if (!used.open(QIODevice::ReadOnly | QIODevice::Text)) {
continue;
}
bool ok1 = false;
const qreal v1 = used.readAll().trimmed().toDouble(&ok1);
used.close();
if (ok1) {
usedMem += v1;
}
qreal total = 0.0;
qreal used = 0.0;
for (const VramFiles& v : std::as_const(m_vramFiles)) {
const auto t = readNumber(v.total);
const auto u = readNumber(v.used);
if (!t || !u) continue;
total += *t;
used += *u;
}
setMemoryTotal(totalMem);
setMemoryUsed(usedMem);
setMemoryTotal(total);
setMemoryUsed(used);
}
void Gpu::readGenericUsage() {
const QStringList paths = QDir(QStringLiteral("/sys/class/drm"))
.entryList(
QStringList() << QStringLiteral("card*"),
QDir::Dirs | QDir::NoDotAndDotDot);
qreal sum = 0.0;
int count = 0;
for (const QString& card : paths) {
QFile f(QStringLiteral("/sys/class/drm/%1/device/gpu_busy_percent")
.arg(card));
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
continue;
}
bool ok = false;
const qreal v = f.readAll().trimmed().toDouble(&ok);
f.close();
if (ok) {
sum += v;
for (const QString& path : std::as_const(m_busyFiles)) {
if (const auto v = readNumber(path)) {
sum += *v;
++count;
}
}
const qreal newPerc = count > 0 ? sum / count / 100.0 : 0.0;
if (std::abs(newPerc - m_percentage) > 0.0001) {
m_percentage = newPerc;
Q_EMIT percentageChanged();
emit percentageChanged();
}
}
void Gpu::startNvidiaUsage() {
if (m_nvidiaProc) {
void Gpu::stopNvidiaUsage() {
m_restartTimer->stop();
if (!m_nvidiaProc) return;
QProcess* proc = m_nvidiaProc;
m_nvidiaProc = nullptr;
proc->disconnect(this);
if (proc->state() == QProcess::NotRunning) {
proc->deleteLater();
return;
}
m_nvidiaProc = new QProcess(this);
QObject::connect(proc, &QProcess::finished, proc, &QObject::deleteLater);
proc->terminate();
}
void Gpu::startNvidiaUsage() {
if (m_nvidiaProc) return;
auto* proc = new QProcess(this);
m_nvidiaProc = proc;
QObject::connect(
m_nvidiaProc, &QProcess::readyReadStandardOutput, this, [this]() {
while (m_nvidiaProc->canReadLine()) {
const QByteArray out = m_nvidiaProc->readLine();
proc, &QProcess::readyReadStandardOutput, this, [this, proc]() {
while (proc->canReadLine()) {
const QByteArray out = proc->readLine();
const QString output = QString::fromUtf8(out).trimmed();
if (output.isEmpty()) continue;
@@ -357,21 +466,21 @@ void Gpu::startNvidiaUsage() {
}
});
QObject::connect(m_nvidiaProc, &QProcess::finished, this, [this]() {
m_nvidiaProc->deleteLater();
m_nvidiaProc = nullptr;
});
auto cleanup = [this, proc]() {
if (m_nvidiaProc == proc) m_nvidiaProc = nullptr;
proc->deleteLater();
};
QObject::connect(proc, &QProcess::finished, this, cleanup);
QObject::connect(
m_nvidiaProc,
proc,
&QProcess::errorOccurred,
this,
[this](QProcess::ProcessError) {
m_nvidiaProc->deleteLater();
m_nvidiaProc = nullptr;
});
[cleanup](QProcess::ProcessError) { cleanup(); });
m_nvidiaProc->start(
proc->start(
QStringLiteral("nvidia-smi"),
{QStringLiteral(
"--query-gpu=utilization.gpu,temperature.gpu,"
@@ -386,32 +495,21 @@ void Gpu::readGpuTemperature() {
const qreal newTemp = t.value_or(0.0);
if (std::abs(newTemp - m_temperature) > 0.05) {
m_temperature = newTemp;
Q_EMIT temperatureChanged();
emit temperatureChanged();
}
}
Gpu::Type Gpu::parseType(const QString& s) {
const QString u = s.trimmed().toUpper();
if (u.isEmpty()) {
return Auto;
void Gpu::resetUsage() {
if (std::abs(m_percentage) > 0.0001) {
m_percentage = 0.0;
emit percentageChanged();
}
if (u == QStringLiteral("NVIDIA")) {
return Nvidia;
if (std::abs(m_temperature) > 0.05) {
m_temperature = 0.0;
emit temperatureChanged();
}
if (u == QStringLiteral("GENERIC")) {
return Generic;
}
return None;
}
QString Gpu::cleanName(QString s) {
static const QRegularExpression noise(
QStringLiteral("\\(R\\)|\\(TM\\)|Graphics"),
QRegularExpression::CaseInsensitiveOption);
static const QRegularExpression spaces(QStringLiteral("\\s+"));
s.replace(noise, QString());
s.replace(spaces, QStringLiteral(" "));
return s.trimmed();
setMemoryUsed(0.0);
setMemoryTotal(0.0);
}
} // namespace ZShell::services
+42 -28
View File
@@ -1,31 +1,29 @@
#pragma once
#include "tickingservice.hpp"
#include "config/enums.hpp"
#include <qprocess.h>
#include <qqmlintegration.h>
#include <qtmetamacros.h>
namespace ZShell::services {
using GpuType = config::GpuType::Enum;
struct VramFiles {
QString used;
QString total;
};
class Gpu : public TickingService {
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
public:
enum Type {
Auto, // user override is empty (config "") — defer to detected autoType
None, // no usable GPU
Nvidia, // queried via nvidia-smi
Generic, // queried via /sys/class/drm/card*/device/gpu_busy_percent
};
Q_ENUM(Type)
private:
Q_PROPERTY(Type type READ type NOTIFY typeChanged)
Q_PROPERTY(Type userType READ userType NOTIFY userTypeChanged)
Q_PROPERTY(Type autoType READ autoType NOTIFY autoTypeChanged)
Q_PROPERTY(GpuType type READ type NOTIFY typeChanged)
Q_PROPERTY(QString name READ name NOTIFY nameChanged)
Q_PROPERTY(bool detecting READ detecting NOTIFY detectingChanged)
Q_PROPERTY(qreal percentage READ percentage NOTIFY percentageChanged)
Q_PROPERTY(qreal temperature READ temperature NOTIFY temperatureChanged)
Q_PROPERTY(qreal memoryUsed READ memoryUsed NOTIFY memoryUsedChanged)
@@ -33,11 +31,11 @@ class Gpu : public TickingService {
public:
explicit Gpu(QObject* parent = nullptr);
~Gpu() override;
[[nodiscard]] Type type() const;
[[nodiscard]] Type userType() const;
[[nodiscard]] Type autoType() const;
[[nodiscard]] GpuType type() const;
[[nodiscard]] QString name() const;
[[nodiscard]] bool detecting() const;
[[nodiscard]] qreal percentage() const;
[[nodiscard]] qreal temperature() const;
[[nodiscard]] qreal memoryUsed() const;
@@ -45,9 +43,8 @@ class Gpu : public TickingService {
signals:
void typeChanged();
void userTypeChanged();
void autoTypeChanged();
void nameChanged();
void detectingChanged();
void percentageChanged();
void temperatureChanged();
void memoryUsedChanged();
@@ -55,35 +52,52 @@ class Gpu : public TickingService {
protected:
void tick() override;
void onStopped() override;
private:
void detectTypeOnce();
void detectNameOnce();
void resolveGpu();
[[nodiscard]] int probeEnd() const;
void tryNameSource(int index, int generation);
void finishNameSource(int index, int generation, QString name);
void readGenericUsage();
void stopNvidiaUsage();
void startNvidiaUsage();
void readGpuTemperature();
void readGenericMemory();
void resetUsage();
void setUserType(Type value);
void setAutoType(Type value);
void runProcess(
const QString& program,
const QStringList& args,
std::function<void(const QByteArray&)> callback);
void setType(GpuType value);
void setName(QString value);
void setDetecting(bool value);
void setMemoryUsed(qreal value);
void setMemoryTotal(qreal value);
[[nodiscard]] static Type parseType(const QString& s);
[[nodiscard]] static QString cleanName(QString s);
void refreshSysfsCache();
Type m_userType = Auto;
Type m_autoType = None;
GpuType m_userType = GpuType::Auto;
GpuType m_type = GpuType::None;
QString m_name;
bool m_detecting = false;
qreal m_percentage = 0.0;
qreal m_temperature = 0.0;
qreal m_memoryUsed = 0.0;
qreal m_memoryTotal = 0.0;
QProcess* m_typeProc = nullptr;
QProcess* m_nameProc = nullptr;
QStringList m_busyFiles;
QList<VramFiles> m_vramFiles;
int m_generation = 0;
bool m_nvidiaQuerying = false;
QProcess* m_nvidiaProc = nullptr;
QTimer* m_restartTimer = nullptr;
};
} // namespace ZShell::services
@@ -1,148 +0,0 @@
#include "hyprsunsetmanager.hpp"
#include <qlogging.h>
#include <qobject.h>
#include <qprocess.h>
#include <qtimer.h>
namespace ZShell::services {
HyprsunsetManager::HyprsunsetManager(QObject* parent) : QObject(parent) {
connect(&m_timer, &QTimer::timeout, this, &HyprsunsetManager::apply);
connect(&m_manualTimer, &QTimer::timeout, this, [this] {
m_manualToggle = false;
emit manualToggleChanged();
apply();
});
connect(&m_startCooldown, &QTimer::timeout, this, [this] {
m_startAllowed = true;
apply();
});
m_startCooldown.start(2000);
m_manualTimer.setSingleShot(true);
m_timer.start(60000);
m_process.setStandardInputFile(QProcess::nullDevice());
m_process.setStandardOutputFile(QProcess::nullDevice());
}
int HyprsunsetManager::startTime() const {
return m_startTime;
}
int HyprsunsetManager::endTime() const {
return m_endTime;
}
bool HyprsunsetManager::manualToggle() const {
return m_manualToggle;
}
bool HyprsunsetManager::enabled() const {
return m_enabled;
}
bool HyprsunsetManager::activeAuto() const {
return m_activeAuto;
}
int HyprsunsetManager::temp() const {
return m_temp;
}
void HyprsunsetManager::setActiveAuto(bool activeAuto) {
if (activeAuto == m_activeAuto) return;
m_activeAuto = activeAuto;
emit activeAutoChanged();
}
void HyprsunsetManager::setManualToggle(bool toggle) {
if (toggle == m_manualToggle) return;
m_manualToggle = toggle;
emit manualToggleChanged();
m_manualTimer.start(60 * 60 * 1000);
}
void HyprsunsetManager::setEndTime(const int& time) {
if (time == m_endTime) return;
m_endTime = time;
emit endTimeChanged();
apply();
}
void HyprsunsetManager::setStartTime(const int& time) {
if (time == m_startTime) return;
m_startTime = time;
emit startTimeChanged();
apply();
}
void HyprsunsetManager::setTemp(const int& temp) {
if (temp == m_temp) return;
m_temp = temp;
emit tempChanged();
apply();
}
void HyprsunsetManager::toggle() {
if (m_enabled) {
end();
} else {
start();
}
}
void HyprsunsetManager::start() {
if (m_enabled && m_initialized) return;
m_initialized = true;
m_enabled = true;
emit enabledChanged();
m_process.setProgram("hyprctl");
m_process.setArguments(
{"hyprsunset", "temperature", QString::number(m_temp)});
m_process.startDetached();
}
void HyprsunsetManager::end() {
if (!m_enabled && m_initialized) return;
m_initialized = true;
m_enabled = false;
emit enabledChanged();
m_process.setProgram("hyprctl");
m_process.setArguments({"hyprsunset", "identity"});
m_process.startDetached();
}
void HyprsunsetManager::apply() {
if (m_manualToggle || !m_activeAuto || !m_startAllowed) return;
const auto current = QTime::currentTime();
const auto currentMin = current.hour() * 60 + current.minute();
bool isDarkTime = false;
if (m_startTime <= m_endTime) {
isDarkTime = (currentMin >= m_startTime && currentMin < m_endTime);
} else {
isDarkTime = (currentMin >= m_startTime || currentMin < m_endTime);
}
if (isDarkTime) {
start();
} else {
end();
}
}
}; // namespace ZShell::services
@@ -1,71 +0,0 @@
#pragma once
#include <QObject>
#include <QTime>
#include <QProcess>
#include <QTimer>
#include <qqmlintegration.h>
#include <qtmetamacros.h>
namespace ZShell::services {
class HyprsunsetManager : public QObject {
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(bool enabled READ enabled NOTIFY enabledChanged)
Q_PROPERTY(
int startTime READ startTime WRITE setStartTime NOTIFY startTimeChanged)
Q_PROPERTY(int endTime READ endTime WRITE setEndTime NOTIFY endTimeChanged)
Q_PROPERTY(int temp READ temp WRITE setTemp NOTIFY tempChanged)
Q_PROPERTY(
bool activeAuto READ activeAuto WRITE setActiveAuto NOTIFY
activeAutoChanged)
Q_PROPERTY(
bool manualToggle READ manualToggle WRITE setManualToggle NOTIFY
manualToggleChanged)
public:
explicit HyprsunsetManager(QObject* parent = nullptr);
[[nodiscard]] int startTime() const;
[[nodiscard]] int endTime() const;
[[nodiscard]] bool enabled() const;
[[nodiscard]] int temp() const;
[[nodiscard]] bool activeAuto() const;
[[nodiscard]] bool manualToggle() const;
Q_INVOKABLE void toggle();
Q_INVOKABLE void apply();
void setStartTime(const int& time);
void setEndTime(const int& time);
void setTemp(const int& temp);
void setActiveAuto(bool activeAuto);
void setManualToggle(bool toggle);
signals:
void enabledChanged();
void startTimeChanged();
void activeAutoChanged();
void endTimeChanged();
void tempChanged();
void manualToggleChanged();
private:
int m_startTime;
int m_endTime;
bool m_enabled = false;
bool m_manualToggle = false;
bool m_activeAuto;
bool m_startAllowed = false;
bool m_initialized = false;
QTimer m_startCooldown;
int m_temp;
QProcess m_process;
QTimer m_timer;
QTimer m_manualTimer;
void start();
void end();
};
}; // namespace ZShell::services
@@ -0,0 +1,55 @@
#include "lyriccandidate.hpp"
namespace ZShell::services {
LyricCandidate::LyricCandidate(
LyricsBackend backend,
QString id,
QString title,
QString artist,
QString album,
qreal duration)
: m_backend(backend)
, m_id(std::move(id))
, m_title(std::move(title))
, m_artist(std::move(artist))
, m_album(std::move(album))
, m_duration(duration) {}
LyricsBackend LyricCandidate::backend() const {
return m_backend;
}
QString LyricCandidate::id() const {
return m_id;
}
QString LyricCandidate::title() const {
return m_title;
}
QString LyricCandidate::artist() const {
return m_artist;
}
QString LyricCandidate::album() const {
return m_album;
}
qreal LyricCandidate::duration() const {
return m_duration;
}
bool LyricCandidate::isValid() const {
return !m_id.isEmpty();
}
bool LyricCandidate::operator==(const LyricCandidate& o) const noexcept {
return m_backend == o.m_backend && m_id == o.m_id;
}
bool LyricCandidate::operator!=(const LyricCandidate& o) const noexcept {
return !(*this == o);
}
} // namespace ZShell::services
@@ -0,0 +1,53 @@
#pragma once
#include <qobject.h>
#include <qqmlintegration.h>
#include "config/enums.hpp"
namespace ZShell::services {
using LyricsBackend = config::LyricsBackend::Enum;
class LyricCandidate {
Q_GADGET
QML_VALUE_TYPE(lyricCandidate)
Q_PROPERTY(ZShell::config::LyricsBackend::Enum backend READ backend)
Q_PROPERTY(QString id READ id)
Q_PROPERTY(QString title READ title)
Q_PROPERTY(QString artist READ artist)
Q_PROPERTY(QString album READ album)
Q_PROPERTY(qreal duration READ duration)
public:
LyricCandidate() = default;
LyricCandidate(
LyricsBackend backend,
QString id,
QString title,
QString artist,
QString album = {},
qreal duration = 0.0);
[[nodiscard]] LyricsBackend backend() const;
[[nodiscard]] QString id() const;
[[nodiscard]] QString title() const;
[[nodiscard]] QString artist() const;
[[nodiscard]] QString album() const;
[[nodiscard]] qreal duration() const;
[[nodiscard]] bool isValid() const;
bool operator==(const LyricCandidate& o) const noexcept;
bool operator!=(const LyricCandidate& o) const noexcept;
private:
LyricsBackend m_backend = LyricsBackend::Auto;
QString m_id;
QString m_title;
QString m_artist;
QString m_album;
qreal m_duration = 0.0;
};
} // namespace ZShell::services
File diff suppressed because it is too large Load Diff
+162
View File
@@ -0,0 +1,162 @@
#pragma once
#include <qhash.h>
#include <qjsonobject.h>
#include <qnetworkaccessmanager.h>
#include <qnetworkreply.h>
#include <qtimer.h>
#include "lyriccandidate.hpp"
namespace ZShell::services {
struct LyricLine {
qreal time = 0.0;
QString text;
};
class Lyrics : public QObject {
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
Q_PROPERTY(QStringList lyrics READ lyrics NOTIFY lyricsChanged)
Q_PROPERTY(
ZShell::config::LyricsBackend::Enum backend READ backend NOTIFY
backendChanged)
Q_PROPERTY(
ZShell::config::LyricsBackend::Enum preferredBackend READ
preferredBackend WRITE setPreferredBackend NOTIFY
preferredBackendChanged)
Q_PROPERTY(
QList<ZShell::services::LyricCandidate> lyricCandidates READ
lyricCandidates NOTIFY lyricCandidatesChanged)
Q_PROPERTY(
ZShell::services::LyricCandidate selectedCandidate READ selectedCandidate
WRITE setSelectedCandidate NOTIFY selectedCandidateChanged)
Q_PROPERTY(bool loading READ loading NOTIFY loadingChanged)
Q_PROPERTY(bool hasLyrics READ hasLyrics NOTIFY hasLyricsChanged)
Q_PROPERTY(qreal offset READ offset WRITE setOffset NOTIFY offsetChanged)
Q_PROPERTY(QString trackArtist READ trackArtist NOTIFY trackChanged)
Q_PROPERTY(QString trackTitle READ trackTitle NOTIFY trackChanged)
public:
explicit Lyrics(QObject* parent = nullptr);
[[nodiscard]] QStringList lyrics() const;
[[nodiscard]] LyricsBackend backend() const;
[[nodiscard]] LyricsBackend preferredBackend() const;
void setPreferredBackend(LyricsBackend value);
[[nodiscard]] QList<LyricCandidate> lyricCandidates() const;
[[nodiscard]] LyricCandidate selectedCandidate() const;
void setSelectedCandidate(const LyricCandidate& value);
[[nodiscard]] bool loading() const;
[[nodiscard]] bool hasLyrics() const;
[[nodiscard]] qreal offset() const;
void setOffset(qreal value);
[[nodiscard]] QString trackArtist() const;
[[nodiscard]] QString trackTitle() const;
[[nodiscard]] Q_INVOKABLE int indexForTime(qreal time) const;
[[nodiscard]] Q_INVOKABLE qreal timeForIndex(int index) const;
Q_INVOKABLE void setTrack(
const QString& artist,
const QString& title,
const QString& album = {},
qreal duration = 0.0);
Q_INVOKABLE void clearTrack();
Q_INVOKABLE void refresh();
signals:
void lyricsChanged();
void backendChanged();
void preferredBackendChanged();
void lyricCandidatesChanged();
void selectedCandidateChanged();
void loadingChanged();
void hasLyricsChanged();
void offsetChanged();
void trackChanged();
private:
void setBackend(LyricsBackend value);
void setLoading(bool value);
void setLines(QVector<LyricLine> lines, LyricsBackend source);
void clearLines();
void appendCandidates(const QList<LyricCandidate>& add);
void clearCandidates();
void scheduleLoad();
void doLoad();
void cancelInFlight();
int newRequestId();
void tryLocal(int reqId);
void tryLrclib(int reqId);
void tryNetEase(int reqId);
void chainNext(LyricsBackend justFailed, int reqId);
void searchLrclibCandidates(int reqId);
void searchNetEaseCandidates(int reqId);
void fetchLrclibById(const QString& id, int reqId);
void fetchNetEaseLyricsById(const QString& id, int reqId);
QNetworkReply* getJson(
const QUrl& url, const QHash<QByteArray, QByteArray>& headers = {});
void trackReply(int reqId, QNetworkReply* reply);
void onPreferredBackendConfigChanged();
void onLyricsDirChanged();
void loadLyricsMap();
void persistTrackPrefs();
[[nodiscard]] static QString lyricsDir();
[[nodiscard]] static QString lyricsMapPath();
[[nodiscard]] QString trackKey() const;
[[nodiscard]] static QString backendKey(LyricsBackend value);
[[nodiscard]] static LyricsBackend backendFromKey(const QString& key);
[[nodiscard]] static const QString& stateDir();
[[nodiscard]] static const QString& cacheDir();
[[nodiscard]] static QString cachePathFor(
LyricsBackend backend, const QString& id);
[[nodiscard]] static QString readCachedLrc(
LyricsBackend backend, const QString& id);
static void writeCachedLrc(
LyricsBackend backend, const QString& id, const QString& text);
[[nodiscard]] static QVector<LyricLine> parseLrc(const QString& text);
[[nodiscard]] static QString tryReadLocalLrc(
const QString& dir, const QString& artist, const QString& title);
[[nodiscard]] static QString findLocalLrcRecursive(
const QString& dir, const QString& artist, const QString& title);
QNetworkAccessManager* m_nam;
QTimer* m_loadDebounce;
QVector<LyricLine> m_lines;
QStringList m_lyrics;
LyricsBackend m_backend = LyricsBackend::Auto;
LyricsBackend m_preferredBackend = LyricsBackend::Auto;
QList<LyricCandidate> m_candidates;
LyricCandidate m_selected;
bool m_loading = false;
bool m_hasLyrics = false;
qreal m_offset = 0.0;
QString m_artist;
QString m_title;
QString m_album;
qreal m_duration = 0.0;
int m_currentRequestId = 0;
QHash<int, QList<QPointer<QNetworkReply>>> m_pendingReplies;
QJsonObject m_lyricsMap;
bool m_lyricsMapLoaded = false;
bool m_settingFromPrefs = false;
};
} // namespace ZShell::services
+52 -1
View File
@@ -3,6 +3,8 @@
#include <qfile.h>
#include <qregularexpression.h>
#include <unistd.h>
namespace ZShell::services {
Memory::Memory(QObject* parent) : TickingService(parent) {}
@@ -15,6 +17,18 @@ qreal Memory::total() const {
return m_total;
}
qreal Memory::cached() const {
return m_cached;
}
qreal Memory::readTotal() const {
return m_readTotal;
}
qreal Memory::writeTotal() const {
return m_writeTotal;
}
qreal Memory::percentage() const {
return m_total > 0.0 ? m_used / m_total : 0.0;
}
@@ -31,6 +45,7 @@ void Memory::tick() {
QStringLiteral("MemTotal: *(\\d+)"));
static const QRegularExpression reAvail(
QStringLiteral("MemAvailable: *(\\d+)"));
static const QRegularExpression reCached(QStringLiteral("Cached: *(\\d+)"));
const QString text = QString::fromLatin1(data);
const auto totalMatch = reTotal.match(text);
@@ -45,14 +60,50 @@ void Memory::tick() {
return;
}
const quint64 usedKib = totalKib > availKib ? totalKib - availKib : 0;
const auto cachedMatch = reCached.match(text);
const quint64 cachedKib =
cachedMatch.hasMatch() ? cachedMatch.captured(1).toULongLong() : 0;
if (totalKib == m_lastTotal && usedKib == m_lastUsed) {
static const QRegularExpression rePgin(QStringLiteral("pgpgin (\\d+)"));
static const QRegularExpression rePgout(QStringLiteral("pgpgout (\\d+)"));
quint64 readKib = 0;
quint64 writeKib = 0;
{
QFile vf(QStringLiteral("/proc/vmstat"));
if (vf.open(QIODevice::ReadOnly | QIODevice::Text)) {
const QString vtext = QString::fromLatin1(vf.readAll());
vf.close();
const double pageKib = getpagesize() / 1024.0;
const auto pgin = rePgin.match(vtext);
const auto pgout = rePgout.match(vtext);
if (pgin.hasMatch()) {
readKib = static_cast<quint64>(
static_cast<double>(pgin.captured(1).toULongLong()) *
pageKib);
}
if (pgout.hasMatch()) {
writeKib = static_cast<quint64>(
static_cast<double>(pgout.captured(1).toULongLong()) *
pageKib);
}
}
}
if (totalKib == m_lastTotal && usedKib == m_lastUsed &&
cachedKib == m_lastCached && readKib == m_lastReadKib &&
writeKib == m_lastWriteKib) {
return;
}
m_lastTotal = totalKib;
m_lastUsed = usedKib;
m_lastCached = cachedKib;
m_lastReadKib = readKib;
m_lastWriteKib = writeKib;
m_total = static_cast<qreal>(totalKib);
m_used = static_cast<qreal>(usedKib);
m_cached = static_cast<qreal>(cachedKib);
m_readTotal = static_cast<qreal>(readKib);
m_writeTotal = static_cast<qreal>(writeKib);
Q_EMIT changed();
}
+12
View File
@@ -14,14 +14,20 @@ class Memory : public TickingService {
Q_PROPERTY(qreal used READ used NOTIFY changed)
Q_PROPERTY(qreal total READ total NOTIFY changed)
Q_PROPERTY(qreal cached READ cached NOTIFY changed)
Q_PROPERTY(qreal percentage READ percentage NOTIFY changed)
Q_PROPERTY(qreal readTotal READ readTotal NOTIFY changed)
Q_PROPERTY(qreal writeTotal READ writeTotal NOTIFY changed)
public:
explicit Memory(QObject* parent = nullptr);
[[nodiscard]] qreal used() const;
[[nodiscard]] qreal total() const;
[[nodiscard]] qreal cached() const;
[[nodiscard]] qreal percentage() const;
[[nodiscard]] qreal readTotal() const;
[[nodiscard]] qreal writeTotal() const;
signals:
void changed();
@@ -32,8 +38,14 @@ class Memory : public TickingService {
private:
qreal m_used = 0.0;
qreal m_total = 1.0;
qreal m_cached = 0.0;
qreal m_readTotal = 0.0;
qreal m_writeTotal = 0.0;
quint64 m_lastUsed = 0;
quint64 m_lastTotal = 0;
quint64 m_lastCached = 0;
quint64 m_lastReadKib = 0;
quint64 m_lastWriteKib = 0;
};
} // namespace ZShell::services
+153
View File
@@ -0,0 +1,153 @@
#include "networkusage.hpp"
#include <qbytearrayview.h>
#include <qfile.h>
#include <qtypes.h>
#include <array>
#include <charconv>
#include <system_error>
#include "config/rootnodes.hpp"
#include "config/dashboard.hpp"
namespace ZShell::services {
using Qt::StringLiterals::operator""_s;
NetworkUsage::NetworkUsage(QObject* parent)
: TickingService(parent)
, m_downloadBuffer(new internal::CircularBuffer(this))
, m_uploadBuffer(new internal::CircularBuffer(this)) {
m_downloadBuffer->setSmoothingRadius(2);
m_uploadBuffer->setSmoothingRadius(2);
auto* resources =
config::ConfigSingleton::instance()->dashboard()->performance();
setHistoryLength(resources->showVram() ? 70 : 30);
QObject::connect(
resources,
&ZShell::config::Performance::showVramChanged,
this,
[this, resources] {
setHistoryLength(resources->showVram() ? 70 : 30);
});
}
qreal NetworkUsage::downloadSpeed() const {
return m_downloadSpeed;
}
qreal NetworkUsage::uploadSpeed() const {
return m_uploadSpeed;
}
qreal NetworkUsage::downloadTotal() const {
return m_downloadTotal;
}
qreal NetworkUsage::uploadTotal() const {
return m_uploadTotal;
}
int NetworkUsage::historyLength() const {
return m_historyLength;
}
void NetworkUsage::setHistoryLength(int length) {
if (length <= 0 || length == m_historyLength) {
return;
}
m_historyLength = length;
m_downloadBuffer->setCapacity(m_historyLength + 1);
m_uploadBuffer->setCapacity(m_historyLength + 1);
Q_EMIT historyLengthChanged();
}
internal::CircularBuffer* NetworkUsage::downloadBuffer() const {
return m_downloadBuffer;
}
internal::CircularBuffer* NetworkUsage::uploadBuffer() const {
return m_uploadBuffer;
}
void NetworkUsage::tick() {
QFile f(u"/proc/net/dev"_s);
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
return;
}
// Skip headers
f.readLine();
f.readLine();
quint64 totalRx = 0;
quint64 totalTx = 0;
while (!f.atEnd()) {
const QByteArray line = f.readLine();
const qsizetype splitIdx = line.indexOf(':');
if (splitIdx == -1) {
continue;
}
const QByteArray iface = line.left(splitIdx).trimmed();
if (iface == QByteArrayView("lo")) {
continue; // Skip loopback interface
}
// Parse every counter through tx bytes to validate the row
const char* pos = line.constData() + splitIdx + 1;
const char* const end = line.constData() + line.size();
std::array<unsigned long long, 9> fields{};
bool valid = true;
for (unsigned long long& field : fields) {
while (pos < end && (*pos == ' ' || *pos == '\t'))
++pos;
const auto [next, ec] = std::from_chars(pos, end, field);
if (ec != std::errc{}) {
valid = false;
break;
}
pos = next;
}
if (!valid) continue;
totalRx += static_cast<quint64>(fields[0]);
totalTx += static_cast<quint64>(fields[8]);
}
f.close();
if (!m_initialized) {
m_prevRx = totalRx;
m_prevTx = totalTx;
m_timer.start();
m_initialized = true;
return;
}
const qreal elapsed = static_cast<qreal>(m_timer.restart()) / 1000.0;
const quint64 rxDelta = totalRx >= m_prevRx ? totalRx - m_prevRx : 0;
const quint64 txDelta = totalTx >= m_prevTx ? totalTx - m_prevTx : 0;
m_downloadTotal += static_cast<qreal>(rxDelta);
m_uploadTotal += static_cast<qreal>(txDelta);
if (elapsed > 0.0) {
// Calculate speeds
m_downloadSpeed = static_cast<qreal>(rxDelta) / elapsed;
m_uploadSpeed = static_cast<qreal>(txDelta) / elapsed;
m_downloadBuffer->push(m_downloadSpeed);
m_uploadBuffer->push(m_uploadSpeed);
}
m_prevRx = totalRx;
m_prevTx = totalTx;
emit changed();
}
} // namespace ZShell::services
+66
View File
@@ -0,0 +1,66 @@
#pragma once
#include <qelapsedtimer.h>
#include <qqmlintegration.h>
#include "internal/circularbuffer.hpp"
#include "tickingservice.hpp"
namespace ZShell::services {
class NetworkUsage : public TickingService {
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
Q_PROPERTY(qreal downloadSpeed READ downloadSpeed NOTIFY changed)
Q_PROPERTY(qreal uploadSpeed READ uploadSpeed NOTIFY changed)
Q_PROPERTY(qreal downloadTotal READ downloadTotal NOTIFY changed)
Q_PROPERTY(qreal uploadTotal READ uploadTotal NOTIFY changed)
Q_PROPERTY(int historyLength READ historyLength NOTIFY historyLengthChanged)
Q_PROPERTY(
ZShell::internal::CircularBuffer* downloadBuffer READ downloadBuffer
CONSTANT)
Q_PROPERTY(
ZShell::internal::CircularBuffer* uploadBuffer READ uploadBuffer
CONSTANT)
public:
explicit NetworkUsage(QObject* parent = nullptr);
[[nodiscard]] qreal downloadSpeed() const;
[[nodiscard]] qreal uploadSpeed() const;
[[nodiscard]] qreal downloadTotal() const;
[[nodiscard]] qreal uploadTotal() const;
[[nodiscard]] int historyLength() const;
[[nodiscard]] internal::CircularBuffer* downloadBuffer() const;
[[nodiscard]] internal::CircularBuffer* uploadBuffer() const;
signals:
void changed();
void historyLengthChanged();
protected:
void tick() override;
private:
void setHistoryLength(int length);
qreal m_downloadSpeed = 0.0;
qreal m_uploadSpeed = 0.0;
qreal m_downloadTotal = 0.0;
qreal m_uploadTotal = 0.0;
int m_historyLength = 30;
internal::CircularBuffer* m_downloadBuffer = nullptr;
internal::CircularBuffer* m_uploadBuffer = nullptr;
quint64 m_prevRx = 0;
quint64 m_prevTx = 0;
bool m_initialized = false;
QElapsedTimer m_timer;
};
} // namespace ZShell::services
@@ -0,0 +1,442 @@
#include "nightlightmanager.hpp"
#include <qfileinfo.h>
#include <qjsondocument.h>
#include <qjsonobject.h>
#include <qlogging.h>
#include <qtenvironmentvariables.h>
namespace ZShell::services {
namespace {
constexpr const char* NL_EVENT = "zshell-nightlight";
constexpr int RECONNECT_MS = 2000;
} // namespace
NightlightManager::NightlightManager(QObject* parent) : QObject(parent) {
connect(&m_timer, &QTimer::timeout, this, &NightlightManager::apply);
connect(&m_manualTimer, &QTimer::timeout, this, [this] {
m_manualToggle = false;
emit manualToggleChanged();
apply();
});
connect(&m_startCooldown, &QTimer::timeout, this, [this] {
m_startAllowed = true;
apply();
});
connect(&m_reconnectTimer, &QTimer::timeout, this, [this] {
if (!m_useNativeNightlight) return;
connectEventSocket();
m_pluginLoadAttempted = false;
m_pluginLoadedByUs = false;
bootstrapState();
});
m_startCooldown.start(2000);
m_manualTimer.setSingleShot(true);
m_reconnectTimer.setSingleShot(true);
m_timer.start(60000);
m_process.setStandardInputFile(QProcess::nullDevice());
m_process.setStandardOutputFile(QProcess::nullDevice());
}
int NightlightManager::startTime() const {
return m_startTime;
}
int NightlightManager::endTime() const {
return m_endTime;
}
bool NightlightManager::manualToggle() const {
return m_manualToggle;
}
bool NightlightManager::enabled() const {
return m_enabled;
}
bool NightlightManager::activeAuto() const {
return m_activeAuto;
}
int NightlightManager::temp() const {
return m_temp;
}
double NightlightManager::fadeDuration() const {
return m_fadeDuration;
}
bool NightlightManager::animating() const {
return m_animating;
}
int NightlightManager::lastTemp() const {
return m_lastTemp;
}
int NightlightManager::currentTemp() const {
return m_currentTemp;
}
void NightlightManager::setActiveAuto(bool activeAuto) {
if (activeAuto == m_activeAuto) return;
m_activeAuto = activeAuto;
emit activeAutoChanged();
}
void NightlightManager::setManualToggle(bool toggle) {
if (toggle == m_manualToggle) return;
m_manualToggle = toggle;
emit manualToggleChanged();
m_manualTimer.start(60 * 60 * 1000);
}
void NightlightManager::setEndTime(const int& time) {
if (time == m_endTime) return;
m_endTime = time;
emit endTimeChanged();
apply();
}
void NightlightManager::setStartTime(const int& time) {
if (time == m_startTime) return;
m_startTime = time;
emit startTimeChanged();
apply();
}
void NightlightManager::setTemp(const int& temp) {
if (temp == m_temp) return;
m_temp = temp;
emit tempChanged();
apply();
}
void NightlightManager::setFadeDuration(double duration) {
if (duration < 0.0) duration = 0.0;
m_fadeDuration = duration;
emit fadeDurationChanged();
}
void NightlightManager::toggle() {
if (m_enabled) {
end();
} else {
start();
}
}
void NightlightManager::start() {
if (m_enabled && m_initialized) return;
m_initialized = true;
m_enabled = true;
emit enabledChanged();
if (m_useNativeNightlight) {
m_process.setProgram("hyprctl");
m_process.setArguments(
{"dispatch",
QString("hl.plugin.zshell.nlSet(%1, %2)")
.arg(m_temp)
.arg(m_fadeDuration)});
m_process.startDetached();
} else {
m_process.setProgram("hyprctl");
m_process.setArguments(
{"hyprsunset", "temperature", QString::number(m_temp)});
m_process.startDetached();
}
}
void NightlightManager::end() {
if (!m_enabled && m_initialized) return;
m_initialized = true;
m_enabled = false;
emit enabledChanged();
if (m_useNativeNightlight) {
m_process.setProgram("hyprctl");
m_process.setArguments(
{"dispatch",
QString("hl.plugin.zshell.nlDisable(%1)").arg(m_fadeDuration)});
m_process.startDetached();
} else {
m_process.setProgram("hyprctl");
m_process.setArguments({"hyprsunset", "identity"});
m_process.startDetached();
}
}
void NightlightManager::apply() {
if (m_manualToggle || !m_activeAuto || !m_startAllowed) return;
const auto current = QTime::currentTime();
const auto currentMin = current.hour() * 60 + current.minute();
bool isDarkTime = false;
if (m_startTime <= m_endTime) {
isDarkTime = (currentMin >= m_startTime && currentMin < m_endTime);
} else {
isDarkTime = (currentMin >= m_startTime || currentMin < m_endTime);
}
if (isDarkTime) {
start();
} else {
end();
}
}
bool NightlightManager::useNativeNightlight() const {
return m_useNativeNightlight;
}
QString NightlightManager::pluginPath() const {
return m_pluginPath;
}
void NightlightManager::setPluginPath(const QString& path) {
if (path == m_pluginPath) return;
m_pluginPath = path;
emit pluginPathChanged();
}
void NightlightManager::setUseNativeNightlight(bool native) {
if (native == m_useNativeNightlight) return;
m_useNativeNightlight = native;
emit useNativeNightlightChanged();
if (native) {
disconnect(&m_eventSocket, nullptr, this, nullptr);
connect(
&m_eventSocket,
&QLocalSocket::readyRead,
this,
&NightlightManager::handleEventLines);
connect(
&m_eventSocket,
&QLocalSocket::errorOccurred,
this,
&NightlightManager::scheduleReconnect);
connectEventSocket();
bootstrapState();
} else {
m_reconnectTimer.stop();
disconnect(&m_eventSocket, nullptr, this, nullptr);
if (m_eventSocket.state() != QLocalSocket::UnconnectedState)
m_eventSocket.disconnectFromServer();
m_socketBuffer.clear();
m_animating = false;
emit animatingChanged();
m_pluginLoadAttempted = false;
m_pluginLoadedByUs = false;
}
}
QString NightlightManager::hyprlandDir() const {
const auto his = qEnvironmentVariable("HYPRLAND_INSTANCE_SIGNATURE");
if (his.isEmpty()) return QString();
auto runtimeDir = qEnvironmentVariable("XDG_RUNTIME_DIR");
auto dir = runtimeDir + "/hypr/" + his;
if (!QFileInfo(dir).isDir()) dir = "/tmp/hypr/" + his;
if (!QFileInfo(dir).isDir()) return QString();
return dir;
}
QString NightlightManager::eventSocketPath() const {
const auto dir = hyprlandDir();
return dir.isEmpty() ? QString() : dir + "/.socket2.sock";
}
QString NightlightManager::resolvePluginPath() const {
if (!m_pluginPath.isEmpty()) return m_pluginPath;
const QString name = QStringLiteral("libzshell-nightlight.so");
const QStringList dirs = {
"/usr/lib/ZShell/plugins",
"/usr/local/lib/ZShell/plugins",
};
for (const auto& dir : dirs) {
const QString candidate = dir + "/" + name;
if (QFileInfo::exists(candidate)) return candidate;
}
const QString home = qEnvironmentVariable("HOME");
if (!home.isEmpty()) {
const QString candidate = home + "/.local/lib/ZShell/plugins/" + name;
if (QFileInfo::exists(candidate)) return candidate;
}
return QString();
}
void NightlightManager::tryLoadPlugin() {
if (m_pluginLoadAttempted) return;
const auto path = resolvePluginPath();
if (path.isEmpty()) {
qWarning() << "NightlightManager: plugin not found; searched "
"pluginPath and <prefix>/usr/lib/ZShell/plugins";
m_pluginLoadAttempted = true;
return;
}
m_pluginLoadAttempted = true;
m_pluginLoadedByUs = true;
qInfo() << "NightlightManager: plugin not loaded, loading" << path;
QProcess::startDetached("hyprctl", {"plugin", "load", path});
QTimer::singleShot(1500, this, [this] { bootstrapState(); });
}
void NightlightManager::bootstrapState() {
if (!m_useNativeNightlight) return;
const auto dir = hyprlandDir();
if (dir.isEmpty()) return;
auto* sock = new QLocalSocket(this);
auto* watchdog = new QTimer(this);
watchdog->setSingleShot(true);
const auto finish = [sock, watchdog]() {
watchdog->stop();
sock->abort();
sock->deleteLater();
};
connect(sock, &QLocalSocket::connected, sock, [sock] {
sock->write("repl return hl.plugin.zshell.nlState()");
sock->flush();
});
connect(sock, &QLocalSocket::readyRead, sock, [this, sock, finish]() {
const auto raw = sock->readAll();
const auto doc = QJsonDocument::fromJson(raw);
finish();
if (!doc.isObject()) {
if (m_pluginLoadAttempted)
qWarning() << "NightlightManager: plugin load failed, state "
<< "query still answered:" << raw;
tryLoadPlugin();
return;
}
if (m_pluginLoadedByUs) {
qInfo() << "NightlightManager: plugin was not loaded, manager "
<< "loaded it; state:" << raw;
m_pluginLoadedByUs = false;
} else {
qInfo() << "NightlightManager: plugin already loaded, state:"
<< raw;
}
const auto obj = doc.object();
updateState(
obj.value("enabled").toBool(),
obj.value("animating").toBool(),
obj.value("last").toInt(),
obj.value("current").toInt());
});
connect(sock, &QLocalSocket::errorOccurred, sock, finish);
connect(watchdog, &QTimer::timeout, sock, finish);
watchdog->start(2500);
sock->connectToServer(dir + "/.socket.sock");
}
void NightlightManager::connectEventSocket() {
if (!m_useNativeNightlight) return;
if (m_eventSocket.state() != QLocalSocket::UnconnectedState) return;
const auto path = eventSocketPath();
if (path.isEmpty()) {
scheduleReconnect();
return;
}
m_socketBuffer.clear();
m_eventSocket.connectToServer(path, QLocalSocket::ReadOnly);
}
void NightlightManager::scheduleReconnect() {
if (!m_useNativeNightlight) return;
m_reconnectTimer.start(RECONNECT_MS);
}
void NightlightManager::handleEventLines() {
m_socketBuffer.append(m_eventSocket.readAll());
qsizetype idx = 0;
while ((idx = m_socketBuffer.indexOf('\n')) >= 0) {
const auto line = m_socketBuffer.left(idx);
m_socketBuffer.remove(0, idx + 1);
handleEventLine(line);
}
}
void NightlightManager::handleEventLine(const QByteArray& line) {
const qsizetype sep = line.indexOf(">>");
if (sep <= 0) return;
if (line.left(sep) != NL_EVENT) return;
const auto fields = line.mid(sep + 2).split(',');
if (fields.size() < 4) return;
bool ok = false;
const auto enabled = fields.at(0).toInt(&ok);
if (!ok) return;
const auto animating = fields.at(1).toInt(&ok);
if (!ok) return;
const auto last = fields.at(2).toInt(&ok);
if (!ok) return;
const auto current = fields.at(3).toInt(&ok);
if (!ok) return;
updateState(enabled == 1, animating == 1, last, current);
}
void NightlightManager::updateState(
bool enabled, bool animating, int last, int current) {
if (enabled != m_enabled) {
m_enabled = enabled;
emit enabledChanged();
}
if (animating != m_animating) {
m_animating = animating;
emit animatingChanged();
}
if (last != m_lastTemp) {
m_lastTemp = last;
emit lastTempChanged();
}
if (current != m_currentTemp) {
m_currentTemp = current;
emit currentTempChanged();
}
}
}; // namespace ZShell::services
@@ -0,0 +1,122 @@
#pragma once
#include <QByteArray>
#include <QLocalSocket>
#include <QObject>
#include <QProcess>
#include <QTime>
#include <QTimer>
#include <qqmlintegration.h>
#include <qtmetamacros.h>
namespace ZShell::services {
class NightlightManager : public QObject {
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(bool enabled READ enabled NOTIFY enabledChanged)
Q_PROPERTY(
int startTime READ startTime WRITE setStartTime NOTIFY startTimeChanged)
Q_PROPERTY(int endTime READ endTime WRITE setEndTime NOTIFY endTimeChanged)
Q_PROPERTY(int temp READ temp WRITE setTemp NOTIFY tempChanged)
Q_PROPERTY(
double fadeDuration READ fadeDuration WRITE setFadeDuration NOTIFY
fadeDurationChanged)
Q_PROPERTY(
bool useNativeNightlight READ useNativeNightlight WRITE
setUseNativeNightlight NOTIFY useNativeNightlightChanged)
Q_PROPERTY(
QString pluginPath READ pluginPath WRITE setPluginPath NOTIFY
pluginPathChanged)
Q_PROPERTY(
bool activeAuto READ activeAuto WRITE setActiveAuto NOTIFY
activeAutoChanged)
Q_PROPERTY(
bool manualToggle READ manualToggle WRITE setManualToggle NOTIFY
manualToggleChanged)
Q_PROPERTY(bool animating READ animating NOTIFY animatingChanged)
Q_PROPERTY(int lastTemp READ lastTemp NOTIFY lastTempChanged)
Q_PROPERTY(int currentTemp READ currentTemp NOTIFY currentTempChanged)
public:
explicit NightlightManager(QObject* parent = nullptr);
[[nodiscard]] int startTime() const;
[[nodiscard]] int endTime() const;
[[nodiscard]] bool enabled() const;
[[nodiscard]] int temp() const;
[[nodiscard]] double fadeDuration() const;
[[nodiscard]] bool useNativeNightlight() const;
[[nodiscard]] QString pluginPath() const;
[[nodiscard]] bool activeAuto() const;
[[nodiscard]] bool manualToggle() const;
[[nodiscard]] bool animating() const;
[[nodiscard]] int lastTemp() const;
[[nodiscard]] int currentTemp() const;
Q_INVOKABLE void toggle();
Q_INVOKABLE void apply();
void setStartTime(const int& time);
void setEndTime(const int& time);
void setTemp(const int& temp);
void setFadeDuration(double duration);
void setUseNativeNightlight(bool native);
void setPluginPath(const QString& path);
void setActiveAuto(bool activeAuto);
void setManualToggle(bool toggle);
signals:
void enabledChanged();
void startTimeChanged();
void activeAutoChanged();
void endTimeChanged();
void tempChanged();
void fadeDurationChanged();
void useNativeNightlightChanged();
void pluginPathChanged();
void manualToggleChanged();
void animatingChanged();
void lastTempChanged();
void currentTempChanged();
private:
int m_startTime;
int m_endTime;
bool m_enabled = false;
bool m_manualToggle = false;
bool m_activeAuto;
bool m_startAllowed = false;
bool m_initialized = false;
QTimer m_startCooldown;
int m_temp;
double m_fadeDuration = 0.4;
bool m_useNativeNightlight = false;
QString m_pluginPath;
bool m_pluginLoadAttempted = false;
bool m_pluginLoadedByUs = false;
bool m_animating = false;
int m_lastTemp = 6500;
int m_currentTemp = 6500;
QProcess m_process;
QTimer m_timer;
QTimer m_manualTimer;
QLocalSocket m_eventSocket;
QByteArray m_socketBuffer;
QTimer m_reconnectTimer;
void start();
void end();
[[nodiscard]] QString hyprlandDir() const;
[[nodiscard]] QString eventSocketPath() const;
[[nodiscard]] QString resolvePluginPath() const;
void tryLoadPlugin();
void bootstrapState();
void connectEventSocket();
void scheduleReconnect();
void handleEventLines();
void handleEventLine(const QByteArray& line);
void updateState(bool enabled, bool animating, int last, int current);
};
}; // namespace ZShell::services
File diff suppressed because it is too large Load Diff
+288
View File
@@ -0,0 +1,288 @@
#pragma once
#include "tickingservice.hpp"
#include <optional>
#include <qabstractitemmodel.h>
#include <qhash.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <sys/types.h>
template <typename T> class QFutureWatcher;
namespace ZShell::services {
class ProcessModel : public QAbstractListModel {
Q_OBJECT
Q_PROPERTY(int count READ count NOTIFY countChanged)
public:
enum Roles : quint16 {
PidRole = Qt::UserRole + 1,
PpidRole,
CommandRole,
CommandLineRole,
StateRole,
SleepingRole,
CpuUsageRole,
MemoryUsageRole,
ElapsedRole,
UidRole,
UserRole,
IoReadBytesRole,
IoWriteBytesRole,
IconNameRole,
};
Q_ENUM(Roles)
explicit ProcessModel(QObject* parent = nullptr);
[[nodiscard]] int rowCount(
const QModelIndex& parent = QModelIndex()) const override;
[[nodiscard]] QVariant data(
const QModelIndex& index, int role) const override;
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
[[nodiscard]] int count() const;
void refresh();
bool killProcess(qint64 pid, int signal = 15) const;
void setDesktopEntryIcons(
QHash<QString, QString> execPathToIcon,
QHash<QString, QString> execNameToIcon);
[[nodiscard]] QString searchText() const;
void setSearchText(const QString& text);
[[nodiscard]] bool currentUserOnly() const;
void setCurrentUserOnly(bool enabled);
[[nodiscard]] int sortField() const;
void setSortField(int field);
[[nodiscard]] bool sortAscending() const;
void setSortAscending(bool ascending);
signals:
void countChanged();
void searchTextChanged();
void currentUserOnlyChanged();
void sortFieldChanged();
void sortAscendingChanged();
private:
struct FuzzyCacheEntry {
quint64 starttime = 0;
QString command;
QString commandChars;
QVector<quint8> commandClasses;
QString commandLineChars;
QVector<quint8> commandLineClasses;
bool hasCommandLine = false;
};
struct RawStat {
pid_t pid = 0;
pid_t ppid = 0;
QString comm;
char state = '?';
quint64 utime = 0;
quint64 stime = 0;
quint64 starttime = 0;
};
struct ProcessEntry {
pid_t pid = 0;
pid_t ppid = 0;
QString command;
QString commandLine;
char state = '?';
double cpuPercent = 0.0;
quint64 rssKb = 0;
double elapsedSecs = 0.0;
uid_t uid = 0;
QString username;
QString iconName;
quint64 ioReadBytes = 0;
quint64 ioWriteBytes = 0;
int searchScore = -1;
quint64 lastUtime = 0;
quint64 lastStime = 0;
quint64 starttime = 0;
};
struct StatusInfo {
quint64 rssKb = 0;
uid_t uid = 0;
};
struct IoInfo {
quint64 readBytes = 0;
quint64 writeBytes = 0;
};
struct ScanContext {
double uptime = 0.0;
double deltaSecs = -1.0;
const QHash<pid_t, int>* prevPidToAll = nullptr;
const QVector<ProcessEntry>* prevAll = nullptr;
long clockTicksPerSec = 100;
const QHash<QString, QString>* execPathToIcon = nullptr;
const QHash<QString, QString>* execNameToIcon = nullptr;
bool iconsChanged = false;
uid_t selfEuid = 0;
};
struct ScanResult {
QVector<ProcessEntry> entries;
QHash<uid_t, QString> newUidNames;
quint64 iconVersion = 0;
};
static std::optional<RawStat> readStat(pid_t pid);
static QString readCommandLine(pid_t pid, const QString& fallbackComm);
static StatusInfo readStatus(pid_t pid);
static IoInfo readIo(pid_t pid);
static std::optional<double> readUptimeSeconds();
static QString resolveUsername(uid_t uid, QHash<uid_t, QString>& cache);
static QString resolveProcessExe(pid_t pid);
static void scanChunk(
const ScanContext& ctx,
const QVector<pid_t>& pids,
QVector<ProcessEntry>& out,
QHash<uid_t, QString>& uidCache);
static ScanResult scanProcesses(
double uptime,
double deltaSecs,
QHash<pid_t, int> prevPidToAll,
QVector<ProcessEntry> prevAll,
QHash<uid_t, QString> uidCache,
long clockTicksPerSec,
QHash<QString, QString> execPathToIcon,
QHash<QString, QString> execNameToIcon,
QStringList pathDirs,
bool desktopEntryIconsChanged,
quint64 iconVersion);
void doRefresh();
void onScanFinished();
void applyView();
void converge(const QVector<ProcessEntry>& desired);
[[nodiscard]] int compareEntries(
const ProcessEntry& a, const ProcessEntry& b) const;
[[nodiscard]] static bool isSleeping(const ProcessEntry& e);
QVector<ProcessEntry> mAll;
QHash<pid_t, int> mPidToAll;
QHash<pid_t, FuzzyCacheEntry> mFuzzyCache;
QVector<ProcessEntry> mProcesses;
QHash<uid_t, QString> mUidToName;
double mLastUptimeSecs = -1.0;
long mClockTicksPerSec = 100;
QFutureWatcher<ScanResult>* mWatcher = nullptr;
bool mRefreshPending = false;
double mPendingUptime = -1.0;
QString m_searchText;
bool m_currentUserOnly = true;
uid_t m_currentUid = 0;
int m_sortField = 0;
bool m_sortAscending = true;
QHash<QString, QString> mExecPathToIcon;
QHash<QString, QString> mExecNameToIcon;
quint64 m_iconVersion = 0;
quint64 m_scannedIconVersion = 0;
};
class Processes : public TickingService {
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
public:
enum SortField : int {
Unsorted = 0,
Pid,
Ppid,
Command,
CommandLine,
State,
Sleeping,
CpuUsage,
MemoryUsage,
Elapsed,
Uid,
User,
IoReadBytes,
IoWriteBytes
};
Q_ENUM(SortField)
private:
Q_PROPERTY(QAbstractItemModel* list READ list CONSTANT)
Q_PROPERTY(int count READ count NOTIFY countChanged)
Q_PROPERTY(
SortField sortField READ sortField WRITE setSortField NOTIFY
sortFieldChanged)
Q_PROPERTY(
bool sortAscending READ sortAscending WRITE setSortAscending NOTIFY
sortAscendingChanged)
Q_PROPERTY(
QString searchText READ searchText WRITE setSearchText NOTIFY
searchTextChanged)
Q_PROPERTY(
bool currentUserOnly READ currentUserOnly WRITE setCurrentUserOnly
NOTIFY currentUserOnlyChanged)
public:
explicit Processes(QObject* parent = nullptr);
[[nodiscard]] QAbstractItemModel* list() const;
[[nodiscard]] int count() const;
[[nodiscard]] SortField sortField() const;
void setSortField(SortField field);
[[nodiscard]] bool sortAscending() const;
void setSortAscending(bool ascending);
[[nodiscard]] QString searchText() const;
void setSearchText(const QString& text);
[[nodiscard]] bool currentUserOnly() const;
void setCurrentUserOnly(bool enabled);
Q_INVOKABLE void sortBy(SortField field);
Q_INVOKABLE void refresh();
Q_INVOKABLE bool killProcess(qint64 pid, int signal = 15) const;
Q_INVOKABLE void setDesktopEntries(const QVariantList& entries);
signals:
void countChanged();
void sortFieldChanged();
void sortAscendingChanged();
void searchTextChanged();
void currentUserOnlyChanged();
protected:
void tick() override;
private:
ProcessModel* m_model;
};
} // namespace ZShell::services
+15 -55
View File
@@ -1,66 +1,25 @@
#include "tickingservice.hpp"
#include <QFile>
#include <QDir>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QFileSystemWatcher>
#include <QTimer>
#include "config/dashboard.hpp"
#include "config/rootnodes.hpp"
namespace ZShell::services {
TickingService::TickingService(QObject* parent)
: Service(parent), m_timer(new QTimer(this)) {
m_timer->setSingleShot(false);
QObject::connect(m_timer, &QTimer::timeout, this, [this] { tick(); });
QObject::connect(m_timer, &QTimer::timeout, this, [this] {
tick();
updateTriggered();
});
QString configPath =
QDir::homePath() + QStringLiteral("/.config/zshell/config.json");
auto reloadConfig = [this, configPath]() {
QFile file(configPath);
if (!file.open(QIODevice::ReadOnly)) {
return;
}
QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
if (doc.isNull()) {
return;
}
QJsonObject dashboard = doc.object().value("dashboard").toObject();
if (dashboard.contains("resourceUpdateInterval")) {
applyInterval(
dashboard.value("resourceUpdateInterval").toInt(1000));
}
};
reloadConfig();
static auto* watcher = new QFileSystemWatcher();
if (!watcher->files().contains(configPath)) {
QObject::connect(
watcher,
&QFileSystemWatcher::fileChanged,
this,
[this, configPath]() {
QTimer::singleShot(100, this, [this, configPath]() {
QFile file(configPath);
if (!file.exists()) {
return;
}
QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
if (doc.isNull()) {
return;
}
QJsonObject dashboard =
doc.object().value("dashboard").toObject();
if (dashboard.contains("resourceUpdateInterval")) {
applyInterval(dashboard.value("resourceUpdateInterval")
.toInt(1000));
}
});
});
watcher->addPath(configPath);
}
auto* dash = ZShell::config::ConfigSingleton::instance()->dashboard();
applyInterval(dash->resourceUpdateInterval());
QObject::connect(
dash,
&ZShell::config::Dashboard::resourceUpdateIntervalChanged,
this,
[this, dash] { applyInterval(dash->resourceUpdateInterval()); });
}
int TickingService::updateInterval() const {
@@ -78,6 +37,7 @@ void TickingService::start() {
void TickingService::stop() {
m_running = false;
m_timer->stop();
onStopped();
}
void TickingService::applyInterval(int ms) {
+4 -1
View File
@@ -1,8 +1,9 @@
#pragma once
#include "service.hpp"
#include <qtimer.h>
#include "service.hpp"
namespace ZShell::services {
class TickingService : public Service {
@@ -18,12 +19,14 @@ class TickingService : public Service {
signals:
void updateIntervalChanged();
void updateTriggered();
protected:
void start() final;
void stop() final;
virtual void tick() = 0;
virtual void onStopped() {}
private:
void applyInterval(int ms);
+34
View File
@@ -14,6 +14,24 @@ bool finitePositive(qreal v) {
namespace ZShell::services::usagefmt {
FormatResult UsageFmt::formatKib(qreal kib) const {
if (!finitePositive(kib)) {
return {0.0, 0.0, "KiB"};
}
if (kib >= kGib) {
return {kib / kGib, 0.0, "GiB"};
}
if (kib >= kMib) {
return {kib / kMib, 0.0, "MiB"};
}
if (kib >= kKib) {
return {kib / kKib, 0.0, "KiB"};
}
return {kib, 0.0, "KiB"};
}
FormatResult UsageFmt::formatKib(qreal kib, qreal total) const {
if (!finitePositive(kib) || !finitePositive(total)) {
return {0.0, 0.0, "KiB"};
@@ -30,4 +48,20 @@ FormatResult UsageFmt::formatKib(qreal kib, qreal total) const {
return {kib, total, "KiB"};
}
FormatResult UsageFmt::formatBytes(qreal bytes) const {
static constexpr std::array<const char*, 5> units = {
"B", "KB", "MB", "GB", "TB"};
qreal value = finitePositive(bytes) ? bytes : 0.0;
const qreal k = 1024.0;
std::size_t i = 0;
while (value >= k && i < units.size() - 1) {
value /= k;
i++;
}
return {value, 0.0, units[i]};
}
} // namespace ZShell::services::usagefmt
+3
View File
@@ -1,5 +1,6 @@
#pragma once
#include <qaccessible_base.h>
#include <qobject.h>
#include <qobjectdefs.h>
#include <qqmlintegration.h>
@@ -27,8 +28,10 @@ class UsageFmt : public QObject {
QML_SINGLETON
public:
Q_INVOKABLE [[nodiscard]] FormatResult formatKib(qreal kib) const;
Q_INVOKABLE [[nodiscard]] FormatResult formatKib(
qreal kib, qreal total) const;
Q_INVOKABLE [[nodiscard]] FormatResult formatBytes(qreal bytes) const;
};
} // namespace ZShell::services::usagefmt
+17
View File
@@ -0,0 +1,17 @@
qml_module(ZShell-settings
URI ZShell.Settings
INCLUDE_PREFIX settings
SOURCES
common.cpp
schema.cpp
codecs.cpp
quarantine.cpp
node.cpp
objectnode.cpp
listnode.cpp
rootnode.cpp
settingsfile.cpp
changebatcher.cpp
LIBRARIES
zshell-util
)
+22
View File
@@ -0,0 +1,22 @@
#include "changebatcher.hpp"
namespace ZShell::settings {
ChangeBatcher::ChangeBatcher(QObject* parent)
: QObject(parent), m_dirty(false) {}
void ChangeBatcher::dirty() {
if (m_dirty) return;
m_dirty = true;
QMetaObject::invokeMethod(this, &ChangeBatcher::flush, Qt::QueuedConnection);
}
void ChangeBatcher::flush() {
if (!m_dirty) return;
m_dirty = false;
emit dirtied();
}
} // namespace ZShell::settings
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include <qobject.h>
namespace ZShell::settings {
class ChangeBatcher : public QObject {
Q_OBJECT
public:
explicit ChangeBatcher(QObject* parent = nullptr);
void dirty();
signals:
void dirtied();
private:
bool m_dirty;
void flush();
};
} // namespace ZShell::settings
+420
View File
@@ -0,0 +1,420 @@
#include "codecs.hpp"
#include <qjsonarray.h>
#include <qjsonobject.h>
#include <qmetaobject.h>
#include <cmath>
#include "util/metaenum.hpp"
namespace ZShell::settings {
using Qt::StringLiterals::operator""_s;
namespace {
// We don't know the option here so it isn't set, it should be set by the caller
DecodeResult error(DiagnosticType::Type type, const QString& message) {
Diagnostic error;
error.type = type;
error.message = message;
return {.value = QVariant(), .error = error, .indexPath = {}};
}
DecodeResult mismatch(ExpectedType expected, const QJsonValue& value) {
return {
.value = QVariant(),
.error = Diagnostic::mismatch(expected, value),
.indexPath = {}};
}
DecodeResult mismatch(
const QList<ExpectedType>& expected, const QJsonValue& value) {
return {
.value = QVariant(),
.error = Diagnostic::mismatch(expected, value),
.indexPath = {}};
}
template <typename Container> ValueCodec* makeListCodec(const QMetaType& type) {
const auto* elementCodec = ValueCodec::codecFor(
QMetaType::fromType<typename Container::value_type>());
return elementCodec ? new ListCodec<Container>(type, elementCodec)
: nullptr;
}
// Unions are keyed by their alternatives in order, since order decides which one wins
QString unionKey(const QList<QMetaType>& types) {
QStringList ids;
ids.reserve(types.size());
for (const auto& type : types)
ids << QString::number(type.id());
return ids.join(u","_s);
}
QList<ExpectedType> expectedTypesOf(
const QList<const ValueCodec*>& alternatives) {
QList<ExpectedType> types;
types.reserve(alternatives.size());
for (const auto* codec : alternatives)
types << codec->expected();
return types;
}
using ListFactory = ValueCodec* (*)(const QMetaType&);
const QHash<int, ListFactory>& listFactories() {
static const QHash<int, ListFactory> k_factories{
{QMetaType::fromType<QStringList>().id(), &makeListCodec<QStringList>},
{QMetaType::fromType<QList<qreal>>().id(),
&makeListCodec<QList<qreal>>},
};
return k_factories;
}
} // namespace
ValueCodec::ValueCodec(const QMetaType& type, ExpectedType expected)
: m_type(type), m_expected(expected) {}
QMetaType ValueCodec::type() const {
return m_type;
}
ExpectedType ValueCodec::expected() const {
return m_expected;
}
ValueCodec* ValueCodec::codecFor(const QMetaType& type) {
// Cache for codecs, keyed by type id
static QHash<int, ValueCodec*> s_registry;
// Cached lookup
if (const auto it = s_registry.constFind(type.id());
it != s_registry.constEnd())
return *it;
ValueCodec* codec = nullptr;
switch (type.id()) {
case QMetaType::Bool:
codec = new BoolCodec(type);
break;
case QMetaType::Int:
codec = new IntCodec(type);
break;
case QMetaType::Double:
codec = new RealCodec(type);
break;
case QMetaType::QString:
codec = new StringCodec(type);
break;
case QMetaType::QVariantList:
codec = new VariantListCodec(type);
break;
case QMetaType::QVariantMap:
codec = new VariantMapCodec(type);
break;
default:
if (const auto factory = listFactories().constFind(type.id());
factory != listFactories().constEnd())
codec = (*factory)(type);
else if (util::isSupportedEnum(type))
codec = new EnumCodec(type, util::metaEnumFor(type));
break;
}
// Cache codec
if (codec) s_registry.insert(type.id(), codec);
return codec;
}
ValueCodec* ValueCodec::unionFor(const QList<QMetaType>& types) {
// Cache for union codecs, keyed by their alternatives
static QHash<QString, ValueCodec*> s_registry;
if (types.size() < 2) {
qCCritical(
lcSettings,
"A union needs at least two types, got %lld",
types.size());
return nullptr;
}
const auto key = unionKey(types);
// Cached lookup
if (const auto it = s_registry.constFind(key); it != s_registry.constEnd())
return *it;
QList<const ValueCodec*> alternatives;
alternatives.reserve(types.size());
for (const auto& type : types) {
const auto* codec = codecFor(type);
if (!codec) {
qCCritical(
lcSettings,
"No codec found for type %s, cannot build union",
type.name());
return nullptr;
}
alternatives << codec;
}
auto* const codec = new UnionCodec(alternatives);
s_registry.insert(key, codec);
return codec;
}
QJsonValue BoolCodec::encode(const QVariant& value) const {
return value.toBool();
}
DecodeResult BoolCodec::decode(const QJsonValue& value) const {
// 1 and "true" are not booleans
if (!value.isBool()) return mismatch(ExpectedType::Bool, value);
return {.value = value.toBool(), .error = std::nullopt, .indexPath = {}};
}
QJsonValue IntCodec::encode(const QVariant& value) const {
return value.toInt();
}
DecodeResult IntCodec::decode(const QJsonValue& value) const {
if (!value.isDouble()) return mismatch(ExpectedType::Int, value);
const auto num = value.toDouble();
double integral;
if (std::fpclassify(std::modf(num, &integral)) != FP_ZERO)
return error(
DiagnosticType::InvalidValue,
QStringLiteral("Expected an integer, got the real %1")
.arg(QString::number(num)));
constexpr auto k_min = std::numeric_limits<int>::min();
constexpr auto k_max = std::numeric_limits<int>::max();
if (num < static_cast<double>(k_min) || num > static_cast<double>(k_max)) {
const auto message =
QStringLiteral(
"Integer %1 is out of range, expected between %2 and %3")
.arg(
QString::number(num, 'f', 0),
QString::number(k_min),
QString::number(k_max));
return error(DiagnosticType::InvalidValue, message);
}
return {
.value = static_cast<int>(num), .error = std::nullopt, .indexPath = {}};
}
QJsonValue RealCodec::encode(const QVariant& value) const {
return value.toDouble();
}
DecodeResult RealCodec::decode(const QJsonValue& value) const {
if (!value.isDouble()) return mismatch(ExpectedType::Real, value);
return {
.value = QVariant::fromValue<qreal>(value.toDouble()),
.error = std::nullopt,
.indexPath = {}};
}
QJsonValue StringCodec::encode(const QVariant& value) const {
return value.toString();
}
DecodeResult StringCodec::decode(const QJsonValue& value) const {
if (!value.isString()) return mismatch(ExpectedType::String, value);
return {.value = value.toString(), .error = std::nullopt, .indexPath = {}};
}
QJsonValue VariantListCodec::encode(const QVariant& value) const {
return QJsonArray::fromVariantList(value.toList());
}
DecodeResult VariantListCodec::decode(const QJsonValue& value) const {
if (!value.isArray()) return mismatch(ExpectedType::Array, value);
return {
.value = value.toArray().toVariantList(),
.error = std::nullopt,
.indexPath = {}};
}
QJsonValue VariantMapCodec::encode(const QVariant& value) const {
return QJsonObject::fromVariantMap(value.toMap());
}
DecodeResult VariantMapCodec::decode(const QJsonValue& value) const {
if (!value.isObject()) return mismatch(ExpectedType::Object, value);
return {
.value = value.toObject().toVariantMap(),
.error = std::nullopt,
.indexPath = {}};
}
EnumCodec::EnumCodec(const QMetaType& type, const QMetaEnum& metaEnum)
: ValueCodec(type, ExpectedType::String), m_metaEnum(metaEnum) {}
QJsonValue EnumCodec::encode(const QVariant& value) const {
const auto* key = util::enumKeyFor(m_metaEnum, value);
if (!key) {
qCWarning(
lcSettings,
"Cannot encode value %lld of enum %s, no such enumerator",
value.toLongLong(),
m_type.name());
return {};
}
return QString::fromUtf8(key);
}
DecodeResult EnumCodec::decode(const QJsonValue& value) const {
if (!value.isString()) return mismatch(ExpectedType::String, value);
const auto key = value.toString();
for (int i = 0; i < m_metaEnum.keyCount(); ++i) {
if (QString::compare(
QString::fromUtf8(m_metaEnum.key(i)),
key,
Qt::CaseInsensitive) != 0)
continue;
const auto raw = m_metaEnum.value(i);
QVariant decoded(m_type);
if (!QMetaType::convert(
QMetaType::fromType<int>(), &raw, m_type, decoded.data())) {
qCCritical(
lcSettings,
"Failed to convert value %d to enum %s",
raw,
m_type.name());
return error(
DiagnosticType::InvalidValue,
QStringLiteral("Could not convert %1 to %2")
.arg(key, QString::fromUtf8(m_type.name())));
}
return {.value = decoded, .error = std::nullopt, .indexPath = {}};
}
QStringList options;
options.reserve(m_metaEnum.keyCount());
for (int i = 0; i < m_metaEnum.keyCount(); ++i)
options << QString::fromUtf8(m_metaEnum.key(i));
return error(
DiagnosticType::InvalidValue,
QStringLiteral("Invalid enum value %1. Expected one of %2")
.arg(key, options.join(u", "_s)));
}
template <typename Container>
ListCodec<Container>::ListCodec(
const QMetaType& type, const ValueCodec* elementCodec)
: ValueCodec(type, ExpectedType::Array), m_elementCodec(elementCodec) {}
template <typename Container>
QJsonValue ListCodec<Container>::encode(const QVariant& value) const {
QJsonArray array;
const auto list = value.value<Container>();
for (const auto& element : list)
array.append(m_elementCodec->encode(QVariant::fromValue(element)));
return array;
}
template <typename Container>
DecodeResult ListCodec<Container>::decode(const QJsonValue& value) const {
if (!value.isArray()) return mismatch(ExpectedType::Array, value);
const auto array = value.toArray();
Container list;
list.reserve(array.size());
for (qsizetype i = 0; i < array.size(); ++i) {
auto result = m_elementCodec->decode(array.at(i));
// Reject the entire list if any element is invalid
if (result.error) {
result.indexPath.prepend(i);
return result;
}
list.append(result.value.value<Value>());
}
return {
.value = QVariant::fromValue(list),
.error = std::nullopt,
.indexPath = {}};
}
UnionCodec::UnionCodec(const QList<const ValueCodec*>& alternatives)
: ValueCodec(
QMetaType::fromType<QVariant>(), alternatives.first()->expected())
, m_alternatives(alternatives)
, m_expectedTypes(expectedTypesOf(alternatives)) {
m_byType.reserve(alternatives.size());
for (const auto* codec : alternatives)
m_byType.insert(codec->type().id(), codec);
}
QJsonValue UnionCodec::encode(const QVariant& value) const {
// An unset union is simply absent from the file
if (!value.isValid()) return QJsonValue::Undefined;
const auto* codec = m_byType.value(value.metaType().id());
if (!codec) {
qCWarning(
lcSettings,
"Cannot encode value of type %s, it is not one of the allowed "
"types",
value.typeName());
return QJsonValue::Undefined;
}
return codec->encode(value);
}
DecodeResult UnionCodec::decode(const QJsonValue& value) const {
std::optional<DecodeResult> best;
for (const auto* codec : m_alternatives) {
auto result = codec->decode(value);
// The first alternative to accept the value wins
if (!result.error) return result;
// Non-nested type mismatch just means try another alternative
if (result.indexPath.isEmpty() &&
result.error->type == DiagnosticType::TypeMismatch)
continue;
// Report the deepest error that isn't a type mismatch
if (!best || result.indexPath.size() > best->indexPath.size())
best = std::move(result);
}
if (best) return *best;
// Nothing matched the shape, so report every alternative
return mismatch(m_expectedTypes, value);
}
// Instantiated for types as needed
template class ListCodec<QStringList>;
template class ListCodec<QList<qreal>>;
} // namespace ZShell::settings
+107
View File
@@ -0,0 +1,107 @@
#pragma once
#include <qhash.h>
#include <qjsonvalue.h>
#include <qlist.h>
#include <qmetaobject.h>
#include <qvariant.h>
#include <optional>
#include "common.hpp"
namespace ZShell::settings {
struct DecodeResult {
QVariant value;
std::optional<Diagnostic> error;
QList<qsizetype>
indexPath; // Index path of the failing element, outermost list first
};
class ValueCodec {
public:
explicit ValueCodec(const QMetaType& type, ExpectedType expected);
virtual ~ValueCodec() = default;
// Returns the shared codec for a type, or nullptr if the type is unsupported
static ValueCodec* codecFor(const QMetaType& type);
// Returns the shared codec for a union of types, or nullptr if any of them is unsupported
static ValueCodec* unionFor(const QList<QMetaType>& types);
[[nodiscard]] QMetaType type() const;
[[nodiscard]] ExpectedType expected()
const; // The JSON type this decodes from
[[nodiscard]] virtual QJsonValue encode(const QVariant& value) const = 0;
[[nodiscard]] virtual DecodeResult decode(const QJsonValue& value) const = 0;
protected:
const QMetaType m_type;
const ExpectedType m_expected;
Q_DISABLE_COPY_MOVE(ValueCodec)
};
#define CODEC(Type, Expected) \
class Type##Codec : public ValueCodec { \
public: \
explicit Type##Codec(const QMetaType& type) \
: ValueCodec(type, ExpectedType::Expected) {} \
\
[[nodiscard]] QJsonValue encode(const QVariant& value) const override; \
[[nodiscard]] DecodeResult decode( \
const QJsonValue& value) const override; \
};
CODEC(Bool, Bool)
CODEC(Int, Int)
CODEC(Real, Real)
CODEC(String, String)
CODEC(VariantList, Array)
CODEC(VariantMap, Object)
#undef CODEC
class EnumCodec : public ValueCodec {
public:
explicit EnumCodec(const QMetaType& type, const QMetaEnum& metaEnum);
[[nodiscard]] QJsonValue encode(const QVariant& value) const override;
[[nodiscard]] DecodeResult decode(const QJsonValue& value) const override;
private:
const QMetaEnum m_metaEnum;
};
template <typename Container> class ListCodec : public ValueCodec {
using Value = Container::value_type;
public:
explicit ListCodec(const QMetaType& type, const ValueCodec* elementCodec);
[[nodiscard]] QJsonValue encode(const QVariant& value) const override;
[[nodiscard]] DecodeResult decode(const QJsonValue& value) const override;
private:
const ValueCodec* m_elementCodec;
};
// Decodes any one of several types, for options that accept more than one shape
class UnionCodec : public ValueCodec {
public:
explicit UnionCodec(const QList<const ValueCodec*>& alternatives);
[[nodiscard]] QJsonValue encode(const QVariant& value) const override;
[[nodiscard]] DecodeResult decode(const QJsonValue& value) const override;
private:
const QList<const ValueCodec*>
m_alternatives; // Tried in order, so the first to accept a value wins
const QList<ExpectedType>
m_expectedTypes; // Types of the alternatives, for diagnostics
QHash<int, const ValueCodec*>
m_byType; // Type id to alternative, for encoding
};
} // namespace ZShell::settings
+131
View File
@@ -0,0 +1,131 @@
#include "common.hpp"
#include "node.hpp"
using Qt::StringLiterals::operator""_s;
namespace ZShell::settings {
Q_LOGGING_CATEGORY(lcSettings, "zshell.settings", QtInfoMsg)
WriteScope::WriteScope(Node* node, WriteOrigin origin)
: m_root(node->rootNode()), m_previous(m_root->m_writeOrigin) {
m_root->m_writeOrigin = origin;
}
WriteScope::~WriteScope() {
m_root->m_writeOrigin = m_previous;
}
InternalRead::InternalRead(Node* node)
: m_root(node->rootNode()), m_previous(m_root->m_internalRead) {
m_root->m_internalRead = true;
}
InternalRead::~InternalRead() {
m_root->m_internalRead = m_previous;
}
QString DiagnosticType::toString(Type t) {
switch (t) {
case UnknownOption:
return u"UnknownOption"_s;
case GlobalOption:
return u"GlobalOption"_s;
case TypeMismatch:
return u"TypeMismatch"_s;
case InvalidValue:
return u"InvalidValue"_s;
}
Q_UNREACHABLE_RETURN(QString());
}
namespace {
QString expectedStr(ExpectedType expected) {
switch (expected) {
case ExpectedType::Bool:
return QStringLiteral("a boolean");
case ExpectedType::Int:
return QStringLiteral("an integer");
case ExpectedType::Real:
return QStringLiteral("a number");
case ExpectedType::String:
return QStringLiteral("a string");
case ExpectedType::Array:
return QStringLiteral("an array");
case ExpectedType::Object:
return QStringLiteral("an object");
}
Q_UNREACHABLE_RETURN(QString());
}
QString receivedStr(const QJsonValue& value) {
switch (value.type()) {
case QJsonValue::Null:
return QStringLiteral("null");
case QJsonValue::Bool:
return QStringLiteral("a boolean");
case QJsonValue::Double:
return QStringLiteral("a number");
case QJsonValue::String:
return QStringLiteral("a string");
case QJsonValue::Array:
return QStringLiteral("an array");
case QJsonValue::Object:
return QStringLiteral("an object");
default:
return QStringLiteral("nothing");
}
}
QString mismatchStr(
const QList<ExpectedType>& expected, const QJsonValue& value) {
QStringList types;
types.reserve(expected.size());
for (const auto type : expected)
types << expectedStr(type);
const auto received = receivedStr(value);
switch (expected.size()) {
case 2:
return QStringLiteral("Expected %1 or %2, got %3")
.arg(types[0], types[1], received);
case 3:
return QStringLiteral("Expected %1, %2 or %3, got %4")
.arg(types[0], types[1], types[2], received);
case 4:
return QStringLiteral("Expected one of: %1, %2, %3, %4; got %5")
.arg(types[0], types[1], types[2], types[3], received);
default:
// The bounds are checked in macros.hpp `unionType<...T>`
Q_UNREACHABLE_RETURN(QString());
}
}
} // namespace
Diagnostic Diagnostic::mismatch(
ExpectedType expected, const QJsonValue& value, const QString& option) {
return {
.type = DiagnosticType::TypeMismatch,
.option = option,
.message = QStringLiteral("Expected %1, got %2")
.arg(expectedStr(expected), receivedStr(value)),
};
}
Diagnostic Diagnostic::mismatch(
const QList<ExpectedType>& expected,
const QJsonValue& value,
const QString& option) {
return {
.type = DiagnosticType::TypeMismatch,
.option = option,
.message = mismatchStr(expected, value),
};
}
} // namespace ZShell::settings
+99
View File
@@ -0,0 +1,99 @@
#pragma once
#include <qjsonvalue.h>
#include <qlist.h>
#include <qloggingcategory.h>
#include <qobject.h>
#include <qqmlintegration.h>
namespace ZShell::settings {
Q_DECLARE_LOGGING_CATEGORY(lcSettings)
class Node;
enum class WriteOrigin : quint8 {
Init, // On init
File, // From the JSON file
FileReset, // When option not present in file
Layer, // From the fallback layer
Qml, // From QML
QmlReset, // On option reset
};
class WriteScope {
public:
explicit WriteScope(Node* node, WriteOrigin origin);
~WriteScope();
private:
Node* const m_root;
const WriteOrigin m_previous;
Q_DISABLE_COPY_MOVE(WriteScope)
};
class InternalRead {
public:
explicit InternalRead(Node* node);
~InternalRead();
private:
Node* const m_root;
const bool m_previous;
Q_DISABLE_COPY_MOVE(InternalRead)
};
class DiagnosticType : public QObject {
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
public:
enum Type : quint8 {
UnknownOption = 0,
GlobalOption,
TypeMismatch,
InvalidValue,
};
Q_ENUM(Type)
Q_INVOKABLE static QString toString(Type t);
};
enum class ExpectedType : quint8 {
Bool,
Int,
Real,
String,
Array,
Object,
};
struct Diagnostic {
Q_GADGET
QML_VALUE_TYPE(diagnostic)
Q_PROPERTY(DiagnosticType::Type type MEMBER type)
Q_PROPERTY(QString option MEMBER option)
Q_PROPERTY(QString message MEMBER message)
public:
DiagnosticType::Type type = DiagnosticType::UnknownOption;
QString option;
QString message;
static Diagnostic mismatch(
ExpectedType expected,
const QJsonValue& value,
const QString& option = {});
static Diagnostic mismatch(
const QList<ExpectedType>& expected,
const QJsonValue& value,
const QString& option = {});
bool operator==(const Diagnostic& other) const = default;
};
} // namespace ZShell::settings
+81
View File
@@ -0,0 +1,81 @@
#pragma once
#include <qhash.h>
#include <qobject.h>
#include <qstringview.h>
#include "node.hpp"
namespace ZShell::settings {
// Derived from Node and has ctor(path, fallback, parent)
template <typename T>
concept LayerType = std::derived_from<T, Node> &&
std::constructible_from<T, const QString&, T*, QObject*>;
template <LayerType T> class LayerRegistry {
public:
explicit LayerRegistry(
const QString& prefix, const QString& suffix, QObject* parent);
[[nodiscard]] QString pathFor(const QString& name) const;
[[nodiscard]] QString nameFor(T* layer) const;
[[nodiscard]] T* get(
const QString& name,
T* fallback,
bool* created = nullptr); // Created on demand
private:
const QString m_prefix;
const QString m_suffix;
QObject* const m_parent;
QHash<QString, T*> m_layers;
};
namespace detail {
inline QString stripTrailingSlashes(QStringView str) {
while (str.endsWith(u'/'))
str = str.left(str.length() - 1);
return str.toString();
}
inline QString stripLeadingSlashes(QStringView str) {
while (str.startsWith(u'/'))
str = str.mid(1);
return str.toString();
}
} // namespace detail
template <LayerType T>
LayerRegistry<T>::LayerRegistry(
const QString& prefix, const QString& suffix, QObject* parent)
: m_prefix(detail::stripTrailingSlashes(prefix))
, m_suffix(detail::stripLeadingSlashes(suffix))
, m_parent(parent) {}
template <LayerType T>
QString LayerRegistry<T>::pathFor(const QString& name) const {
return m_prefix + u'/' + name + u'/' + m_suffix;
}
template <LayerType T> QString LayerRegistry<T>::nameFor(T* layer) const {
return m_layers.key(layer);
}
template <LayerType T>
T* LayerRegistry<T>::get(const QString& name, T* fallback, bool* created) {
if (auto* const layer = m_layers.value(name)) {
if (created) *created = false;
return layer;
}
if (created) *created = true;
auto* const layer = new T(pathFor(name), fallback, m_parent);
m_layers.insert(name, layer);
return layer;
}
} // namespace ZShell::settings
+535
View File
@@ -0,0 +1,535 @@
#include "listnode.hpp"
#include <qjsonarray.h>
#include <qjsonobject.h>
namespace ZShell::settings {
using Qt::StringLiterals::operator""_s;
namespace {
QString valuesKey() {
return u"values"_s;
}
void deleteNode(Node* node) {
node->detachFallback();
node->setParent(nullptr);
node->deleteLater();
}
bool isVariantListType(const QVariant& value) {
return value.typeId() == QMetaType::QVariantList ||
value.typeId() == qMetaTypeId<QList<QVariantMap>>();
}
QList<QVariantMap> toMaps(const QVariant& value) {
if (value.typeId() == qMetaTypeId<QList<QVariantMap>>())
return value.value<QList<QVariantMap>>();
const auto list = value.toList();
QList<QVariantMap> maps;
maps.reserve(list.count());
for (const auto& el : list) {
if (el.typeId() != QMetaType::QVariantMap) {
qCCritical(
lcSettings,
"Expected a map in list properties, got %s.",
el.typeName());
continue;
}
maps << el.toMap();
}
return maps;
}
void syncNodeRecursive(Node* node, const QVariantMap& props) {
const auto& schema = node->schema();
for (const auto& [key, val] : props.asKeyValueRange()) {
const auto* desc = schema.get(key);
if (!desc) {
qCCritical(
lcSettings,
"Unknown key %s in props for %s, something is wrong.",
qUtf8Printable(key),
qUtf8Printable(node->path()));
continue;
}
// `setValue` warns on type mismatches itself
if (!desc->isNode) {
node->setValue(key, val);
continue;
}
auto* const child = node->value(key).value<Node*>();
// Nested lists take their elements straight from props
if (auto* const list = qobject_cast<ListNode*>(child)) {
if (!isVariantListType(val)) {
qCCritical(
lcSettings,
"Expected a list for node key %s on %s, got %s.",
qUtf8Printable(key),
qUtf8Printable(node->path()),
val.typeName());
continue;
}
list->setValue(valuesKey(), val);
continue;
}
if (val.typeId() != QMetaType::QVariantMap) {
qCCritical(
lcSettings,
"Expected a map for node key %s on %s, got %s.",
qUtf8Printable(key),
qUtf8Printable(node->path()),
val.typeName());
continue;
}
syncNodeRecursive(child, val.toMap());
}
}
} // namespace
ListNode::ListNode(ListNode* fallback, QObject* parent, bool globalOnly)
: Node(fallback, parent, globalOnly) {
if (fallback) {
// Disconnect generic fallback notify, lists use a custom one
QObject::disconnect(fallback, &ListNode::optionChanged, this, nullptr);
if (!m_globalOnly)
QObject::connect(
fallback,
&ListNode::elementsChanged,
this,
&ListNode::onFallbackListNotify);
}
}
qsizetype ListNode::count() const {
return m_elements.count();
}
QVariantList ListNode::values() const {
QVariantList list;
list.reserve(m_elements.count());
for (auto* const element : m_elements)
list << QVariant::fromValue(element);
return list;
}
void ListNode::remove(qsizetype index) {
if (rejectGlobalMutation()) return;
const WriteScope scope(this, WriteOrigin::Qml);
if (!validIndex(index)) {
qCWarning(
lcSettings,
"Attempted to remove invalid index %lld from list node %s, "
"ignoring.",
index,
qUtf8Printable(path()));
recordWrite(valuesKey(), false);
return;
}
deleteNode(m_elements.takeAt(index));
recordWrite(valuesKey(), true);
emit countChanged();
emit elementsChanged({}, {index}, {});
}
void ListNode::move(qsizetype from, qsizetype to) {
if (rejectGlobalMutation()) return;
const WriteScope scope(this, WriteOrigin::Qml);
const auto fromInvalid = !validIndex(from);
if (fromInvalid || !validIndex(to)) {
if (fromInvalid)
qCWarning(
lcSettings,
"Attempted to move invalid index %lld to index %lld on list "
"node %s, ignoring.",
from,
to,
qUtf8Printable(path()));
else
qCWarning(
lcSettings,
"Attempted to move index %lld to invalid index %lld on list "
"node %s, ignoring.",
from,
to,
qUtf8Printable(path()));
recordWrite(valuesKey(), false);
return;
}
m_elements.move(from, to);
recordWrite(valuesKey(), true);
emit elementsChanged({}, {}, {{from, to}});
}
void ListNode::clear() {
if (rejectGlobalMutation()) return;
const WriteScope scope(this, WriteOrigin::Qml);
if (m_elements.isEmpty()) {
recordWrite(valuesKey(), false);
return;
}
const auto len = m_elements.count();
for (auto* const element : std::as_const(m_elements))
deleteNode(element);
m_elements.clear();
recordWrite(valuesKey(), true);
emit countChanged();
NodeChanges removed(len);
std::iota(removed.begin(), removed.end(), 0);
emit elementsChanged({}, removed, {});
}
QString ListNode::pathFor(const QString& key) const {
return elementPath(path(), key);
}
const Schema& ListNode::schema() const {
// Offset +1 so count is not registered (allow read only cause values is read only)
static const auto k_schema = Schema::build(
&staticMetaObject, Node::staticMetaObject.propertyCount() + 1, true);
return k_schema;
}
QVariant ListNode::value(const QString& key) const {
if (key != valuesKey()) {
qCCritical(
lcSettings,
"Attempted to read %s on list node %s. List nodes only have a "
"'values' key, something is wrong.",
qUtf8Printable(key),
qUtf8Printable(path()));
return {};
}
return QVariant::fromValue(m_elements);
}
bool ListNode::setValue(const QString& key, const QVariant& value) {
return setValue(key, value, nullptr);
}
bool ListNode::setValue(
const QString& key, const QVariant& value, QList<Diagnostic>* diagnostics) {
if (key != valuesKey()) {
qCCritical(
lcSettings,
"Attempted to write %s on list node %s. List nodes only have a "
"'values' key, something is wrong.",
qUtf8Printable(key),
qUtf8Printable(path()));
return false;
}
const auto len = m_elements.count();
for (auto* const element : std::as_const(m_elements))
deleteNode(element);
m_elements.clear();
if (value.typeId() == qMetaTypeId<QList<Node*>>()) {
// On reset to fallback
const auto nodes = value.value<QList<Node*>>();
for (auto* const node : nodes)
insertNode(m_elements.count(), node);
} else if (isVariantListType(value)) {
// From defaults or QML
const auto list = toMaps(value);
for (qsizetype i = 0; i < list.count(); ++i)
insertNode(i, list.at(i), fallbackFor(i));
} else if (value.typeId() == QMetaType::QJsonArray) {
// From syncJson
const auto array = value.toJsonArray();
for (const auto& json : array)
insertNode(m_elements.count(), json.toObject(), *diagnostics);
insertMissingDefaults();
} else {
qCCritical(
lcSettings,
"Unexpected type %s for list node %s, something is wrong.",
value.typeName(),
qUtf8Printable(path()));
return false;
}
if (!recordWrite(valuesKey(), true)) return false;
if (len != m_elements.count()) emit countChanged();
NodeChanges added(m_elements.count());
NodeChanges removed(len);
std::iota(added.begin(), added.end(), 0);
std::iota(removed.begin(), removed.end(), 0);
emit elementsChanged(added, removed, {});
return true;
}
void ListNode::insertMissingDefaults() {
if (fallbackNode()) return;
const auto* desc = getDescriptor();
if (!desc) return;
const auto idKey = desc->identityKey();
if (idKey.isEmpty()) return;
if (!m_elements.isEmpty() && !m_elements.first()->schema().get(idKey)) {
qCCritical(
lcSettings,
"List %s has identityKey '%s' which its elements don't define.",
qUtf8Printable(path()),
qUtf8Printable(idKey));
return;
}
QHash<QString, QList<Node*>> unmatched;
for (auto* const element : std::as_const(m_elements))
unmatched[element->value(idKey).toString()] << element;
Node* anchor = nullptr;
for (const auto& props : defaultValue()) {
const auto id = props.value(idKey).toString();
if (id.isEmpty()) continue;
auto& candidates = unmatched[id];
if (!candidates.isEmpty()) {
anchor = candidates.takeFirst();
continue;
}
const auto index = anchor ? m_elements.indexOf(anchor) + 1 : 0;
anchor = insertNode(index, props, nullptr);
}
}
void ListNode::resetToDefaults() {
if (isNested()) {
qCCritical(
lcSettings,
"List node %s has a parent list node, resetToDefaults should never "
"be called on it.",
qUtf8Printable(path()));
return;
}
// Don't reset global only list nodes on overlays
if (fallbackNode() && m_globalOnly) return;
const WriteScope scope(this, WriteOrigin::FileReset);
setValue(
valuesKey(),
fallbackNode() ? fallbackNode()->value(valuesKey())
: QVariant::fromValue(defaultValue()));
}
QJsonValue ListNode::toJson(bool sparse) const {
Q_UNUSED(sparse) // Lists can't be sparse
QJsonArray array;
for (auto* const element : m_elements)
array << element->toJson(false);
return array;
}
bool ListNode::syncJson(const QJsonValue& json, QList<Diagnostic>& diagnostics) {
if (!json.isArray()) {
const auto d = Diagnostic::mismatch(ExpectedType::Array, json, path());
qCWarning(
lcSettings,
"Error decoding option %s: %s",
qUtf8Printable(d.option),
qUtf8Printable(d.message));
diagnostics << d;
return false;
}
// Refuse syncs to global only list nodes on overlays
if (rejectGlobalSync(diagnostics)) return false;
const WriteScope scope(this, WriteOrigin::File);
setValue(valuesKey(), json.toArray(), &diagnostics);
return true;
}
bool ListNode::recordWrite(const QString& key, bool changed) {
const auto wasOverride = isOverride(key);
const auto notify = Node::recordWrite(key, changed);
// Elements inside overridden lists have no fallbacks, so detach here
if (!wasOverride && isOverride(key))
for (auto* const element : std::as_const(m_elements))
element->detachFallback();
return notify;
}
QString ListNode::keyOf(const Node* child) const {
return QString::number(m_elements.indexOf(const_cast<Node*>(child)));
}
Node* ListNode::elementAt(qsizetype index) const {
if (!validIndex(index)) return nullptr;
return m_elements.at(index);
}
Node* ListNode::insertElement(const QVariantMap& props, qsizetype index) {
if (rejectGlobalMutation()) return nullptr;
const WriteScope scope(this, WriteOrigin::Qml);
if (!validIndex(index)) // Invalid index means append
index = m_elements.count();
auto* const element = insertNode(index, props, nullptr);
recordWrite(valuesKey(), true);
emit countChanged();
emit elementsChanged({index}, {}, {});
return element;
}
QList<QVariantMap> ListNode::defaultValue() const {
const auto* desc = getDescriptor();
if (!desc) return {};
return desc->defaultValue().value<QList<QVariantMap>>();
}
bool ListNode::isNested() const {
return qobject_cast<ListNode*>(parentNode());
}
bool ListNode::rejectGlobalMutation() const {
if (!m_globalOnly || !fallbackNode()) return false;
qCWarning(
lcSettings,
"Attempted to mutate global list %s from an overlay layer, ignoring. "
"This should not be used, mutate global lists from the global layer "
"instead.",
qUtf8Printable(path()));
return true;
}
bool ListNode::validIndex(qsizetype index) const {
return index >= 0 && index < m_elements.count();
}
const Descriptor* ListNode::getDescriptor() const {
if (!parentNode()) { // List nodes should not be root nodes
qCCritical(
lcSettings,
"List node %s has no parent, something is wrong.",
qUtf8Printable(path()));
return nullptr;
}
const auto* desc = parentNode()->schema().get(key());
if (!desc) {
qCCritical(
lcSettings,
"List node %s is not in parent schema, something is wrong.",
qUtf8Printable(path()));
return nullptr;
}
return desc;
}
Node* ListNode::fallbackFor(qsizetype index) const {
if (isOverride(valuesKey())) return nullptr;
const auto* fallback = static_cast<ListNode*>(fallbackNode());
if (!fallback || !fallback->validIndex(index)) return nullptr;
return fallback->m_elements.at(index);
}
Node* ListNode::insertNode(
qsizetype index, const QVariantMap& props, Node* fallback) {
auto* const node = createElement(fallback);
m_elements.insert(index, node);
const WriteScope scope(node, WriteOrigin::Init);
syncNodeRecursive(node, props);
return node;
}
Node* ListNode::insertNode(qsizetype index, Node* node) {
auto* const copy = createElement(node);
m_elements.insert(index, copy);
return copy;
}
Node* ListNode::insertNode(
qsizetype index, const QJsonObject& json, QList<Diagnostic>& diagnostics) {
// Write origin will be file/file reset, which is correct.
// This function should only be called from the JSON path of setValue.
auto* const node = createElement(
nullptr); // Nodes synced from JSON get no fallback as they are overrides
m_elements.insert(index, node);
node->syncJson(json, diagnostics);
return node;
}
void ListNode::onFallbackListNotify(
const NodeChanges& added,
const NodeChanges& removed,
const MoveChanges& moved) {
if (isOverride(valuesKey())) return;
const WriteScope scope(this, WriteOrigin::Layer);
for (const auto index : removed | std::views::reverse)
deleteNode(m_elements.takeAt(index));
for (const auto index : added)
insertNode(index, fallbackFor(index));
for (const auto& move : moved)
m_elements.move(move.first, move.second);
if (!recordWrite(valuesKey(), true)) return;
if (!added.isEmpty() || !removed.isEmpty()) emit countChanged();
emit elementsChanged(added, removed, moved);
}
} // namespace ZShell::settings
+86
View File
@@ -0,0 +1,86 @@
#pragma once
#include "node.hpp"
namespace ZShell::settings {
class ListNode : public Node {
Q_OBJECT
QML_ANONYMOUS
Q_PROPERTY(qsizetype count READ count NOTIFY countChanged)
Q_PROPERTY(QVariantList values READ values NOTIFY elementsChanged)
using NodeChanges = QList<qsizetype>;
using MoveChanges = QList<QPair<qsizetype, qsizetype>>;
public:
explicit ListNode(
ListNode* fallback, QObject* parent = nullptr, bool globalOnly = false);
[[nodiscard]] qsizetype count() const;
[[nodiscard]] QVariantList values() const;
Q_INVOKABLE void remove(qsizetype index);
Q_INVOKABLE void move(qsizetype from, qsizetype to);
Q_INVOKABLE void clear();
[[nodiscard]] QString pathFor(const QString& key) const override;
[[nodiscard]] const Schema& schema() const override;
[[nodiscard]] QVariant value(const QString& key) const override;
bool setValue(const QString& key, const QVariant& value) override;
bool setValue(
const QString& key,
const QVariant& value,
QList<Diagnostic>* diagnostics);
void resetToDefaults() override;
[[nodiscard]] QJsonValue toJson(bool sparse = true) const override;
bool syncJson(
const QJsonValue& json, QList<Diagnostic>& diagnostics) override;
signals:
void countChanged();
void elementsChanged(
const NodeChanges& added,
const NodeChanges& removed,
const MoveChanges& moved);
protected:
bool recordWrite(const QString& key, bool changed) override;
[[nodiscard]] QString keyOf(const Node* child) const override;
[[nodiscard]] Node* elementAt(qsizetype index) const;
[[nodiscard]] Node* insertElement(
const QVariantMap& props, qsizetype index = -1);
[[nodiscard]] virtual Node* createElement(Node* fallback) = 0;
private:
QList<Node*> m_elements;
[[nodiscard]] QList<QVariantMap> defaultValue() const;
[[nodiscard]] bool isNested() const;
// Returns true if the mutation should be skipped, overlays cannot mutate global lists
[[nodiscard]] bool rejectGlobalMutation() const;
[[nodiscard]] bool validIndex(qsizetype index) const;
[[nodiscard]] const Descriptor* getDescriptor() const;
[[nodiscard]] Node* fallbackFor(qsizetype index) const;
void insertMissingDefaults();
Node* insertNode(qsizetype index, const QVariantMap& props, Node* fallback);
Node* insertNode(qsizetype index, Node* node);
Node* insertNode(
qsizetype index,
const QJsonObject& json,
QList<Diagnostic>& diagnostics);
void onFallbackListNotify(
const NodeChanges& added,
const NodeChanges& removed,
const MoveChanges& moved);
};
} // namespace ZShell::settings
+208
View File
@@ -0,0 +1,208 @@
#pragma once
#include <qmetatype.h>
#include <qobject.h>
#include <qvariant.h>
#include "listnode.hpp"
#include "schema.hpp"
namespace ZShell::settings {
inline QVariantMap vmap(
std::initializer_list<std::pair<QString, QVariant>> entries) {
QVariantMap map;
for (const auto& [key, value] : entries)
map.insert(key, value);
return map;
}
template <typename... Ts> inline QList<QMetaType> unionTypes() {
static_assert(sizeof...(Ts) >= 2, "A union needs at least two types");
static_assert(sizeof...(Ts) <= 4, "A union cannot have more than 4 types");
static_assert(
(!std::is_same_v<Ts, QVariant> && ...),
"A union cannot contain QVariant");
return {QMetaType::fromType<Ts>()...};
}
namespace detail {
template <typename T> inline bool compare(const T& a, const T& b) {
return a == b;
}
template <std::floating_point T> bool compare(const T& a, const T& b) {
return qFuzzyCompare(a + T(1), b + T(1));
}
template <std::floating_point T>
bool compare(const QList<T>& a, const QList<T>& b) {
if (a.size() != b.size()) return false;
for (qsizetype i = 0; i < a.size(); ++i)
if (!compare(a.at(i), b.at(i))) return false;
return true;
}
} // namespace detail
} // namespace ZShell::settings
#define DEFAULT_ARG(...) __VA_ARGS__
#define SETTINGS_NODE_NO_CTOR(Class, Base) \
Q_OBJECT \
\
public: \
[[nodiscard]] const ZShell::settings::Schema& schema() const override { \
static const auto schema = ZShell::settings::Schema::build( \
&staticMetaObject, Base::staticMetaObject.propertyCount()); \
return schema; \
} \
\
private: \
using Self = Class; // For use in the below macros
#define SETTINGS_NODE(Class, Base) \
SETTINGS_NODE_NO_CTOR(Class, Base) \
QML_ANONYMOUS \
\
public: \
explicit Class( \
Class* fallback = nullptr, \
QObject* parent = nullptr, \
bool globalOnly = false) \
: Base(fallback, parent, globalOnly) {} \
\
private:
#define SETTINGS_PROPERTY_IMPL(Type, name, global, defaultVal, ...) \
Q_PROPERTY(Type name READ name WRITE set_##name NOTIFY name##Changed) \
\
public: \
[[nodiscard]] Type name() const { \
if (global || m_globalOnly) warnGlobalRead(QStringLiteral(#name)); \
return m_##name; \
} \
\
void set_##name(const Type& value) { \
if (!true /* TODO: validation */) return; \
\
if (rejectInvalidWrite(QStringLiteral(#name), value)) \
return; /* Skip writes of the wrong type */ \
\
if (rejectGlobalWrite(QStringLiteral(#name))) \
return; /* Skip writes to global only keys */ \
\
const auto needsNotify = \
!ZShell::settings::detail::compare(value, m_##name); \
m_##name = value; \
if (recordWrite(QStringLiteral(#name), needsNotify)) \
Q_EMIT name##Changed(); \
} \
\
Q_SIGNAL void name##Changed(); \
\
private: \
Type m_##name = fallbackValue(&Self::m_##name, defaultVal); \
inline static const bool s_register_##name = \
(ZShell::settings::Schema::annotate( \
&staticMetaObject, \
QStringLiteral(#name), \
{.defaultValue = QVariant::fromValue<Type>(defaultVal), \
.globalOnly = global, \
__VA_ARGS__}), \
true);
#define SETTINGS_PROPERTY(Type, name, defaultVal, ...) \
SETTINGS_PROPERTY_IMPL( \
Type, name, false, DEFAULT_ARG(defaultVal), __VA_ARGS__)
// Defines a global property on a node. Shorthand for .globalOnly = true.
#define SETTINGS_GLOBAL_PROPERTY(Type, name, defaultVal, ...) \
SETTINGS_PROPERTY_IMPL( \
Type, name, true, DEFAULT_ARG(defaultVal), __VA_ARGS__)
// Defines a subobject property on a node. Subobject properties are CONSTANT.
#define SETTINGS_SUBOBJECT_IMPL(Type, name, global) \
Q_PROPERTY(Type* name READ name CONSTANT) \
\
public: \
[[nodiscard]] Type* name() const { \
if (global || m_globalOnly) warnGlobalRead(QStringLiteral(#name)); \
return m_##name; \
} \
\
private: \
Type* m_##name = \
new Type(fallbackValue(&Self::m_##name, nullptr), this, global); \
inline static const bool s_register_##name = \
(ZShell::settings::Schema::annotate( \
&staticMetaObject, \
QStringLiteral(#name), \
{.defaultValue = {}, .globalOnly = global}), \
true);
#define SETTINGS_SUBOBJECT(Type, name) \
SETTINGS_SUBOBJECT_IMPL(Type, name, false)
#define SETTINGS_GLOBAL_SUBOBJECT(Type, name) \
SETTINGS_SUBOBJECT_IMPL(Type, name, true)
#define SETTINGS_LIST_TYPE(Element, Name) \
class Name : public ZShell::settings::ListNode { \
Q_OBJECT \
QML_ANONYMOUS \
\
public: \
explicit Name( \
Name* fallback = nullptr, \
QObject* parent = nullptr, \
bool globalOnly = false) \
: ZShell::settings::ListNode(fallback, parent, globalOnly) {} \
\
[[nodiscard]] Q_INVOKABLE Element* at( \
qsizetype index) const { /* Format ugh */ \
return static_cast<Element*>(elementAt(index)); \
} \
[[nodiscard]] Q_INVOKABLE Element* insert( \
const QVariantMap& props, qsizetype index = -1) { \
return static_cast<Element*>(insertElement(props, index)); \
} \
\
protected: \
[[nodiscard]] ZShell::settings::Node* createElement( \
ZShell::settings::Node* fallback) override { \
return new Element(static_cast<Element*>(fallback), this); \
} \
};
#define SETTINGS_LIST_IMPL(Type, name, global, defaultVal, ...) \
Q_PROPERTY(Type* name READ name CONSTANT) \
\
public: \
[[nodiscard]] Type* name() const { \
if (global || m_globalOnly) warnGlobalRead(QStringLiteral(#name)); \
return m_##name; \
} \
\
private: \
Type* m_##name = \
new Type(fallbackValue(&Self::m_##name, nullptr), this, global); \
inline static const bool s_register_##name = \
(ZShell::settings::Schema::annotate( \
&staticMetaObject, \
QStringLiteral(#name), \
{.defaultValue = \
QVariant::fromValue<QList<QVariantMap>>(defaultVal), \
.globalOnly = global, \
__VA_ARGS__}), \
true);
#define SETTINGS_LIST(Type, name, defaultVal, ...) \
SETTINGS_LIST_IMPL(Type, name, false, DEFAULT_ARG(defaultVal), __VA_ARGS__)
#define SETTINGS_GLOBAL_LIST(Type, name, defaultVal, ...) \
SETTINGS_LIST_IMPL(Type, name, true, DEFAULT_ARG(defaultVal), __VA_ARGS__)
+308
View File
@@ -0,0 +1,308 @@
#include "node.hpp"
namespace ZShell::settings {
using Qt::StringLiterals::operator""_s;
Node::Node(Node* fallback, QObject* parent, bool globalOnly)
: QObject(parent)
, m_globalOnly(globalOnly || (parentNode() && parentNode()->m_globalOnly))
, m_rootNode(parentNode() ? parentNode()->rootNode() : this)
, m_fallbackNode(fallback)
, m_writeOrigin(WriteOrigin::Qml)
, m_internalRead(false)
, m_batcher(m_rootNode == this ? new ChangeBatcher(this) : nullptr) {
if (fallback && !m_globalOnly)
QObject::connect(
fallback, &Node::optionChanged, this, &Node::onFallbackNotify);
}
QString Node::key() const {
return parentNode() ? parentNode()->keyOf(this) : QString();
}
QString Node::path() const {
return parentNode() ? parentNode()->pathFor(key()) : key();
}
QString Node::pathFor(const QString& key) const {
const auto p = path();
return p.isEmpty() ? key : p + u'.' + key;
}
QString Node::elementPath(const QString& path, const QString& index) {
return path + u'[' + index + u']';
}
Node* Node::parentNode() const {
return qobject_cast<Node*>(parent());
}
Node* Node::rootNode() const {
return m_rootNode;
}
Node* Node::fallbackNode() const {
return m_fallbackNode;
}
void Node::detachFallback() {
if (m_fallbackNode) {
QObject::disconnect(m_fallbackNode, nullptr, this, nullptr);
m_fallbackNode = nullptr;
}
const auto childNodes = findChildren<Node*>(Qt::FindDirectChildrenOnly);
for (auto* const child : childNodes)
child->detachFallback();
}
bool Node::isGlobalOnly() const {
return m_globalOnly;
}
bool Node::isOverride(const QString& key) const {
return m_overrides.contains(key);
}
const QSet<QString>& Node::overrides() const {
return m_overrides;
}
bool Node::hasContent() const {
return !m_overrides.isEmpty() || m_quarantine ||
std::ranges::any_of(
findChildren<Node*>(Qt::FindDirectChildrenOnly),
&Node::hasContent);
}
QVariant Node::value(const QString& key) const {
const auto* desc = schema().get(key);
if (!desc) {
qCCritical(
lcSettings,
"Attempted to read an unknown key %s, something is wrong.",
qUtf8Printable(pathFor(key)));
return {};
}
// Generated getters warn on global reads, this silences them as the warning is only for QML reads
const InternalRead guard(m_rootNode);
return metaObject()->property(desc->metaIndex).read(this);
}
bool Node::setValue(const QString& key, const QVariant& value) {
const auto* desc = schema().get(key);
if (!desc) {
qCCritical(
lcSettings,
"Attempted to set an unknown key %s, something is wrong.",
qUtf8Printable(pathFor(key)));
return false;
}
if (desc->isNode) {
qCCritical(
lcSettings,
"Attempted to set node %s directly, something is wrong.",
qUtf8Printable(pathFor(key)));
return false;
}
// Type mismatch, conversion should happen before this function is called
if (rejectInvalidWrite(key, value)) return false;
return metaObject()->property(desc->metaIndex).write(this, value);
}
void Node::resetToDefaults() {
// Don't reset global only nodes on overlays, the whole subtree belongs to the global layer
if (m_globalOnly && m_fallbackNode) return;
m_quarantine.reset(); // Reset quarantine as well
// No write scope, callers should create the scope
for (const auto& desc : schema().descriptors()) {
if (desc.isNode)
value(desc.key).value<Node*>()->resetToDefaults();
else if (
!desc.globalOnly() ||
!m_fallbackNode) // Skip resetting global options on overlays
setValue(
desc.key,
m_fallbackNode ? m_fallbackNode->value(desc.key)
: desc.defaultValue());
}
}
const Quarantine* Node::quarantine() const {
return m_quarantine.get();
}
bool Node::rejectInvalidWrite(const QString& key, const QVariant& value) const {
const auto* desc = schema().get(key);
if (!desc || desc->accepts(value.metaType())) return false;
qCWarning(
lcSettings,
"Type mismatch for %s, expected %s got %s",
qUtf8Printable(pathFor(key)),
qUtf8Printable(desc->typeString()),
value.metaType().name());
return true;
}
void Node::warnGlobalRead(const QString& key) const {
if (!m_fallbackNode || m_rootNode->m_internalRead) return;
qCWarning(
lcSettings,
"Global option %s was read from an overlay layer. "
"This should not be used, read global options from the global layer "
"instead.",
qUtf8Printable(pathFor(key)));
}
bool Node::rejectGlobalWrite(const QString& key) {
const auto* desc = schema().get(key);
if (!desc) {
qCCritical(
lcSettings,
"Attempted to check a write for an unknown key %s, something is "
"seriously wrong...",
qUtf8Printable(pathFor(key)));
return false;
}
const auto origin = m_rootNode->m_writeOrigin;
const auto fromUser = origin == WriteOrigin::Qml ||
origin == WriteOrigin::QmlReset;
if ((!m_globalOnly && !desc->globalOnly()) || !fromUser || !m_fallbackNode)
return false;
if (origin == WriteOrigin::QmlReset)
qCWarning(
lcSettings,
"Attempted to reset global option %s from an overlay layer, "
"ignoring. "
"This should not be used, reset global options from the global "
"layer instead.",
qUtf8Printable(pathFor(key)));
else
qCWarning(
lcSettings,
"Attempted to write global option %s from an overlay layer, "
"ignoring. "
"This should not be used, write global options from the global "
"layer instead.",
qUtf8Printable(pathFor(key)));
return true;
}
void Node::warnGlobalSync(QList<Diagnostic>& diagnostics, const QString& path) {
qCWarning(
lcSettings,
"Global option definition %s found in overlay file, ignoring.",
qUtf8Printable(path));
diagnostics << Diagnostic{
.type = DiagnosticType::GlobalOption,
.option = path,
.message = QStringLiteral(
"Global options should not be defined in overlay files"),
};
}
bool Node::rejectGlobalSync(QList<Diagnostic>& diagnostics) const {
if (!m_globalOnly || !m_fallbackNode) return false;
warnGlobalSync(diagnostics, path());
return true;
}
bool Node::recordWrite(const QString& key, bool changed) {
const auto* desc = schema().get(key);
if (!desc) {
qCCritical(
lcSettings,
"Attempted to record a write for an unknown key %s, something is "
"seriously wrong...",
qUtf8Printable(pathFor(key)));
return false;
}
const auto origin = m_rootNode->m_writeOrigin;
const auto fromUser = origin == WriteOrigin::Qml ||
origin == WriteOrigin::QmlReset;
bool dirty = changed;
switch (origin) {
// Init does not notify or write to file
case WriteOrigin::Init:
return false;
// File and qml both count as overrides
case WriteOrigin::File:
case WriteOrigin::Qml:
dirty |= !m_overrides.contains(key);
m_overrides << key;
break;
// Layer is not an override, it is a sync with the fallback value
case WriteOrigin::Layer:
break;
// Both resets clear the override
case WriteOrigin::FileReset:
case WriteOrigin::QmlReset:
dirty |= m_overrides.remove(key);
break;
}
// User writes/reset override quarantine
if (fromUser) dirty |= removeQuarantined(key);
// Both qml and reset write to the file (only write if dirty)
if (fromUser && dirty) m_rootNode->m_batcher->dirty();
if (changed) emit optionChanged(key);
return changed;
}
bool Node::removeQuarantined(const QString& key) {
if (!m_quarantine) return false;
const auto removed = m_quarantine->remove(key);
if (m_quarantine->isEmpty()) m_quarantine.reset();
return removed;
}
ChangeBatcher* Node::batcher() const {
return m_rootNode->m_batcher;
}
QString Node::keyOf(const Node* child) const {
for (const auto& desc : schema().descriptors()) {
if (desc.isNode && child == value(desc.key).value<Node*>())
return desc.key;
}
return {};
}
void Node::onFallbackNotify(const QString& key) {
if (m_overrides.contains(key)) return;
// Don't mirror global options onto overlays
const auto* desc = schema().get(key);
if (desc && desc->globalOnly()) return;
const WriteScope scope(this, WriteOrigin::Layer);
setValue(key, m_fallbackNode->value(key));
}
} // namespace ZShell::settings
+116
View File
@@ -0,0 +1,116 @@
#pragma once
#include <qjsonvalue.h>
#include <qobject.h>
#include <qvariant.h>
#include "changebatcher.hpp"
#include "common.hpp"
#include "quarantine.hpp"
#include "schema.hpp"
namespace ZShell::settings {
class Node : public QObject {
Q_OBJECT
public:
// Global only nodes are inherited, anything inside one is also global only
explicit Node(
Node* fallback, QObject* parent = nullptr, bool globalOnly = false);
[[nodiscard]] QString key() const; // The key of this in the parent node
[[nodiscard]] QString path() const;
[[nodiscard]] virtual QString pathFor(const QString& key) const;
[[nodiscard]] static QString elementPath(
const QString& path, const QString& index);
[[nodiscard]] Node* parentNode() const;
[[nodiscard]] Node* rootNode() const;
[[nodiscard]] Node* fallbackNode() const;
void
detachFallback(); // Recursive, unlinks this and its children from the fallback layer
[[nodiscard]] Q_INVOKABLE bool isGlobalOnly() const;
[[nodiscard]] bool isOverride(const QString& key) const;
[[nodiscard]] const QSet<QString>& overrides() const;
[[nodiscard]] bool hasContent() const; // Recursive
[[nodiscard]] virtual const Schema& schema() const = 0;
[[nodiscard]] virtual QVariant value(const QString& key) const;
virtual bool setValue(
const QString& key,
const QVariant& value); // Returns whether the write was successful or not
virtual void
resetToDefaults(); // Recursive, resets to fallbacks then defaults if not overridden
[[nodiscard]] virtual QJsonValue toJson(bool sparse = true) const = 0;
// Returns false if the entire node was rejected
virtual bool syncJson(
const QJsonValue& json, QList<Diagnostic>& diagnostics) = 0;
[[nodiscard]] const Quarantine* quarantine() const;
signals:
void optionChanged(const QString& key);
protected:
// Null means empty, otherwise it has content
std::unique_ptr<Quarantine> m_quarantine;
const bool m_globalOnly; // Own flag or inherited from the parent node
void warnGlobalRead(const QString& key) const;
// Returns true if the write should be skipped afterwards, the value is not one of the allowed types
[[nodiscard]] bool rejectInvalidWrite(
const QString& key, const QVariant& value) const;
template <typename T>
[[nodiscard]] bool rejectInvalidWrite(
const QString& key, const T& value) const;
// Returns true if the write should be skipped afterwards, overlays cannot write global options
bool rejectGlobalWrite(const QString& key);
static void warnGlobalSync(
QList<Diagnostic>& diagnostics, const QString& path);
bool rejectGlobalSync(QList<Diagnostic>& diagnostics)
const; // Returns true if the sync should be rejected
// Returns true if the notify signal should be emitted
virtual bool recordWrite(const QString& key, bool changed);
[[nodiscard]] bool removeQuarantined(const QString& key);
[[nodiscard]] ChangeBatcher* batcher() const;
[[nodiscard]] virtual QString keyOf(const Node* child) const;
template <typename C, typename T>
[[nodiscard]] T fallbackValue(
T C::* member, std::type_identity_t<T> defaultValue) const;
private:
QSet<QString> m_overrides; // Overridden keys from file/qml writes
Node* const m_rootNode;
Node* m_fallbackNode; // No fallback node either means global tree or inside overridden list
// For root node use only
WriteOrigin m_writeOrigin;
bool m_internalRead;
ChangeBatcher* const m_batcher;
void onFallbackNotify(const QString& key);
friend class WriteScope;
friend class InternalRead;
};
template <typename T>
bool Node::rejectInvalidWrite(const QString& key, const T& value) const {
Q_UNUSED(key)
Q_UNUSED(value)
return false; // Only QVariant unions can be given the wrong type
}
template <typename C, typename T>
T Node::fallbackValue(
T C::* member, std::type_identity_t<T> defaultValue) const {
const auto* fallback = static_cast<const C*>(m_fallbackNode);
return fallback ? fallback->*member : defaultValue;
}
} // namespace ZShell::settings
+219
View File
@@ -0,0 +1,219 @@
#include "objectnode.hpp"
#include <qjsonobject.h>
#include "codecs.hpp"
namespace ZShell::settings {
using Qt::StringLiterals::operator""_s;
ObjectNode::ObjectNode(ObjectNode* fallback, QObject* parent, bool globalOnly)
: Node(fallback, parent, globalOnly) {}
void ObjectNode::resetOption(const QString& key) {
const auto* desc = schema().get(key);
if (!desc) {
qCWarning(lcSettings)
<< "Attempted to reset unknown option" << pathFor(key);
return;
}
// Warn and ignore if this is a global node property and we are an overlay
if (desc->isNode && (m_globalOnly || desc->globalOnly()) &&
fallbackNode()) {
qCWarning(
lcSettings,
"Attempted to reset global node %s, ignoring. "
"This should not be used, reset global nodes from the global layer "
"instead.",
qUtf8Printable(pathFor(key)));
return;
}
const WriteScope scope(this, WriteOrigin::QmlReset);
if (desc->isNode)
value(key).value<Node*>()->resetToDefaults();
else
setValue(
key,
fallbackNode() ? fallbackNode()->value(key) : desc->defaultValue());
}
Descriptor ObjectNode::descriptorFor(const QString& key) const {
const auto* desc = schema().get(key);
if (!desc) {
qCWarning(lcSettings)
<< "Attempted to get descriptor for unknown option" << pathFor(key);
return {};
}
return *desc;
}
QJsonValue ObjectNode::toJson(bool sparse) const {
QJsonObject json;
for (const auto& desc : schema().descriptors()) {
const auto val = value(desc.key);
if (const auto* node = val.value<Node*>()) {
if (!sparse || node->hasContent())
json.insert(desc.key, node->toJson(sparse));
continue;
}
if (sparse && !isOverride(desc.key)) continue;
if (!desc.codec) { // This should not happen
qCCritical(
lcSettings,
"No codec found for type %s, not serialising %s",
desc.type.name(),
qUtf8Printable(pathFor(desc.key)));
continue;
}
json.insert(desc.key, desc.codec->encode(val));
}
if (m_quarantine) return m_quarantine->apply(json);
return json;
}
bool ObjectNode::syncJson(
const QJsonValue& json, QList<Diagnostic>& diagnostics) {
m_quarantine.reset(); // Clear out old quarantine
if (!json.isObject()) {
const auto d = Diagnostic::mismatch(ExpectedType::Object, json, path());
qCWarning(
lcSettings,
"Error decoding option %s: %s",
qUtf8Printable(d.option),
qUtf8Printable(d.message));
diagnostics << d;
return false;
}
// Refuse syncs to global only nodes on overlays
if (rejectGlobalSync(diagnostics)) return false;
const auto obj = json.toObject();
qCDebug(lcSettings) << "Loading JSON into" << metaObject()->className()
<< "with" << obj.size() << "keys:" << obj.keys();
const auto visited = loadFromJson(obj, diagnostics);
resetUnvisited(visited);
return true;
}
void ObjectNode::quarantineKey(const QString& key, const QJsonValue& value) {
if (!m_quarantine) m_quarantine = std::make_unique<ObjectQuarantine>();
m_quarantine->insert(key, value);
}
QSet<QString> ObjectNode::loadFromJson(
const QJsonObject& json, QList<Diagnostic>& diagnostics) {
const WriteScope scope(this, WriteOrigin::File);
QSet<QString> visited;
visited.reserve(json.size());
// Convenience macro for quarantine then skip
#define SKIP \
quarantineKey(key, v); \
continue
for (const auto [k, v] : json.asKeyValueRange()) {
const auto key = k.toString();
const auto* desc = schema().get(key);
if (!desc) {
const auto path = pathFor(key);
qCWarning(lcSettings) << "Unknown option" << path;
diagnostics << Diagnostic{
.type = DiagnosticType::UnknownOption,
.option = path,
.message = QStringLiteral("Unknown option %1").arg(key),
};
SKIP;
}
// Recurse into child nodes
if (desc->isNode) {
qCDebug(lcSettings) << " Recursing into" << key;
auto* const node = value(key).value<Node*>();
if (node->syncJson(v, diagnostics))
visited << key;
else
quarantineKey(
key, v); // Quarantine entire node cause sync failed
continue;
}
if ((m_globalOnly || desc->globalOnly()) && fallbackNode()) {
warnGlobalSync(diagnostics, pathFor(key));
SKIP;
}
if (!desc->codec) { // This should not happen
qCCritical(
lcSettings,
"No codec found for type %s, not loading %s",
desc->type.name(),
qUtf8Printable(pathFor(key)));
SKIP;
}
auto val = desc->codec->decode(v);
if (val.error) {
auto path = pathFor(key);
for (const auto index : std::as_const(val.indexPath))
path = elementPath(path, QString::number(index));
qCWarning(
lcSettings,
"Error decoding option %s: %s",
qUtf8Printable(path),
qUtf8Printable(val.error->message));
val.error->option = path;
diagnostics << *val.error;
SKIP;
}
visited << key;
setValue(key, val.value);
}
#undef SKIP
return visited;
}
void ObjectNode::resetUnvisited(const QSet<QString>& visited) {
const WriteScope scope(this, WriteOrigin::FileReset);
for (const auto& desc : schema().descriptors()) {
if (visited.contains(desc.key)) continue;
// Reset nodes recursively
if (desc.isNode) {
value(desc.key).value<Node*>()->resetToDefaults();
continue;
}
// Skip global options on overlays
if ((m_globalOnly || desc.globalOnly()) && fallbackNode()) continue;
setValue(
desc.key,
fallbackNode() ? fallbackNode()->value(desc.key)
: desc.defaultValue());
}
}
} // namespace ZShell::settings
+35
View File
@@ -0,0 +1,35 @@
#pragma once
#include <qjsonobject.h>
#include <qobject.h>
#include <qset.h>
#include "node.hpp"
namespace ZShell::settings {
class ObjectNode : public Node {
Q_OBJECT
public:
explicit ObjectNode(
ObjectNode* fallback,
QObject* parent = nullptr,
bool globalOnly = false);
Q_INVOKABLE void resetOption(const QString& key);
[[nodiscard]] Q_INVOKABLE Descriptor descriptorFor(const QString& key) const;
[[nodiscard]] QJsonValue toJson(bool sparse = true) const override;
bool syncJson(
const QJsonValue& json, QList<Diagnostic>& diagnostics) override;
private:
void quarantineKey(const QString& key, const QJsonValue& value);
QSet<QString> loadFromJson(
const QJsonObject& json, QList<Diagnostic>& diagnostics);
void resetUnvisited(const QSet<QString>& visited);
};
} // namespace ZShell::settings
+27
View File
@@ -0,0 +1,27 @@
#include "quarantine.hpp"
#include <qjsonobject.h>
namespace ZShell::settings {
void ObjectQuarantine::insert(const QString& key, const QJsonValue& value) {
m_quarantine.insert(key, value);
}
bool ObjectQuarantine::remove(const QString& key) {
return m_quarantine.remove(key);
}
bool ObjectQuarantine::isEmpty() const {
return m_quarantine.isEmpty();
}
QJsonValue ObjectQuarantine::apply(const QJsonValue& json) const {
auto result = json.toObject();
for (const auto& [key, value] : m_quarantine.asKeyValueRange())
if (!result.contains(key)) // Don't clobber existing values
result.insert(key, value);
return result;
}
} // namespace ZShell::settings
+33
View File
@@ -0,0 +1,33 @@
#pragma once
#include <qhash.h>
#include <qjsonvalue.h>
#include <qtclasshelpermacros.h>
namespace ZShell::settings {
class Quarantine {
public:
explicit Quarantine() = default;
virtual ~Quarantine() = default;
virtual void insert(const QString& key, const QJsonValue& value) = 0;
[[nodiscard]] virtual bool remove(const QString& key) = 0;
[[nodiscard]] virtual bool isEmpty() const = 0;
[[nodiscard]] virtual QJsonValue apply(const QJsonValue& json) const = 0;
Q_DISABLE_COPY_MOVE(Quarantine)
};
class ObjectQuarantine : public Quarantine {
public:
void insert(const QString& key, const QJsonValue& value) override;
[[nodiscard]] bool remove(const QString& key) override;
[[nodiscard]] bool isEmpty() const override;
[[nodiscard]] QJsonValue apply(const QJsonValue& json) const override;
private:
QHash<QString, QJsonValue> m_quarantine;
};
} // namespace ZShell::settings
+47
View File
@@ -0,0 +1,47 @@
#include "rootnode.hpp"
#include <qjsonobject.h>
namespace ZShell::settings {
RootNode::RootNode(const QString& path, RootNode* fallback, QObject* parent)
: ObjectNode(fallback, parent), m_file(new SettingsFile(path, this)) {
QObject::connect(
batcher(), &ChangeBatcher::dirtied, this, &RootNode::saveToFile);
QObject::connect(
m_file, &SettingsFile::changed, this, &RootNode::reloadFromFile);
QObject::connect(
m_file, &SettingsFile::readFailed, this, [this](const QString& error) {
emit treeLoadFailed(this, error);
});
QObject::connect(
m_file, &SettingsFile::writeFailed, this, [this](const QString& error) {
emit treeSaveFailed(this, error);
});
}
void RootNode::load() {
if (m_file->load() != SettingsFile::LoadResult::Changed) reloadFromFile();
}
QList<Diagnostic> RootNode::diagnostics() const {
return m_diagnostics;
}
void RootNode::reloadFromFile() {
const auto oldDiagnostics = m_diagnostics;
m_diagnostics.clear();
const auto data = m_file->read();
syncJson(data ? data.value() : QJsonObject(), m_diagnostics);
if (m_diagnostics != oldDiagnostics) emit diagnosticsChanged();
emit treeLoaded(this);
}
void RootNode::saveToFile() {
m_file->write(toJson());
}
} // namespace ZShell::settings
+38
View File
@@ -0,0 +1,38 @@
#pragma once
#include "common.hpp"
#include "objectnode.hpp"
#include "settingsfile.hpp"
namespace ZShell::settings {
class RootNode : public ObjectNode {
Q_OBJECT
Q_PROPERTY(
QList<ZShell::settings::Diagnostic> diagnostics READ diagnostics NOTIFY
diagnosticsChanged)
public:
explicit RootNode(
const QString& path, RootNode* fallback, QObject* parent = nullptr);
[[nodiscard]] QList<Diagnostic> diagnostics() const;
void load();
signals:
void diagnosticsChanged();
void treeLoaded(RootNode* self);
void treeLoadFailed(RootNode* self, const QString& error);
void treeSaveFailed(RootNode* self, const QString& error);
private:
SettingsFile* const m_file;
QList<Diagnostic> m_diagnostics;
void reloadFromFile();
void saveToFile();
};
} // namespace ZShell::settings
+137
View File
@@ -0,0 +1,137 @@
#include "schema.hpp"
#include "codecs.hpp"
#include "node.hpp"
namespace {
Q_LOGGING_CATEGORY(lcSchema, "zshell.settings.schema", QtInfoMsg)
} // namespace
namespace ZShell::settings {
namespace {
// Descriptor annotations are stored here by Schema::annotate, which is called in the CONFIG_XXX macros.
// Schema::build then takes them from here.
QHash<const QMetaObject*, QHash<QString, Annotation>>& annotationCache() {
static QHash<const QMetaObject*, QHash<QString, Annotation>> s_cache;
return s_cache;
}
// Union options carry their alternatives in the annotation, everything else goes by property type
const ValueCodec* resolveCodec(Descriptor& desc) {
auto& allowed = desc.annotation.allowedTypes;
if (allowed.isEmpty()) return ValueCodec::codecFor(desc.type);
if (desc.type.id() != QMetaType::QVariant) {
qCCritical(
lcSchema,
"Allowed types are only valid for QVariant properties, ignoring "
"them for %s",
qUtf8Printable(desc.key));
allowed.clear();
return ValueCodec::codecFor(desc.type);
}
if (const auto* codec = ValueCodec::unionFor(allowed)) return codec;
allowed.clear();
return nullptr;
}
bool isNodeType(const QMetaType& type) {
if (!type.flags().testFlag(QMetaType::PointerToQObject)) return false;
const auto* meta = type.metaObject();
return meta && meta->inherits(&Node::staticMetaObject);
}
} // namespace
QString Descriptor::typeString() const {
if (annotation.allowedTypes.isEmpty())
return QString::fromUtf8(type.name());
QStringList names;
names.reserve(annotation.allowedTypes.size());
for (const auto& allowed : annotation.allowedTypes)
names << QString::fromUtf8(allowed.name());
return names.join(QStringLiteral(" | "));
}
bool Descriptor::accepts(const QMetaType& valueType) const {
// Unset is a valid state for QVariant options, they are simply absent from the file
if (type.id() == QMetaType::QVariant && !valueType.isValid()) return true;
if (annotation.allowedTypes.isEmpty()) return type == valueType;
return annotation.allowedTypes.contains(valueType);
}
Schema Schema::build(
const QMetaObject* meta, int baseOffset, bool includeReadOnly) {
Schema schema;
schema.m_descriptors.reserve(meta->propertyCount() - baseOffset);
// Descriptors are taken from cache since this should be called exactly once per class
const auto annotations = annotationCache().take(meta);
qCDebug(lcSchema) << "Building schema for" << meta->className();
for (int i = baseOffset; i < meta->propertyCount(); ++i) {
const auto prop = meta->property(i);
const auto key = QString::fromUtf8(prop.name());
const auto isNode = isNodeType(prop.metaType());
// Skip read only properties (unless includeReadOnly)
if (!isNode && !includeReadOnly && !prop.isWritable()) {
qCDebug(lcSchema) << " Skipping computed property" << key;
continue;
}
qCDebug(lcSchema) << " Adding property" << key;
Descriptor desc{
.key = key,
.type = prop.metaType(),
.metaIndex = i,
.isNode = isNode,
.annotation = annotations.value(key),
};
if (!isNode) {
desc.codec = resolveCodec(desc);
if (!desc.codec)
qCCritical(
lcSchema,
"No codec for %s of type %s, it will not be loaded or "
"saved",
qUtf8Printable(key),
desc.type.name());
}
schema.m_descriptors.append(std::move(desc));
schema.m_keyToIndex.insert(key, schema.m_descriptors.size() - 1);
}
return schema;
}
void Schema::annotate(
const QMetaObject* meta, const QString& key, Annotation annotation) {
annotationCache()[meta].insert(key, std::move(annotation));
}
const QList<Descriptor>& Schema::descriptors() const {
return m_descriptors;
}
const Descriptor* Schema::get(const QString& key) const {
const auto it = m_keyToIndex.find(key);
return it != m_keyToIndex.end() ? &m_descriptors[it.value()] : nullptr;
}
} // namespace ZShell::settings
+89
View File
@@ -0,0 +1,89 @@
#pragma once
#include <qhash.h>
#include <qlist.h>
#include <qmetaobject.h>
#include <qmetatype.h>
#include <qqmlintegration.h>
#include <qstring.h>
#include <qvariant.h>
namespace ZShell::settings {
class ValueCodec;
struct Annotation {
QVariant defaultValue;
bool globalOnly = false;
// NOLINTNEXTLINE(readability-redundant-member-init) callers will get missing field init warnings without
QList<QMetaType> allowedTypes = {}; // For QVariant properties
// NOLINTNEXTLINE(readability-redundant-member-init)
QString identityKey = {};
};
namespace detail {
// Return trivially copyable types (e.g. bool) by value and others (e.g. QVariant) by const ref
template <typename T>
using AnnotationReturn = std::conditional_t<
std::is_trivially_copyable_v<T> && sizeof(T) <= sizeof(void*),
T,
const T&>;
} // namespace detail
#define ANNOTATION(Type, name) \
Q_PROPERTY(Type name READ name) \
\
public: \
[[nodiscard]] detail::AnnotationReturn<Type> name() const { \
return annotation.name; \
} \
\
private:
struct Descriptor {
Q_GADGET
QML_VALUE_TYPE(descriptor)
Q_PROPERTY(QString key MEMBER key)
Q_PROPERTY(QString type READ typeString)
Q_PROPERTY(int metaIndex MEMBER metaIndex)
Q_PROPERTY(bool isNode MEMBER isNode)
ANNOTATION(QVariant, defaultValue)
ANNOTATION(bool, globalOnly)
ANNOTATION(QString, identityKey)
public:
QString key;
QMetaType type;
int metaIndex;
bool isNode;
Annotation annotation;
const ValueCodec* codec = nullptr; // Null for nodes and unsupported types
[[nodiscard]] QString typeString() const;
[[nodiscard]] bool accepts(const QMetaType& valueType) const;
};
#undef ANNOTATION
class Schema {
public:
static Schema build(
const QMetaObject* meta, int baseOffset, bool includeReadOnly = false);
static void annotate(
const QMetaObject* meta, const QString& key, Annotation annotation);
[[nodiscard]] const QList<Descriptor>& descriptors() const;
[[nodiscard]] const Descriptor* get(const QString& key) const;
private:
explicit Schema() = default;
QList<Descriptor> m_descriptors;
QHash<QString, qsizetype> m_keyToIndex;
};
} // namespace ZShell::settings
+250
View File
@@ -0,0 +1,250 @@
#include "settingsfile.hpp"
#include <qdir.h>
#include <qfile.h>
#include <qfileinfo.h>
#include <qjsonarray.h>
#include <qjsondocument.h>
#include <qjsonobject.h>
#include <qloggingcategory.h>
#include <qsavefile.h>
#include <utility>
namespace {
Q_LOGGING_CATEGORY(lcSettingsFile, "zshell.settings.file", QtInfoMsg)
} // namespace
namespace ZShell::settings {
using Qt::StringLiterals::operator""_s;
namespace {
// Max retries for loads which fail due to malformed JSON, e.g. partial writes
constexpr int k_maxLoadRetries = 3;
} // namespace
SettingsFile::SettingsFile(QString path, QObject* parent)
: QObject(parent)
, m_path(std::move(path))
, m_watcher(new QFileSystemWatcher(this))
, m_saveDebounce(new QTimer(this))
, m_loadDebounce(new QTimer(this))
, m_loadRetries(0) {
m_saveDebounce->setSingleShot(true);
m_saveDebounce->setInterval(500); // Save at most once every 500ms
m_loadDebounce->setSingleShot(true);
m_loadDebounce->setInterval(50); // Coalesce watcher events within 50ms
QObject::connect(
m_loadDebounce, &QTimer::timeout, this, &SettingsFile::onLoadDebounced);
QObject::connect(
m_watcher,
&QFileSystemWatcher::fileChanged,
this,
&SettingsFile::onFileChanged);
QObject::connect(
m_watcher,
&QFileSystemWatcher::directoryChanged,
this,
&SettingsFile::onDirChanged);
initWatcher();
}
std::optional<QJsonValue> SettingsFile::read() const {
return m_lastData;
}
void SettingsFile::write(const QJsonValue& json) {
m_pendingWrite = json;
save();
}
void SettingsFile::onFileChanged() {
initWatcher(); // Re-add path in case the file was replaced
scheduleLoad();
}
void SettingsFile::onDirChanged() {
// Ignore if the file is already watched
if (m_watcher->files().contains(m_path)) return;
initWatcher();
if (QFile::exists(m_path)) scheduleLoad();
}
void SettingsFile::initWatcher() {
const QFileInfo info(m_path);
const auto dir = info.absolutePath();
if (QDir(dir).exists() && !m_watcher->directories().contains(dir))
m_watcher->addPath(dir);
if (info.exists() && !m_watcher->files().contains(m_path))
m_watcher->addPath(m_path);
}
void SettingsFile::scheduleLoad() {
// Ignore if a load is already pending
if (m_loadDebounce->isActive()) return;
m_loadRetries = 0;
m_loadDebounce->start();
}
void SettingsFile::onLoadDebounced() {
const auto isFinalTry = m_loadRetries >= k_maxLoadRetries;
if (load(isFinalTry) == LoadResult::ParseError && !isFinalTry) {
// Likely a partial write, retry after another debounce
++m_loadRetries;
qCDebug(
lcSettingsFile,
"Retrying load of %s (%d/%d)",
qUtf8Printable(m_path),
m_loadRetries,
k_maxLoadRetries);
m_loadDebounce->start();
}
}
SettingsFile::LoadResult SettingsFile::load(bool reportErrors) {
QFile file(m_path);
if (!file.exists()) {
if (m_lastData) {
m_lastData = std::nullopt;
emit changed();
return LoadResult::Changed;
}
return LoadResult::Unchanged;
}
if (!file.open(QIODevice::ReadOnly)) {
qCWarning(
lcSettingsFile,
"Failed to open %s for reading: %s",
qUtf8Printable(m_path),
qUtf8Printable(file.errorString()));
emit readFailed(
QStringLiteral("Failed to open: %1").arg(file.errorString()));
return LoadResult::Error;
}
const auto data = file.readAll();
file.close();
QJsonParseError error;
const auto doc = QJsonDocument::fromJson(data, &error);
if (error.error != QJsonParseError::NoError) {
if (reportErrors) {
qCWarning(
lcSettingsFile,
"Failed to parse %s as JSON: %s",
qUtf8Printable(m_path),
qUtf8Printable(error.errorString()));
emit readFailed(
QStringLiteral("Failed to parse: %1").arg(error.errorString()));
} else {
qCDebug(
lcSettingsFile,
"Failed to parse %s as JSON: %s",
qUtf8Printable(m_path),
qUtf8Printable(error.errorString()));
}
return LoadResult::ParseError;
}
const auto json = doc.isObject() ? QJsonValue(doc.object())
: QJsonValue(doc.array());
if (json == m_lastData) return LoadResult::Unchanged;
// Clear pending write, stuff loaded from file should take precedence
m_pendingWrite = std::nullopt;
m_lastData = json;
emit changed();
qCDebug(lcSettingsFile) << "Read JSON from" << m_path;
qCDebug(lcSettingsFile) << " " << json;
return LoadResult::Changed;
}
void SettingsFile::save() {
if (!m_pendingWrite) return;
if (m_saveDebounce->isActive()) {
// Queue save for debounce end
QObject::connect(
m_saveDebounce,
&QTimer::timeout,
this,
&SettingsFile::save,
// NOLINTNEXTLINE(clang-analyzer-optin.core.EnumCastOutOfRange) ConnectionType is OR-able by design
static_cast<Qt::ConnectionType>(
Qt::UniqueConnection | Qt::SingleShotConnection));
return;
}
// Debounce regardless of whether the save actually succeeded
m_saveDebounce->start();
const auto json = m_pendingWrite.value();
const auto data = (json.isObject() ? QJsonDocument(json.toObject())
: QJsonDocument(json.toArray()))
.toJson();
m_pendingWrite = std::nullopt;
const auto dir = QFileInfo(m_path).absolutePath();
if (!QDir().mkpath(dir)) {
qCWarning(lcSettingsFile) << "Failed to create dir" << dir;
emit writeFailed(QStringLiteral("Failed to create parent directory"));
return;
}
// Write atomically
QSaveFile file(m_path);
if (!file.open(QIODevice::WriteOnly)) {
qCWarning(
lcSettingsFile,
"Failed to open %s for writing: %s",
qUtf8Printable(m_path),
qUtf8Printable(file.errorString()));
emit writeFailed(
QStringLiteral("Failed to open: %1").arg(file.errorString()));
return;
}
file.write(data);
if (!file.commit()) {
qCWarning(
lcSettingsFile,
"Failed to write %s: %s",
qUtf8Printable(m_path),
qUtf8Printable(file.errorString()));
emit writeFailed(
QStringLiteral("Failed to write: %1").arg(file.errorString()));
return;
}
m_lastData = json;
qCDebug(lcSettingsFile) << "Saved" << m_path;
initWatcher(); // The file may have just been created, or replaced by the rename
}
} // namespace ZShell::settings

Some files were not shown because too many files have changed in this diff Show More