Skip to content

Commit

Permalink
core: Add IPC Support (#18)
Browse files Browse the repository at this point in the history
* Refactor in preparation for IPC

* Add IPC support

* `clang-format`

* Resolve comments

* Allow querying value via IPC

* Adjustable gamma clamp

* Resolve review comments
  • Loading branch information
aaronjamt authored Feb 14, 2025
1 parent e8b5cd5 commit 005f0fc
Show file tree
Hide file tree
Showing 6 changed files with 452 additions and 176 deletions.
151 changes: 151 additions & 0 deletions src/Hyprsunset.cpp
Original file line number Diff line number Diff line change
@@ -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<float, 9>{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<float, 9>{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<CCWlRegistry>((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<CCHyprlandCtmControlManagerV1>(
(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<SOutput>(makeShared<CCWlOutput>((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<CIPCSocket>();
g_pIPCSocket->initialize();

while (wl_display_dispatch(state.wlDisplay) != -1) {
std::lock_guard<std::mutex> 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);
}
}
58 changes: 58 additions & 0 deletions src/Hyprsunset.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#include <iostream>
#include <cmath>
#include <algorithm>
#include <sys/signal.h>
#include <wayland-client.h>
#include <vector>
#include "protocols/hyprland-ctm-control-v1.hpp"
#include "protocols/wayland.hpp"

#include "helpers/Log.hpp"

#include "InstanceLock.hpp"

#include "IPCSocket.hpp"
#include <mutex>

#include <hyprutils/math/Mat3x3.hpp>
#include <hyprutils/memory/WeakPtr.hpp>
using namespace Hyprutils::Math;
using namespace Hyprutils::Memory;
#define SP CSharedPointer
#define WP CWeakPointer

struct SOutput {
SP<CCWlOutput> output;
uint32_t id = 0;
void applyCTM(struct SState*);
};

struct SState {
SP<CCWlRegistry> pRegistry;
SP<CCHyprlandCtmControlManagerV1> pCTMMgr;
wl_display* wlDisplay = nullptr;
std::vector<SP<SOutput>> 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<CHyprsunset> g_pHyprsunset;
186 changes: 186 additions & 0 deletions src/IPCSocket.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
#include "IPCSocket.hpp"
#include "Hyprsunset.hpp"

#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/un.h>
#include <unistd.h>
#include <pwd.h>
#include <thread>

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<std::mutex> 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;
}
Loading

0 comments on commit 005f0fc

Please sign in to comment.