#include "nightlightmanager.hpp" #include #include #include #include #include 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 /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