Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ pkg_check_modules(
wayland-client
wayland-protocols
hyprutils>=0.2.3
hyprlang
hyprwayland-scanner>=0.4.0)

file(GLOB_RECURSE SRCFILES "src/*.cpp")
Expand Down
27 changes: 27 additions & 0 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@
inputs.systems.follows = "systems";
};

hyprlang = {
url = "github:hyprwm/hyprlang";
inputs.nixpkgs.follows = "nixpkgs";
inputs.systems.follows = "systems";
inputs.hyprutils.follows = "hyprutils";
};

hyprwayland-scanner = {
url = "github:hyprwm/hyprwayland-scanner";
inputs.nixpkgs.follows = "nixpkgs";
Expand Down
2 changes: 2 additions & 0 deletions nix/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
pkg-config,
hyprland-protocols,
hyprutils,
hyprlang,
hyprwayland-scanner,
wayland,
wayland-protocols,
Expand Down Expand Up @@ -35,6 +36,7 @@ in
buildInputs = [
hyprland-protocols
hyprutils
hyprlang
wayland
wayland-protocols
wayland-scanner
Expand Down
1 change: 1 addition & 0 deletions nix/overlays.nix
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ in {

hyprsunset = lib.composeManyExtensions [
inputs.hyprland-protocols.overlays.default
inputs.hyprlang.overlays.default
inputs.hyprutils.overlays.default
inputs.hyprwayland-scanner.overlays.default
(final: prev: {
Expand Down
99 changes: 99 additions & 0 deletions src/ConfigManager.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
#include "ConfigManager.hpp"
#include <cstdlib>
#include <hyprlang.hpp>
#include <hyprutils/path/Path.hpp>
#include <string>
#include <sys/ucontext.h>
#include "helpers/Log.hpp"

static std::string getMainConfigPath() {
static const auto paths = Hyprutils::Path::findConfig("hyprsunset");
if (paths.first.has_value())
return paths.first.value();
else
throw std::runtime_error("Could not find config in HOME, XDG_CONFIG_HOME, XDG_CONFIG_DIRS or /etc/hypr.");
}

CConfigManager::CConfigManager(std::string configPath) :
m_config(configPath.empty() ? getMainConfigPath().c_str() : configPath.c_str(), Hyprlang::SConfigOptions{.throwAllErrors = true, .allowMissingConfig = configPath.empty()}) {
currentConfigPath = configPath.empty() ? getMainConfigPath() : configPath;
}

void CConfigManager::init() {
m_config.addConfigValue("max-gamma", Hyprlang::INT{100});

m_config.addSpecialCategory("profile", Hyprlang::SSpecialCategoryOptions{.key = nullptr, .anonymousKeyBased = true});
m_config.addSpecialConfigValue("profile", "time", Hyprlang::STRING{"00:00"});
m_config.addSpecialConfigValue("profile", "temperature", Hyprlang::INT{6500});
m_config.addSpecialConfigValue("profile", "gamma", Hyprlang::FLOAT{1.0f});
m_config.addSpecialConfigValue("profile", "identity", Hyprlang::INT{0});

m_config.commence();

auto result = m_config.parse();

if (result.error)
Debug::log(ERR, "Config has errors:\n{}\nProceeding ignoring faulty entries", result.getError());
}

std::vector<SSunsetProfile> CConfigManager::getSunsetProfiles() {
std::vector<SSunsetProfile> result;

auto keys = m_config.listKeysForSpecialCategory("profile");
result.reserve(keys.size());

for (auto& key : keys) {
std::string time;
unsigned long temperature;
float gamma;
bool identity;

try {
time = std::any_cast<Hyprlang::STRING>(m_config.getSpecialConfigValue("profile", "time", key.c_str()));
temperature = std::any_cast<Hyprlang::INT>(m_config.getSpecialConfigValue("profile", "temperature", key.c_str()));
gamma = std::any_cast<Hyprlang::FLOAT>(m_config.getSpecialConfigValue("profile", "gamma", key.c_str()));
identity = std::any_cast<Hyprlang::INT>(m_config.getSpecialConfigValue("profile", "identity", key.c_str()));
} catch (const std::bad_any_cast& e) {
RASSERT(false, "Failed to construct Profile: {}", e.what()); //
} catch (const std::out_of_range& e) {
RASSERT(false, "Missing property for Profile: {}", e.what()); //
}

int separator = time.find_first_of(':');

if (separator == -1) {
RASSERT(false, "Invalid time format for profile {}", key);
}

int hour, minute;
try {
hour = std::stoi(time.substr(0, separator));
minute = std::stoi(time.substr(separator + 1).c_str());
} catch (const std::exception& e) {
Debug::log(ERR, "Invalid time format: {}, skipping this profile", time);
continue;
}

// clang-format off
result.push_back(SSunsetProfile{
.time = {
.hour = std::chrono::hours(hour),
.minute = std::chrono::minutes(minute),
},
.temperature = temperature,
.gamma = gamma,
.identity = identity,
});
// clang-format on
}

return result;
}

float CConfigManager::getMaxGamma() {
try {
return std::any_cast<Hyprlang::INT>(m_config.getConfigValue("max-gamma")) / 100.f;
} catch (const std::bad_any_cast& e) {
RASSERT(false, "Failed to construct max-gamma: {}", e.what()); //
}
}
22 changes: 22 additions & 0 deletions src/ConfigManager.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#pragma once

#include "Hyprsunset.hpp"
#include <hyprlang.hpp>
#include <vector>

class CConfigManager {
public:
CConfigManager(std::string configPath);

std::vector<SSunsetProfile> getSunsetProfiles();
float getMaxGamma();

void init();

private:
Hyprlang::CConfig m_config;

std::string currentConfigPath;
};

inline UP<CConfigManager> g_pConfigManager;
104 changes: 97 additions & 7 deletions src/Hyprsunset.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
#include "Hyprsunset.hpp"
#include "ConfigManager.hpp"
#include "helpers/Log.hpp"
#include "IPCSocket.hpp"

#include <thread>
#include <chrono>

// kindly borrowed from https://tannerhelland.com/2012/09/18/convert-temperature-rgb-algorithm-code.html
static Mat3x3 matrixForKelvin(unsigned long long temp) {
Expand Down Expand Up @@ -132,6 +137,8 @@ int CHyprsunset::init() {

commitCTMs();

schedule();

state.initialized = true;

g_pIPCSocket = std::make_unique<CIPCSocket>();
Expand All @@ -147,15 +154,98 @@ int CHyprsunset::init() {

void CHyprsunset::tick() {
if (g_pIPCSocket && g_pIPCSocket->mainThreadParseRequest()) {
// Reload
calculateMatrix();
reload();
}
}

void CHyprsunset::loadCurrentProfile() {
profiles = g_pConfigManager->getSunsetProfiles();

Debug::log(NONE, "┣ Loaded {} profiles, applying the current one", profiles.size());

std::sort(profiles.begin(), profiles.end(), [](const auto& a, const auto& b) {
if (a.time.hour < b.time.hour)
return true;
else if (a.time.hour > b.time.hour)
return false;
else
return a.time.minute < b.time.minute;
});

int current = g_pHyprsunset->currentProfile();

for (auto& o : state.outputs) {
o->applyCTM(&state);
if (current == -1)
return;

SSunsetProfile profile = g_pHyprsunset->profiles[current];
KELVIN = profile.temperature;
GAMMA = profile.gamma;
identity = profile.identity;
MAX_GAMMA = g_pConfigManager->getMaxGamma();
}

int CHyprsunset::currentProfile() {
if (profiles.empty())
return -1;
else if (profiles.size() == 1)
return 0;

auto now = std::chrono::zoned_time(std::chrono::current_zone(), std::chrono::system_clock::now()).get_local_time();

for (size_t i = 0; i < profiles.size(); ++i) {
const auto& p = profiles[i];

auto time = std::chrono::floor<std::chrono::days>(now) + p.time.hour + p.time.minute;

if (time >= now) {
if (i == 0)
return profiles.size() - 1;
return i - 1;
}
}

commitCTMs();
return 0;
}

void CHyprsunset::schedule() {
std::thread([&]() {
while (true) {
int current = currentProfile();
if (current == -1)
break;

SSunsetProfile nextSettings = (size_t)current == profiles.size() - 1 ? profiles[0] : profiles[current + 1];

auto now = std::chrono::zoned_time(std::chrono::current_zone(), std::chrono::system_clock::now()).get_local_time();
auto time = std::chrono::floor<std::chrono::days>(now) + nextSettings.time.hour + nextSettings.time.minute;

if (now >= time) {
time += std::chrono::days(1);
}

auto system_time = std::chrono::zoned_time{std::chrono::current_zone(), time}.get_sys_time();

std::this_thread::sleep_until(system_time);

KELVIN = nextSettings.temperature;
GAMMA = nextSettings.gamma;
identity = nextSettings.identity;

wl_display_flush(state.wlDisplay);
Debug::log(NONE, "┣ Switched to new profile at: {}", time);

reload();
};
}).detach();
}

void CHyprsunset::reload() {
calculateMatrix();

for (auto& o : state.outputs) {
o->applyCTM(&state);
}

commitCTMs();

wl_display_flush(state.wlDisplay);
}
26 changes: 19 additions & 7 deletions src/Hyprsunset.hpp
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
#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 "IPCSocket.hpp"
#include <mutex>

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

Expand All @@ -34,6 +30,17 @@ struct SState {
Mat3x3 ctm;
};

struct SSunsetProfile {
struct {
std::chrono::hours hour;
std::chrono::minutes minute;
} time;

unsigned long temperature = 6000;
float gamma = 1.0f;
bool identity = false;
};

class CHyprsunset {
public:
float MAX_GAMMA = 1.0f; // default
Expand All @@ -44,12 +51,17 @@ class CHyprsunset {
std::mutex m_mtTickMutex;

int calculateMatrix();
int applySettings();
int init();
void tick();
void loadCurrentProfile();

private:
static void commitCTMs();
static void commitCTMs();
void reload();
void schedule();
int currentProfile();

std::vector<SSunsetProfile> profiles;
};

inline std::unique_ptr<CHyprsunset> g_pHyprsunset;
Loading