From e448d731f8d3180460d54817af9f5751a0739942 Mon Sep 17 00:00:00 2001 From: Jaylyn Barbee <51131738+Jaylyn-Barbee@users.noreply.github.com> Date: Wed, 5 Nov 2025 13:34:26 -0500 Subject: [PATCH] [Light Switch] Refactor + cleaner behavior (#43159) ## Summary of the Pull Request This PR also includes a refactor to the light switch service. The refactor will help me make cleaner updates in the future and makes the "state" easier to track through the service. ## Validation Completed the following steps as testing for normal behavior and edge cases: 1. Start up - Check defaults (delete settings.json files) - Ensure PowerToys properly starts/does not start the module - Ensure turning the module on and off starts/terminates the service - Ensure the settings in the settings file are reflected in the front end 2. Manual Override Activation - Ensure that pressing the shortcut key triggers an update - Ensure that pressing the shortcut key triggers a block in the schedule (Should see logs Skipping schedule due to manual override) - Ensure that changing windows settings triggers a manual override (at the next minute) - Ensure in both scenarios that the schedule is ignored until a boundary is met - Ensure that the schedule resumes following the boundary clearance. 4. New Day Detection / Sun Time Recalculation - Keep service running past midnight (or simulate date change) - Verify the last updated day is updated - Verify new sun times are accurate and set in settings. 5. Coordinates / Mode Change - Ensure that updates occur when the coordinates or mode changes. These updates should reflect the new settings. 6. Schedule Mode OFF - Turn the schedule off, check logs for notice - Ensure the shortcut still works - Ensure the schedule resumes as expected once turned back on. 8. Sleep / Hibernate Resume - Set your schedule to change themes in the next 2 minutes. - Send your machine to hibernate shutdown /h - Wake your machine after your theme should have changed and ensure Light Switch catches itself up - Repeat steps above but with a manual override triggered prior to the theme change.Ensure manual override is flushed and schedule resumes. 9. Stop and Restart behavior - Stop and restart the module. Ensure it behaves as expected. No reset settings, etc. --- .../LightSwitchService/LightSwitchService.cpp | 334 +++++++----------- .../LightSwitchService.vcxproj | 2 - .../LightSwitchService.vcxproj.filters | 6 - .../LightSwitchServiceObserver.cpp | 29 -- .../LightSwitchServiceObserver.h | 16 - .../LightSwitchSettings.cpp | 121 ++++++- .../LightSwitchService/LightSwitchSettings.h | 15 +- 7 files changed, 266 insertions(+), 257 deletions(-) delete mode 100644 src/modules/LightSwitch/LightSwitchService/LightSwitchServiceObserver.cpp delete mode 100644 src/modules/LightSwitch/LightSwitchService/LightSwitchServiceObserver.h diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchService.cpp b/src/modules/LightSwitch/LightSwitchService/LightSwitchService.cpp index fc93c4d387..503a98de29 100644 --- a/src/modules/LightSwitch/LightSwitchService/LightSwitchService.cpp +++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchService.cpp @@ -11,7 +11,6 @@ #include #include #include -#include SERVICE_STATUS g_ServiceStatus = {}; SERVICE_STATUS_HANDLE g_StatusHandle = nullptr; @@ -20,6 +19,7 @@ extern int g_lastUpdatedDay = -1; static ScheduleMode prevMode = ScheduleMode::Off; static std::wstring prevLat, prevLon; static int prevMinutes = -1; +static bool lastOverrideStatus = false; VOID WINAPI ServiceMain(DWORD argc, LPTSTR* argv); VOID WINAPI ServiceCtrlHandler(DWORD dwCtrl); @@ -164,48 +164,8 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam) LightSwitchSettings::instance().InitFileWatcher(); - LightSwitchServiceObserver observer({ SettingId::LightTime, - SettingId::DarkTime, - SettingId::ScheduleMode, - SettingId::Sunrise_Offset, - SettingId::Sunset_Offset }); - HANDLE hManualOverride = OpenEventW(SYNCHRONIZE | EVENT_MODIFY_STATE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE"); - auto applyTheme = [](int nowMinutes, int lightMinutes, int darkMinutes, const auto& settings) { - bool isLightActive = (lightMinutes < darkMinutes) ? (nowMinutes >= lightMinutes && nowMinutes < darkMinutes) : (nowMinutes >= lightMinutes || nowMinutes < darkMinutes); - - bool isSystemCurrentlyLight = GetCurrentSystemTheme(); - bool isAppsCurrentlyLight = GetCurrentAppsTheme(); - - if (isLightActive) - { - if (settings.changeSystem && !isSystemCurrentlyLight) - { - SetSystemTheme(true); - Logger::info(L"[LightSwitchService] Changing system theme to light mode."); - } - if (settings.changeApps && !isAppsCurrentlyLight) - { - SetAppsTheme(true); - Logger::info(L"[LightSwitchService] Changing apps theme to light mode."); - } - } - else - { - if (settings.changeSystem && isSystemCurrentlyLight) - { - SetSystemTheme(false); - Logger::info(L"[LightSwitchService] Changing system theme to dark mode."); - } - if (settings.changeApps && isAppsCurrentlyLight) - { - SetAppsTheme(false); - Logger::info(L"[LightSwitchService] Changing apps theme to dark mode."); - } - } - }; - LightSwitchSettings::instance().LoadSettings(); auto& settings = LightSwitchSettings::instance().settings(); @@ -213,22 +173,22 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam) GetLocalTime(&st); int nowMinutes = st.wHour * 60 + st.wMinute; + // Handle initial theme application if necessary if (settings.scheduleMode != ScheduleMode::Off) { - applyTheme(nowMinutes, - settings.lightTime + settings.sunrise_offset, - settings.darkTime + settings.sunset_offset, - settings); - Logger::trace(L"[LightSwitchService] Initialized g_lastUpdatedDay = {}", g_lastUpdatedDay); + Logger::info(L"[LightSwitchService] Schedule mode is set to {}. Applying theme if necessary.", settings.scheduleMode); + LightSwitchSettings::instance().ApplyThemeIfNecessary(); } else { - Logger::info(L"[LightSwitchService] Schedule mode is OFF - ticker suspended, waiting for manual action or mode change."); + Logger::info(L"[LightSwitchService] Schedule mode is set to Off."); } g_lastUpdatedDay = st.wDay; + Logger::info(L"[LightSwitchService] Initializing g_lastUpdatedDay to {}.", g_lastUpdatedDay); ULONGLONG lastSettingsReload = 0; + // ticker loop for (;;) { HANDLE waits[2] = { g_ServiceStopEvent, hParent }; @@ -237,13 +197,10 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam) const auto& settings = LightSwitchSettings::instance().settings(); - bool scheduleJustEnabled = (prevMode == ScheduleMode::Off && settings.scheduleMode != ScheduleMode::Off); - prevMode = settings.scheduleMode; - - // ─── Handle "Schedule Off" Mode ───────────────────────────────────────────── + // If the mode is set to Off, suspend the scheduler and avoid extra work if (settings.scheduleMode == ScheduleMode::Off) { - Logger::info(L"[LightSwitchService] Schedule mode OFF - suspending scheduler but keeping service alive."); + Logger::info(L"[LightSwitchService] Schedule mode is OFF - suspending scheduler but keeping service alive."); if (!hManualOverride) hManualOverride = OpenEventW(SYNCHRONIZE | EVENT_MODIFY_STATE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE"); @@ -283,7 +240,6 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam) { Logger::trace(L"[LightSwitchService] Settings change event triggered, reloading settings..."); ResetEvent(LightSwitchSettings::instance().GetSettingsChangedEvent()); - LightSwitchSettings::instance().LoadSettings(); const auto& newSettings = LightSwitchSettings::instance().settings(); lastSettingsReload = GetTickCount64(); @@ -298,73 +254,150 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam) continue; } - // ─── Normal Schedule Loop ─────────────────────────────────────────────────── + bool scheduleJustEnabled = (prevMode == ScheduleMode::Off && settings.scheduleMode != ScheduleMode::Off); + prevMode = settings.scheduleMode; + ULONGLONG nowTick = GetTickCount64(); - bool recentSettingsReload = (nowTick - lastSettingsReload < 5000); + bool recentSettingsReload = (nowTick - lastSettingsReload < 2000); - if (g_lastUpdatedDay != -1) + Logger::debug(L"[LightSwitchService] Current g_lastUpdatedDay value = {}.", g_lastUpdatedDay); + + // Manual Override Detection Logic + bool manualOverrideActive = (hManualOverride && WaitForSingleObject(hManualOverride, 0) == WAIT_OBJECT_0); + + if (manualOverrideActive != lastOverrideStatus) { - bool manualOverrideActive = (hManualOverride && WaitForSingleObject(hManualOverride, 0) == WAIT_OBJECT_0); + Logger::debug(L"[LightSwitchService] Manual override active = {}", manualOverrideActive); + lastOverrideStatus = manualOverrideActive; + } - if (settings.scheduleMode != ScheduleMode::Off && !recentSettingsReload && !scheduleJustEnabled) + if (settings.scheduleMode != ScheduleMode::Off && !recentSettingsReload && !scheduleJustEnabled && !manualOverrideActive) + { + bool currentSystemTheme = GetCurrentSystemTheme(); + bool currentAppsTheme = GetCurrentAppsTheme(); + + SYSTEMTIME st; + GetLocalTime(&st); + int nowMinutes = st.wHour * 60 + st.wMinute; + + int lightBoundary = 0; + int darkBoundary = 0; + + if (settings.scheduleMode == ScheduleMode::SunsetToSunrise) { - Logger::debug(L"[LightSwitchService] Checking if manual override is active..."); - bool manualOverrideActive = (hManualOverride && WaitForSingleObject(hManualOverride, 0) == WAIT_OBJECT_0); - Logger::debug(L"[LightSwitchService] Manual override active = {}", manualOverrideActive); - - if (!manualOverrideActive) - { - bool currentSystemTheme = GetCurrentSystemTheme(); - bool currentAppsTheme = GetCurrentAppsTheme(); - - SYSTEMTIME st; - GetLocalTime(&st); - int nowMinutes = st.wHour * 60 + st.wMinute; - - bool shouldBeLight = (settings.lightTime < settings.darkTime) ? (nowMinutes >= settings.lightTime && nowMinutes < settings.darkTime) : (nowMinutes >= settings.lightTime || nowMinutes < settings.darkTime); - - Logger::debug(L"[LightSwitchService] shouldBeLight = {}", shouldBeLight); - - if ((settings.changeSystem && (currentSystemTheme != shouldBeLight)) || - (settings.changeApps && (currentAppsTheme != shouldBeLight))) - { - Logger::debug(L"[LightSwitchService] External theme change detected - enabling manual override"); - - if (!hManualOverride) - { - hManualOverride = OpenEventW(SYNCHRONIZE | EVENT_MODIFY_STATE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE"); - if (!hManualOverride) - hManualOverride = CreateEventW(nullptr, TRUE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE"); - } - - if (hManualOverride) - { - SetEvent(hManualOverride); - Logger::info(L"[LightSwitchService] Detected manual theme change outside of LightSwitch. Triggering manual override."); - skipRest = true; - } - } - } + lightBoundary = (settings.lightTime + settings.sunrise_offset) % 1440; + darkBoundary = (settings.darkTime + settings.sunset_offset) % 1440; } else { - Logger::debug(L"[LightSwitchService] Skipping external-change detection (schedule off, recent reload, or just enabled)."); + lightBoundary = settings.lightTime; + darkBoundary = settings.darkTime; + } + + bool shouldBeLight = (lightBoundary < darkBoundary) ? (nowMinutes >= lightBoundary && nowMinutes < darkBoundary) : (nowMinutes >= lightBoundary || nowMinutes < darkBoundary); + + Logger::debug(L"[LightSwitchService] shouldBeLight = {}", shouldBeLight); + + bool systemMismatch = settings.changeSystem && (currentSystemTheme != shouldBeLight); + bool appsMismatch = settings.changeApps && (currentAppsTheme != shouldBeLight); + + if (systemMismatch || appsMismatch) + { + // Make sure this is not because we crossed a boundary + bool crossedBoundary = false; + if (prevMinutes != -1) + { + if (nowMinutes < prevMinutes) + { + // wrapped around midnight + crossedBoundary = (prevMinutes <= lightBoundary || nowMinutes >= lightBoundary) || + (prevMinutes <= darkBoundary || nowMinutes >= darkBoundary); + } + else + { + crossedBoundary = (prevMinutes < lightBoundary && nowMinutes >= lightBoundary) || + (prevMinutes < darkBoundary && nowMinutes >= darkBoundary); + } + } + + if (crossedBoundary) + { + Logger::info(L"[LightSwitchService] Missed boundary detected. Applying theme instead of triggering manual override."); + LightSwitchSettings::instance().ApplyThemeIfNecessary(); + } + else + { + Logger::info(L"[LightSwitchService] External {} theme change detected, enabling manual override.", + systemMismatch && appsMismatch ? L"system/app" : + systemMismatch ? L"system" : + L"app"); + SetEvent(hManualOverride); + skipRest = true; + } + } + } + else + { + Logger::debug(L"[LightSwitchService] Skipping external-change detection (schedule off, recent reload, or just enabled)."); + } + + if (hManualOverride) + manualOverrideActive = (WaitForSingleObject(hManualOverride, 0) == WAIT_OBJECT_0); + + if (manualOverrideActive) + { + int lightBoundary = (settings.lightTime + settings.sunrise_offset) % 1440; + int darkBoundary = (settings.darkTime + settings.sunset_offset) % 1440; + + SYSTEMTIME st; + GetLocalTime(&st); + nowMinutes = st.wHour * 60 + st.wMinute; + + bool crossedLight = false; + bool crossedDark = false; + + if (prevMinutes != -1) + { + // this means we are in a new day cycle + if (nowMinutes < prevMinutes) + { + crossedLight = (prevMinutes <= lightBoundary || nowMinutes >= lightBoundary); + crossedDark = (prevMinutes <= darkBoundary || nowMinutes >= darkBoundary); + } + else + { + crossedLight = (prevMinutes < lightBoundary && nowMinutes >= lightBoundary); + crossedDark = (prevMinutes < darkBoundary && nowMinutes >= darkBoundary); + } + } + + if (crossedLight || crossedDark) + { + ResetEvent(hManualOverride); + Logger::info(L"[LightSwitchService] Manual override cleared after crossing schedule boundary."); + } + else + { + Logger::debug(L"[LightSwitchService] Skipping schedule due to manual override"); + skipRest = true; } } - // ─── Apply Schedule Logic ─────────────────────────────────────────────────── + // Apply theme if nothing has made us skip if (!skipRest) { + // Next two conditionals check for any updates necessary to the sun times. bool modeChangedToSunset = (prevMode != settings.scheduleMode && settings.scheduleMode == ScheduleMode::SunsetToSunrise); bool coordsChanged = (prevLat != settings.latitude || prevLon != settings.longitude); if ((modeChangedToSunset || coordsChanged) && settings.scheduleMode == ScheduleMode::SunsetToSunrise) { - Logger::info(L"[LightSwitchService] Mode or coordinates changed, recalculating sun times."); - update_sun_times(settings); SYSTEMTIME st; GetLocalTime(&st); + + Logger::info(L"[LightSwitchService] Mode or coordinates changed, recalculating sun times."); + update_sun_times(settings); g_lastUpdatedDay = st.wDay; prevMode = settings.scheduleMode; prevLat = settings.latitude; @@ -383,70 +416,23 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam) Logger::info(L"[LightSwitchService] Recalculated sun times at new day boundary."); } + // settings after any necessary updates. LightSwitchSettings::instance().LoadSettings(); const auto& currentSettings = LightSwitchSettings::instance().settings(); wchar_t msg[160]; swprintf_s(msg, - L"[LightSwitchService] now=%02d:%02d | light=%02d:%02d | dark=%02d:%02d | mode=%d", + L"[LightSwitchService] now=%02d:%02d | light=%02d:%02d | dark=%02d:%02d | mode=%s", st.wHour, st.wMinute, currentSettings.lightTime / 60, currentSettings.lightTime % 60, currentSettings.darkTime / 60, currentSettings.darkTime % 60, - static_cast(currentSettings.scheduleMode)); + ToString(currentSettings.scheduleMode).c_str()); Logger::info(msg); - bool manualOverrideActive = false; - if (hManualOverride) - manualOverrideActive = (WaitForSingleObject(hManualOverride, 0) == WAIT_OBJECT_0); - - if (manualOverrideActive) - { - int lightBoundary = (currentSettings.lightTime + currentSettings.sunrise_offset) % 1440; - int darkBoundary = (currentSettings.darkTime + currentSettings.sunset_offset) % 1440; - - bool crossedLight = false; - bool crossedDark = false; - - if (prevMinutes != -1) - { - if (nowMinutes < prevMinutes) - { - crossedLight = (prevMinutes <= lightBoundary || nowMinutes >= lightBoundary); - crossedDark = (prevMinutes <= darkBoundary || nowMinutes >= darkBoundary); - } - else - { - crossedLight = (prevMinutes < lightBoundary && nowMinutes >= lightBoundary); - crossedDark = (prevMinutes < darkBoundary && nowMinutes >= darkBoundary); - } - } - - Logger::debug(L"[LightSwitchService] prevMinutes={} nowMinutes={} light={} dark={}", - prevMinutes, - nowMinutes, - lightBoundary, - darkBoundary); - - if (crossedLight || crossedDark) - { - ResetEvent(hManualOverride); - Logger::info(L"[LightSwitchService] Manual override cleared after crossing schedule boundary."); - } - else - { - Logger::info(L"[LightSwitchService] Skipping schedule due to manual override"); - skipRest = true; - } - } - - if (!skipRest) - applyTheme(nowMinutes, - currentSettings.lightTime + currentSettings.sunrise_offset, - currentSettings.darkTime + currentSettings.sunset_offset, - currentSettings); + LightSwitchSettings::instance().ApplyThemeIfNecessary(); } // ─── Wait For Next Minute Tick Or Stop Event ──────────────────────────────── @@ -480,54 +466,6 @@ cleanup: return 0; } -void ApplyThemeNow() -{ - LightSwitchSettings::instance().LoadSettings(); - const auto& settings = LightSwitchSettings::instance().settings(); - - SYSTEMTIME st; - GetLocalTime(&st); - int nowMinutes = st.wHour * 60 + st.wMinute; - - bool shouldBeLight = false; - if (settings.lightTime < settings.darkTime) - shouldBeLight = (nowMinutes >= settings.lightTime && nowMinutes < settings.darkTime); - else - shouldBeLight = (nowMinutes >= settings.lightTime || nowMinutes < settings.darkTime); - - bool isSystemCurrentlyLight = GetCurrentSystemTheme(); - bool isAppsCurrentlyLight = GetCurrentAppsTheme(); - - Logger::info(L"[LightSwitchService] Applying (if needed) theme immediately due to schedule change."); - - if (shouldBeLight) - { - if (settings.changeSystem && !isSystemCurrentlyLight) - { - SetSystemTheme(true); - Logger::info(L"[LightSwitchService] Changing system theme to light mode."); - } - if (settings.changeApps && !isAppsCurrentlyLight) - { - SetAppsTheme(true); - Logger::info(L"[LightSwitchService] Changing apps theme to light mode."); - } - } - else - { - if (settings.changeSystem && isSystemCurrentlyLight) - { - SetSystemTheme(false); - Logger::info(L"[LightSwitchService] Changing system theme to dark mode."); - } - if (settings.changeApps && isAppsCurrentlyLight) - { - SetAppsTheme(false); - Logger::info(L"[LightSwitchService] Changing apps theme to dark mode."); - } - } -} - int APIENTRY wWinMain(HINSTANCE, HINSTANCE, PWSTR, int) { if (powertoys_gpo::getConfiguredLightSwitchEnabledValue() == powertoys_gpo::gpo_rule_configured_disabled) diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj b/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj index 832f8ab50c..b082250f61 100644 --- a/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj +++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj @@ -74,7 +74,6 @@ - @@ -85,7 +84,6 @@ - diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj.filters b/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj.filters index 7aa39aa0c2..f5aa05afc3 100644 --- a/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj.filters +++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj.filters @@ -33,9 +33,6 @@ Source Files - - Source Files - @@ -56,9 +53,6 @@ Header Files - - Header Files - diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchServiceObserver.cpp b/src/modules/LightSwitch/LightSwitchService/LightSwitchServiceObserver.cpp deleted file mode 100644 index e28a2625fe..0000000000 --- a/src/modules/LightSwitch/LightSwitchService/LightSwitchServiceObserver.cpp +++ /dev/null @@ -1,29 +0,0 @@ -#include "LightSwitchServiceObserver.h" -#include -#include "LightSwitchSettings.h" - -// These are defined elsewhere in your service module (ServiceWorkerThread.cpp) -extern int g_lastUpdatedDay; -void ApplyThemeNow(); - -void LightSwitchServiceObserver::SettingsUpdate(SettingId id) -{ - Logger::info(L"[LightSwitchService] Setting changed: {}", static_cast(id)); - g_lastUpdatedDay = -1; - ApplyThemeNow(); -} - -bool LightSwitchServiceObserver::WantsToBeNotified(SettingId id) const noexcept -{ - switch (id) - { - case SettingId::LightTime: - case SettingId::DarkTime: - case SettingId::ScheduleMode: - case SettingId::Sunrise_Offset: - case SettingId::Sunset_Offset: - return true; - default: - return false; - } -} diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchServiceObserver.h b/src/modules/LightSwitch/LightSwitchService/LightSwitchServiceObserver.h deleted file mode 100644 index 14c88e656d..0000000000 --- a/src/modules/LightSwitch/LightSwitchService/LightSwitchServiceObserver.h +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -#include "SettingsObserver.h" - -// The LightSwitchServiceObserver reacts when LightSwitchSettings changes. -class LightSwitchServiceObserver : public SettingsObserver -{ -public: - explicit LightSwitchServiceObserver(std::unordered_set observedSettings) : - SettingsObserver(std::move(observedSettings)) - { - } - - void SettingsUpdate(SettingId id) override; - bool WantsToBeNotified(SettingId id) const noexcept override; -}; diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.cpp b/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.cpp index 2e00417001..8105b0ab3a 100644 --- a/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.cpp +++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.cpp @@ -2,7 +2,7 @@ #include #include #include "SettingsObserver.h" - +#include "ThemeHelper.h" #include #include #include @@ -38,13 +38,80 @@ void LightSwitchSettings::InitFileWatcher() m_settingsFileWatcher = std::make_unique( GetSettingsFileName(), [this]() { - Logger::info(L"[LightSwitchSettings] Settings file changed, signaling event."); - LoadSettings(); - SetEvent(m_settingsChangedEvent); + using namespace std::chrono; + + { + std::lock_guard lock(m_debounceMutex); + m_lastChangeTime = steady_clock::now(); + if (m_debouncePending) + return; + m_debouncePending = true; + } + + m_debounceThread = std::jthread([this](std::stop_token stop) { + using namespace std::chrono; + while (!stop.stop_requested()) + { + std::this_thread::sleep_for(seconds(3)); + + auto elapsed = steady_clock::now() - m_lastChangeTime; + if (elapsed >= seconds(1)) + break; + } + + { + std::lock_guard lock(m_debounceMutex); + m_debouncePending = false; + } + + Logger::info(L"[LightSwitchSettings] Settings file stabilized, reloading."); + + try + { + LoadSettings(); + ApplyThemeIfNecessary(); + SetEvent(m_settingsChangedEvent); + } + catch (const std::exception& e) + { + std::wstring wmsg; + wmsg.assign(e.what(), e.what() + strlen(e.what())); + Logger::error(L"[LightSwitchSettings] Exception during debounced reload: {}", wmsg); + } + }); }); } } +LightSwitchSettings::~LightSwitchSettings() +{ + Logger::info(L"[LightSwitchSettings] Cleaning up settings resources..."); + + // Stop and join the debounce thread (std::jthread auto-joins, but we can signal stop too) + if (m_debounceThread.joinable()) + { + m_debounceThread.request_stop(); + } + + // Release the file watcher so it closes file handles and background threads + if (m_settingsFileWatcher) + { + m_settingsFileWatcher.reset(); + Logger::info(L"[LightSwitchSettings] File watcher stopped."); + } + + // Close the Windows event handle + if (m_settingsChangedEvent) + { + CloseHandle(m_settingsChangedEvent); + m_settingsChangedEvent = nullptr; + Logger::info(L"[LightSwitchSettings] Settings changed event closed."); + } + + Logger::info(L"[LightSwitchSettings] Cleanup complete."); +} + + void LightSwitchSettings::AddObserver(SettingsObserver& observer) { m_observers.insert(&observer); @@ -73,6 +140,7 @@ HANDLE LightSwitchSettings::GetSettingsChangedEvent() const void LightSwitchSettings::LoadSettings() { + std::lock_guard guard(m_settingsMutex); try { PowerToysSettings::PowerToyValues values = @@ -181,4 +249,49 @@ void LightSwitchSettings::LoadSettings() { // Keeps defaults if load fails } +} + +void LightSwitchSettings::ApplyThemeIfNecessary() +{ + std::lock_guard guard(m_settingsMutex); + + SYSTEMTIME st; + GetLocalTime(&st); + int nowMinutes = st.wHour * 60 + st.wMinute; + + bool shouldBeLight = false; + if (m_settings.lightTime < m_settings.darkTime) + shouldBeLight = (nowMinutes >= m_settings.lightTime && nowMinutes < m_settings.darkTime); + else + shouldBeLight = (nowMinutes >= m_settings.lightTime || nowMinutes < m_settings.darkTime); + + bool isSystemCurrentlyLight = GetCurrentSystemTheme(); + bool isAppsCurrentlyLight = GetCurrentAppsTheme(); + + if (shouldBeLight) + { + if (m_settings.changeSystem && !isSystemCurrentlyLight) + { + SetSystemTheme(true); + Logger::info(L"[LightSwitchService] Changing system theme to light mode."); + } + if (m_settings.changeApps && !isAppsCurrentlyLight) + { + SetAppsTheme(true); + Logger::info(L"[LightSwitchService] Changing apps theme to light mode."); + } + } + else + { + if (m_settings.changeSystem && isSystemCurrentlyLight) + { + SetSystemTheme(false); + Logger::info(L"[LightSwitchService] Changing system theme to dark mode."); + } + if (m_settings.changeApps && isAppsCurrentlyLight) + { + SetAppsTheme(false); + Logger::info(L"[LightSwitchService] Changing apps theme to dark mode."); + } + } } \ No newline at end of file diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.h b/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.h index e5e993c696..25f60cec82 100644 --- a/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.h +++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.h @@ -5,7 +5,10 @@ #include #include #include - +#include +#include +#include +#include #include #include #include @@ -78,12 +81,13 @@ public: void RemoveObserver(SettingsObserver& observer); void LoadSettings(); + void ApplyThemeIfNecessary(); HANDLE GetSettingsChangedEvent() const; private: LightSwitchSettings(); - ~LightSwitchSettings() = default; + ~LightSwitchSettings(); LightSwitchConfig m_settings; std::unique_ptr m_settingsFileWatcher; @@ -92,4 +96,11 @@ private: void NotifyObservers(SettingId id) const; HANDLE m_settingsChangedEvent = nullptr; + mutable std::mutex m_settingsMutex; + + // Debounce state + std::atomic_bool m_debouncePending{ false }; + std::mutex m_debounceMutex; + std::chrono::steady_clock::time_point m_lastChangeTime{}; + std::jthread m_debounceThread; };