C++ / fmt (pull_request) Failing after 4s
C++ / build (pull_request) Failing after 12s
JS/TS / fmt (pull_request) Successful in 15s
JS/TS / lint (pull_request) Successful in 15s
C++ / clang-tidy (pull_request) Failing after 33s
Python / static (pull_request) Failing after 36s
Rust / fmt (pull_request) Successful in 1m14s
Rust / build (pull_request) Successful in 1m49s
Rust / clippy (pull_request) Successful in 1m33s
Python / verify (pull_request) Successful in 2m10s
443 lines
10 KiB
C++
443 lines
10 KiB
C++
#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
|