add nightlight hyprland plugin + rename hyprsunsetmanager -> nightlightmanager to control both
This commit is contained in:
@@ -26,12 +26,14 @@ Singleton {
|
|||||||
service.toggle();
|
service.toggle();
|
||||||
}
|
}
|
||||||
|
|
||||||
HyprsunsetManager {
|
NightlightManager {
|
||||||
id: service
|
id: service
|
||||||
|
|
||||||
activeAuto: Config.general.color.scheduleHyprsunset
|
activeAuto: Config.general.color.scheduleHyprsunset
|
||||||
endTime: root.end
|
endTime: root.end
|
||||||
startTime: root.start
|
startTime: root.start
|
||||||
temp: root.temp
|
temp: root.temp
|
||||||
|
fadeDuration: Config.general.color.nativeFadeDuration / 1000
|
||||||
|
useNativeNightlight: Config.general.color.useNativeNightlight
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,3 +80,11 @@ add_subdirectory(Components)
|
|||||||
add_subdirectory(Blobs)
|
add_subdirectory(Blobs)
|
||||||
add_subdirectory(Config)
|
add_subdirectory(Config)
|
||||||
add_subdirectory(Llm)
|
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()
|
||||||
|
|||||||
@@ -98,6 +98,8 @@ class ColorSettings : public ConfigObject {
|
|||||||
CFG_PROPERTY(int, hyprsunsetTemp, 2600)
|
CFG_PROPERTY(int, hyprsunsetTemp, 2600)
|
||||||
CFG_PROPERTY(QString, mode, QStringLiteral("dark"))
|
CFG_PROPERTY(QString, mode, QStringLiteral("dark"))
|
||||||
CFG_PROPERTY(bool, neovimColors, false)
|
CFG_PROPERTY(bool, neovimColors, false)
|
||||||
|
CFG_PROPERTY(bool, useNativeNightlight, true)
|
||||||
|
CFG_PROPERTY(int, nativeFadeDuration, 2000)
|
||||||
CFG_PROPERTY(bool, scheduleDark, false)
|
CFG_PROPERTY(bool, scheduleDark, false)
|
||||||
CFG_PROPERTY(int, scheduleDarkEnd, 600)
|
CFG_PROPERTY(int, scheduleDarkEnd, 600)
|
||||||
CFG_PROPERTY(int, scheduleDarkStart, 1140)
|
CFG_PROPERTY(int, scheduleDarkStart, 1140)
|
||||||
|
|||||||
@@ -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")
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
#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)};
|
||||||
|
}
|
||||||
|
|
||||||
|
inline float easeInOutCubic(float x) {
|
||||||
|
return x < 0.5f ? 4.f * x * x * x
|
||||||
|
: 1.f - std::pow(-2.f * x + 2.f, 3.f) / 2.f;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ZShell::services
|
||||||
@@ -0,0 +1,457 @@
|
|||||||
|
#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::easeInOutCubic;
|
||||||
|
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);
|
||||||
|
const float e = easeInOutCubic(t);
|
||||||
|
|
||||||
|
for (size_t i = 0; i < 3; ++i)
|
||||||
|
g_currentGain[i] =
|
||||||
|
g_transition.fromGain[i] +
|
||||||
|
(g_transition.toGain[i] - g_transition.fromGain[i]) * e;
|
||||||
|
|
||||||
|
if (g_uniformMode) {
|
||||||
|
setUniformGain(g_currentGain);
|
||||||
|
} else {
|
||||||
|
const int step = static_cast<int>(e * 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() {}
|
||||||
@@ -9,7 +9,7 @@ qml_module(ZShell-services
|
|||||||
cavaprovider.hpp cavaprovider.cpp
|
cavaprovider.hpp cavaprovider.cpp
|
||||||
desktopmodel.hpp desktopmodel.cpp
|
desktopmodel.hpp desktopmodel.cpp
|
||||||
desktopstatemanager.hpp desktopstatemanager.cpp
|
desktopstatemanager.hpp desktopstatemanager.cpp
|
||||||
hyprsunsetmanager.hpp hyprsunsetmanager.cpp
|
nightlightmanager.hpp nightlightmanager.cpp
|
||||||
tickingservice.hpp tickingservice.cpp
|
tickingservice.hpp tickingservice.cpp
|
||||||
sensorslib.hpp sensorslib.cpp
|
sensorslib.hpp sensorslib.cpp
|
||||||
usagefmt.hpp usagefmt.cpp
|
usagefmt.hpp usagefmt.cpp
|
||||||
@@ -25,4 +25,4 @@ qml_module(ZShell-services
|
|||||||
PkgConfig::Aubio
|
PkgConfig::Aubio
|
||||||
PkgConfig::Cava
|
PkgConfig::Cava
|
||||||
Sensors::Sensors
|
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
|
||||||
Reference in New Issue
Block a user