add nightlight hyprland plugin + rename hyprsunsetmanager -> nightlightmanager to control both

This commit is contained in:
2026-08-31 13:53:37 +02:00
parent 703d804746
commit d4e936a7e2
11 changed files with 1087 additions and 222 deletions
+2 -2
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
@@ -25,4 +25,4 @@ qml_module(ZShell-services
PkgConfig::Aubio
PkgConfig::Cava
Sensors::Sensors
)
)
@@ -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,444 @@
#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 NEUTRAL_KELVIN = 6500;
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;
// data = enabled,animating,last,current
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