From 005f0fcfdcf2d335816d1922d5730602920d323b Mon Sep 17 00:00:00 2001 From: Aaron Tulino <13600347+aaronjamt@users.noreply.github.com> Date: Fri, 14 Feb 2025 14:43:14 -0700 Subject: [PATCH] core: Add IPC Support (#18) * Refactor in preparation for IPC * Add IPC support * `clang-format` * Resolve comments * Allow querying value via IPC * Adjustable gamma clamp * Resolve review comments --- src/Hyprsunset.cpp | 151 ++++++++++++++++++++++++++++++++++++ src/Hyprsunset.hpp | 58 ++++++++++++++ src/IPCSocket.cpp | 186 ++++++++++++++++++++++++++++++++++++++++++++ src/IPCSocket.hpp | 22 ++++++ src/main.cpp | 188 ++++++--------------------------------------- src/meson.build | 23 +++--- 6 files changed, 452 insertions(+), 176 deletions(-) create mode 100644 src/Hyprsunset.cpp create mode 100644 src/Hyprsunset.hpp create mode 100644 src/IPCSocket.cpp create mode 100644 src/IPCSocket.hpp diff --git a/src/Hyprsunset.cpp b/src/Hyprsunset.cpp new file mode 100644 index 0000000..9f54f87 --- /dev/null +++ b/src/Hyprsunset.cpp @@ -0,0 +1,151 @@ +#include "Hyprsunset.hpp" + +// kindly borrowed from https://tannerhelland.com/2012/09/18/convert-temperature-rgb-algorithm-code.html +static Mat3x3 matrixForKelvin(unsigned long long temp) { + float r = 1.F, g = 1.F, b = 1.F; + + temp /= 100; + + if (temp <= 66) { + r = 255; + g = std::clamp(99.4708025861 * std::log(temp) - 161.1195681661, 0.0, 255.0); + if (temp <= 19) + b = 0; + else + b = std::clamp(std::log(temp - 10) * 138.5177312231 - 305.0447927307, 0.0, 255.0); + } else { + r = std::clamp(329.698727446 * (std::pow(temp - 60, -0.1332047592)), 0.0, 255.0); + g = std::clamp(288.1221695283 * (std::pow(temp - 60, -0.0755148492)), 0.0, 255.0); + b = 255; + } + + return std::array{r / 255.F, 0, 0, 0, g / 255.F, 0, 0, 0, b / 255.F}; +} + +static void sigHandler(int sig) { + if (g_pHyprsunset->state.pCTMMgr) // reset the CTM state... + g_pHyprsunset->state.pCTMMgr.reset(); + + Debug::log(NONE, "┣ Exiting on user interrupt\n╹"); + + exit(0); +} + +void SOutput::applyCTM(struct SState* state) { + auto arr = state->ctm.getMatrix(); + state->pCTMMgr->sendSetCtmForOutput(output->resource(), wl_fixed_from_double(arr[0]), wl_fixed_from_double(arr[1]), wl_fixed_from_double(arr[2]), wl_fixed_from_double(arr[3]), + wl_fixed_from_double(arr[4]), wl_fixed_from_double(arr[5]), wl_fixed_from_double(arr[6]), wl_fixed_from_double(arr[7]), + wl_fixed_from_double(arr[8])); +} + +void CHyprsunset::commitCTMs() { + g_pHyprsunset->state.pCTMMgr->sendCommit(); +} + +int CHyprsunset::calculateMatrix() { + if (KELVIN < 1000 || KELVIN > 20000) { + Debug::log(NONE, "✖ Temperature invalid: {}. The temperature has to be between 1000 and 20000K", KELVIN); + return 0; + } + + if (GAMMA < 0 || GAMMA > MAX_GAMMA) { + Debug::log(NONE, "✖ Gamma invalid: {}%. The gamma has to be between 0% and {}%", GAMMA * 100, MAX_GAMMA * 100); + return 0; + } + + if (!identity) + Debug::log(NONE, "┣ Setting the temperature to {}K{}\n┃", KELVIN, kelvinSet ? "" : " (default)"); + else + Debug::log(NONE, "┣ Resetting the matrix (--identity passed)\n┃", KELVIN, kelvinSet ? "" : " (default)"); + + // calculate the matrix + state.ctm = identity ? Mat3x3::identity() : matrixForKelvin(KELVIN); + state.ctm.multiply(std::array{GAMMA, 0, 0, 0, GAMMA, 0, 0, 0, GAMMA}); + + Debug::log(NONE, "┣ Calculated the CTM to be {}\n┃", state.ctm.toString()); + + return 1; +} + +int CHyprsunset::init() { + // connect to the wayland server + if (const auto SERVER = getenv("XDG_CURRENT_DESKTOP"); SERVER) + Debug::log(NONE, "┣ Running on {}", SERVER); + + state.wlDisplay = wl_display_connect(nullptr); + + if (!state.wlDisplay) { + Debug::log(NONE, "✖ Couldn't connect to a wayland compositor", KELVIN); + return 0; + } + + signal(SIGINT, sigHandler); + signal(SIGTERM, sigHandler); + + state.pRegistry = makeShared((wl_proxy*)wl_display_get_registry(state.wlDisplay)); + state.pRegistry->setGlobal([this](CCWlRegistry* r, uint32_t name, const char* interface, uint32_t version) { + const std::string IFACE = interface; + + if (IFACE == hyprland_ctm_control_manager_v1_interface.name) { + Debug::log(NONE, "┣ Found hyprland-ctm-control-v1 supported with version {}, binding to v1", version); + state.pCTMMgr = makeShared( + (wl_proxy*)wl_registry_bind((wl_registry*)state.pRegistry->resource(), name, &hyprland_ctm_control_manager_v1_interface, 1)); + } else if (IFACE == wl_output_interface.name) { + + if (std::find_if(state.outputs.begin(), state.outputs.end(), [name](const auto& el) { return el->id == name; }) != state.outputs.end()) + return; + + Debug::log(NONE, "┣ Found new output with ID {}, binding", name); + auto o = state.outputs.emplace_back( + makeShared(makeShared((wl_proxy*)wl_registry_bind((wl_registry*)state.pRegistry->resource(), name, &wl_output_interface, 1)), name)); + + if (state.initialized) { + Debug::log(NONE, "┣ already initialized, applying CTM instantly", name); + o->applyCTM(&state); + commitCTMs(); + } + } + }); + + wl_display_roundtrip(state.wlDisplay); + + if (!state.pCTMMgr) { + Debug::log(NONE, "✖ Compositor doesn't support hyprland-ctm-control-v1, are you running on Hyprland?", KELVIN); + return 0; + } + + Debug::log(NONE, "┣ Found {} outputs, applying CTMs", state.outputs.size()); + + for (auto& o : state.outputs) { + o->applyCTM(&state); + } + + commitCTMs(); + + state.initialized = true; + + g_pIPCSocket = std::make_unique(); + g_pIPCSocket->initialize(); + + while (wl_display_dispatch(state.wlDisplay) != -1) { + std::lock_guard lg(m_mtTickMutex); + tick(); + } + + return 1; +} + +void CHyprsunset::tick() { + if (g_pIPCSocket && g_pIPCSocket->mainThreadParseRequest()) { + // Reload + calculateMatrix(); + + for (auto& o : state.outputs) { + o->applyCTM(&state); + } + + commitCTMs(); + + wl_display_flush(state.wlDisplay); + } +} diff --git a/src/Hyprsunset.hpp b/src/Hyprsunset.hpp new file mode 100644 index 0000000..5ca4ddd --- /dev/null +++ b/src/Hyprsunset.hpp @@ -0,0 +1,58 @@ +#include +#include +#include +#include +#include +#include +#include "protocols/hyprland-ctm-control-v1.hpp" +#include "protocols/wayland.hpp" + +#include "helpers/Log.hpp" + +#include "InstanceLock.hpp" + +#include "IPCSocket.hpp" +#include + +#include +#include +using namespace Hyprutils::Math; +using namespace Hyprutils::Memory; +#define SP CSharedPointer +#define WP CWeakPointer + +struct SOutput { + SP output; + uint32_t id = 0; + void applyCTM(struct SState*); +}; + +struct SState { + SP pRegistry; + SP pCTMMgr; + wl_display* wlDisplay = nullptr; + std::vector> outputs; + bool initialized = false; + Mat3x3 ctm; + CInstanceLock instLock; +}; + +class CHyprsunset { + public: + float MAX_GAMMA = 1.0f; // default + float GAMMA = 1.0f; // default + unsigned long long KELVIN = 6000; // default + bool kelvinSet = false, identity = false; + SState state; + std::mutex m_mtTickMutex; + + int calculateMatrix(); + int applySettings(); + int init(); + void tick(); + + private: + static void commitCTMs(); +}; + +inline std::unique_ptr g_pHyprsunset; diff --git a/src/IPCSocket.cpp b/src/IPCSocket.cpp new file mode 100644 index 0000000..5b64d5c --- /dev/null +++ b/src/IPCSocket.cpp @@ -0,0 +1,186 @@ +#include "IPCSocket.hpp" +#include "Hyprsunset.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +void CIPCSocket::initialize() { + std::thread([&]() { + const auto SOCKET = socket(AF_UNIX, SOCK_STREAM, 0); + + if (SOCKET < 0) { + Debug::log(ERR, "Couldn't start the hyprsunset Socket. (1) IPC will not work."); + return; + } + + sockaddr_un SERVERADDRESS = {.sun_family = AF_UNIX}; + + const auto HISenv = getenv("HYPRLAND_INSTANCE_SIGNATURE"); + const auto RUNTIMEdir = getenv("XDG_RUNTIME_DIR"); + const std::string USERID = std::to_string(getpwuid(getuid())->pw_uid); + + const auto USERDIR = RUNTIMEdir ? RUNTIMEdir + std::string{"/hypr/"} : "/run/user/" + USERID + "/hypr/"; + + std::string socketPath = HISenv ? USERDIR + std::string(HISenv) + "/.hyprsunset.sock" : USERDIR + ".hyprsunset.sock"; + + if (!HISenv) + mkdir(USERDIR.c_str(), S_IRWXU); + + unlink(socketPath.c_str()); + + strcpy(SERVERADDRESS.sun_path, socketPath.c_str()); + + bind(SOCKET, (sockaddr*)&SERVERADDRESS, SUN_LEN(&SERVERADDRESS)); + + // 10 max queued. + listen(SOCKET, 10); + + sockaddr_in clientAddress = {}; + socklen_t clientSize = sizeof(clientAddress); + + char readBuffer[1024] = {0}; + + Debug::log(LOG, "hyprsunset socket started at {} (fd: {})", socketPath, SOCKET); + while (1) { + const auto ACCEPTEDCONNECTION = accept(SOCKET, (sockaddr*)&clientAddress, &clientSize); + if (ACCEPTEDCONNECTION < 0) { + Debug::log(ERR, "Couldn't listen on the hyprsunset Socket. (3) IPC will not work."); + break; + } else { + do { + Debug::log(LOG, "Accepted incoming socket connection request on fd {}", ACCEPTEDCONNECTION); + std::lock_guard lg(g_pHyprsunset->m_mtTickMutex); + + auto messageSize = read(ACCEPTEDCONNECTION, readBuffer, 1024); + readBuffer[messageSize == 1024 ? 1023 : messageSize] = '\0'; + if (messageSize == 0) + break; + std::string request(readBuffer); + + m_szRequest = request; + m_bRequestReady = true; + + g_pHyprsunset->tick(); + while (!m_bReplyReady) { // wait for Hyprsunset to finish processing the request + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + write(ACCEPTEDCONNECTION, m_szReply.c_str(), m_szReply.length()); + m_bReplyReady = false; + m_szReply = ""; + + } while (1); + Debug::log(LOG, "Closing Accepted Connection"); + close(ACCEPTEDCONNECTION); + } + } + + close(SOCKET); + }).detach(); +} + +bool CIPCSocket::mainThreadParseRequest() { + + if (!m_bRequestReady) + return false; + + std::string copy = m_szRequest; + + if (copy == "") + return false; + + // now we can work on the copy + + Debug::log(LOG, "Received a request: {}", copy); + + // set default reply + m_szReply = "ok"; + m_bReplyReady = true; + m_bRequestReady = false; + + // config commands + if (copy.find("gamma") == 0) { + int spaceSeparator = copy.find_first_of(' '); + if (spaceSeparator == -1) { + m_szReply = std::to_string(g_pHyprsunset->GAMMA * 100); + return false; + } + + std::string args = copy.substr(spaceSeparator + 1); + float gamma = g_pHyprsunset->GAMMA * 100; + float maxGamma = g_pHyprsunset->MAX_GAMMA * 100; + if (args[0] == '+' || args[0] == '-') { + try { + if (args[0] == '-') + gamma -= std::stof(args.substr(1)); + else + gamma += std::stof(args.substr(1)); + } catch (std::exception& e) { + m_szReply = "Invalid gamma value (should be in range 0-" + std::to_string(maxGamma) + "%)"; + return false; + } + + gamma = std::clamp(gamma, 0.0f, maxGamma); + } else + gamma = std::stof(args); + + if (gamma < 0 || gamma > maxGamma) { + m_szReply = "Invalid gamma value (should be in range 0-" + std::to_string(maxGamma) + "%)"; + return false; + } + + g_pHyprsunset->GAMMA = gamma / 100; + return true; + } + + if (copy.find("temperature") == 0) { + int spaceSeparator = copy.find_first_of(' '); + if (spaceSeparator == -1) { + m_szReply = std::to_string(g_pHyprsunset->KELVIN); + return false; + } + + std::string args = copy.substr(spaceSeparator + 1); + unsigned long long kelvin = g_pHyprsunset->KELVIN; + if (args[0] == '+' || args[0] == '-') { + try { + if (args[0] == '-') + kelvin -= std::stoull(args.substr(1)); + else + kelvin += std::stoull(args.substr(1)); + } catch (std::exception& e) { + m_szReply = "Invalid temperature (should be in range 1000-20000)"; + return false; + } + + kelvin = std::clamp(kelvin, 1000ull, 20000ull); + } else + kelvin = std::stoull(args); + + if (kelvin < 1000 || kelvin > 20000) { + m_szReply = "Invalid temperature (should be in range 1000-20000)"; + return false; + } + + g_pHyprsunset->KELVIN = kelvin; + g_pHyprsunset->identity = false; + return true; + } + + if (copy.find("identity") == 0) { + g_pHyprsunset->identity = true; + return true; + } + + m_szReply = "invalid command"; + return false; +} diff --git a/src/IPCSocket.hpp b/src/IPCSocket.hpp new file mode 100644 index 0000000..d993e1f --- /dev/null +++ b/src/IPCSocket.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include +#include +#include + +class CIPCSocket { + public: + void initialize(); + + bool mainThreadParseRequest(); + + private: + std::mutex m_mtRequestMutex; + std::string m_szRequest = ""; + std::string m_szReply = ""; + + bool m_bRequestReady = false; + bool m_bReplyReady = false; +}; + +inline std::unique_ptr g_pIPCSocket; diff --git a/src/main.cpp b/src/main.cpp index 183e10f..2ad5c52 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,83 +1,9 @@ #include -#include -#include -#include -#include -#include -#include "protocols/hyprland-ctm-control-v1.hpp" -#include "protocols/wayland.hpp" - -#include "helpers/Log.hpp" - -#include "InstanceLock.hpp" - -#include -#include -using namespace Hyprutils::Math; -using namespace Hyprutils::Memory; -#define SP CSharedPointer -#define WP CWeakPointer - -// kindly borrowed from https://tannerhelland.com/2012/09/18/convert-temperature-rgb-algorithm-code.html -static Mat3x3 matrixForKelvin(unsigned long long temp) { - float r = 1.F, g = 1.F, b = 1.F; - - temp /= 100; - - if (temp <= 66) { - r = 255; - g = std::clamp(99.4708025861 * std::log(temp) - 161.1195681661, 0.0, 255.0); - if (temp <= 19) - b = 0; - else - b = std::clamp(std::log(temp - 10) * 138.5177312231 - 305.0447927307, 0.0, 255.0); - } else { - r = std::clamp(329.698727446 * (std::pow(temp - 60, -0.1332047592)), 0.0, 255.0); - g = std::clamp(288.1221695283 * (std::pow(temp - 60, -0.0755148492)), 0.0, 255.0); - b = 255; - } - - return std::array{r / 255.F, 0, 0, 0, g / 255.F, 0, 0, 0, b / 255.F}; -} - -struct SOutput { - SP output; - uint32_t id = 0; - void applyCTM(); -}; - -struct { - SP pRegistry; - SP pCTMMgr; - wl_display* wlDisplay = nullptr; - std::vector> outputs; - bool initialized = false; - Mat3x3 ctm; - CInstanceLock instLock; -} state; - -void sigHandler(int sig) { - if (state.pCTMMgr) // reset the CTM state... - state.pCTMMgr.reset(); - - Debug::log(NONE, "┣ Exiting on user interrupt\n╹"); - - exit(0); -} - -void SOutput::applyCTM() { - auto arr = state.ctm.getMatrix(); - state.pCTMMgr->sendSetCtmForOutput(output->resource(), wl_fixed_from_double(arr[0]), wl_fixed_from_double(arr[1]), wl_fixed_from_double(arr[2]), wl_fixed_from_double(arr[3]), - wl_fixed_from_double(arr[4]), wl_fixed_from_double(arr[5]), wl_fixed_from_double(arr[6]), wl_fixed_from_double(arr[7]), - wl_fixed_from_double(arr[8])); -} - -static void commitCTMs() { - state.pCTMMgr->sendCommit(); -} +#include "Hyprsunset.hpp" static void printHelp() { Debug::log(NONE, "┣ --gamma -g → Set the display gamma (default 100%)"); + Debug::log(NONE, "┣ --gamma_max → Set the maximum display gamma (default 100%, maximum 200%)"); Debug::log(NONE, "┣ --temperature -t → Set the temperature in K (default 6000)"); Debug::log(NONE, "┣ --identity -i → Use the identity matrix (no color change)"); Debug::log(NONE, "┣ --help -h → Print this info"); @@ -87,15 +13,13 @@ static void printHelp() { int main(int argc, char** argv, char** envp) { Debug::log(NONE, "┏ hyprsunset v{} ━━╸\n┃", HYPRSUNSET_VERSION); - if (!state.instLock.isOnlyInstance) { + g_pHyprsunset = std::make_unique(); + + if (!g_pHyprsunset->state.instLock.isOnlyInstance) { Debug::log(NONE, "✖ Another instance of hyprsunset is running"); return 1; } - float GAMMA = 1.0f; // default - unsigned long long KELVIN = 6000; // default - bool kelvinSet = false, identity = false; - for (int i = 1; i < argc; ++i) { if (argv[i] == std::string{"-t"} || argv[i] == std::string{"--temperature"}) { if (i + 1 >= argc) { @@ -104,8 +28,8 @@ int main(int argc, char** argv, char** envp) { } try { - KELVIN = std::stoull(argv[i + 1]); - kelvinSet = true; + g_pHyprsunset->KELVIN = std::stoull(argv[i + 1]); + g_pHyprsunset->kelvinSet = true; } catch (std::exception& e) { Debug::log(NONE, "✖ Temperature {} is not valid", argv[i + 1]); return 1; @@ -119,15 +43,29 @@ int main(int argc, char** argv, char** envp) { } try { - GAMMA = std::stof(argv[i + 1]) / 100; + g_pHyprsunset->GAMMA = std::stof(argv[i + 1]) / 100; } catch (std::exception& e) { Debug::log(NONE, "✖ Gamma {} is not valid", argv[i + 1]); return 1; } + ++i; + } else if (argv[i] == std::string{"--gamma_max"}) { + if (i + 1 >= argc) { + Debug::log(NONE, "✖ No gamma provided for {}", argv[i]); + return 1; + } + + try { + g_pHyprsunset->MAX_GAMMA = std::stof(argv[i + 1]) / 100; + } catch (std::exception& e) { + Debug::log(NONE, "✖ Maximum gamma {} is not valid", argv[i + 1]); + return 1; + } + ++i; } else if (argv[i] == std::string{"-i"} || argv[i] == std::string{"--identity"}) { - identity = true; + g_pHyprsunset->identity = true; } else if (argv[i] == std::string{"-h"} || argv[i] == std::string{"--help"}) { printHelp(); return 0; @@ -138,86 +76,10 @@ int main(int argc, char** argv, char** envp) { } } - if (KELVIN < 1000 || KELVIN > 20000) { - Debug::log(NONE, "✖ Temperature invalid: {}. The temperature has to be between 1000 and 20000K", KELVIN); - return 1; - } - - if (GAMMA < 0 || GAMMA > 2) { - Debug::log(NONE, "✖ Gamma invalid: {}%. The gamma has to be between 0% and 200%", GAMMA * 100); + if (!g_pHyprsunset->calculateMatrix()) return 1; - } - - if (!identity) - Debug::log(NONE, "┣ Setting the temperature to {}K{}\n┃", KELVIN, kelvinSet ? "" : " (default)"); - else - Debug::log(NONE, "┣ Resetting the matrix (--identity passed)\n┃", KELVIN, kelvinSet ? "" : " (default)"); - - // calculate the matrix - state.ctm = identity ? Mat3x3::identity() : matrixForKelvin(KELVIN); - state.ctm.multiply(std::array{GAMMA, 0, 0, 0, GAMMA, 0, 0, 0, GAMMA}); - - Debug::log(NONE, "┣ Calculated the CTM to be {}\n┃", state.ctm.toString()); - - // connect to the wayland server - if (const auto SERVER = getenv("XDG_CURRENT_DESKTOP"); SERVER) - Debug::log(NONE, "┣ Running on {}", SERVER); - - state.wlDisplay = wl_display_connect(nullptr); - - if (!state.wlDisplay) { - Debug::log(NONE, "✖ Couldn't connect to a wayland compositor", KELVIN); + if (!g_pHyprsunset->init()) return 1; - } - - signal(SIGINT, sigHandler); - signal(SIGTERM, sigHandler); - - state.pRegistry = makeShared((wl_proxy*)wl_display_get_registry(state.wlDisplay)); - state.pRegistry->setGlobal([](CCWlRegistry* r, uint32_t name, const char* interface, uint32_t version) { - const std::string IFACE = interface; - - if (IFACE == hyprland_ctm_control_manager_v1_interface.name) { - Debug::log(NONE, "┣ Found hyprland-ctm-control-v1 supported with version {}, binding to v1", version); - state.pCTMMgr = makeShared( - (wl_proxy*)wl_registry_bind((wl_registry*)state.pRegistry->resource(), name, &hyprland_ctm_control_manager_v1_interface, 1)); - } else if (IFACE == wl_output_interface.name) { - - if (std::find_if(state.outputs.begin(), state.outputs.end(), [name](const auto& el) { return el->id == name; }) != state.outputs.end()) - return; - - Debug::log(NONE, "┣ Found new output with ID {}, binding", name); - auto o = state.outputs.emplace_back( - makeShared(makeShared((wl_proxy*)wl_registry_bind((wl_registry*)state.pRegistry->resource(), name, &wl_output_interface, 1)), name)); - - if (state.initialized) { - Debug::log(NONE, "┣ already initialized, applying CTM instantly", name); - o->applyCTM(); - commitCTMs(); - } - } - }); - - wl_display_roundtrip(state.wlDisplay); - - if (!state.pCTMMgr) { - Debug::log(NONE, "✖ Compositor doesn't support hyprland-ctm-control-v1, are you running on Hyprland?", KELVIN); - return 1; - } - - Debug::log(NONE, "┣ Found {} outputs, applying CTMs", state.outputs.size()); - - for (auto& o : state.outputs) { - o->applyCTM(); - } - - commitCTMs(); - - state.initialized = true; - - while (wl_display_dispatch(state.wlDisplay) != -1) { - ; - } return 0; } diff --git a/src/meson.build b/src/meson.build index a2d15f1..1760532 100644 --- a/src/meson.build +++ b/src/meson.build @@ -1,14 +1,11 @@ -globber = run_command('sh', '-c', 'find . -name "*.cpp" | sort', check: true) -src = globber.stdout().strip().split('\n') +globber = run_command('sh', '-c', 'find . -name "*.cpp" | sort', check : true) src = globber.stdout().strip().split('\n') -executable( - 'hyprsunset', - src, - dependencies: [ - dependency('wayland-client'), - dependency('wayland-cursor'), - dependency('hyprutils', version: '>= 0.2.3'), - dependency('threads'), - ], - install: true, -) + executable('hyprsunset', src, + dependencies : + [ + dependency('wayland-client'), + dependency('wayland-cursor'), + dependency('hyprutils', version : '>= 0.2.3'), + dependency('threads'), + ], + install : true, )