From 55f28f8ff28f5e27af74148129be5be60424f299 Mon Sep 17 00:00:00 2001 From: Jaylyn Barbee <51131738+Jaylyn-Barbee@users.noreply.github.com> Date: Fri, 7 Nov 2025 12:02:18 -0500 Subject: [PATCH] Refactored to use MVC layout in service --- .../LightSwitchService/LightSwitchService.cpp | 392 +++++------------- .../LightSwitchService.vcxproj | 3 + .../LightSwitchService.vcxproj.filters | 9 + .../LightSwitchSettings.cpp | 48 --- .../LightSwitchService/LightSwitchSettings.h | 1 - .../LightSwitchStateManager.cpp | 195 +++++++++ .../LightSwitchStateManager.h | 42 ++ .../LightSwitchService/LightSwitchUtils.h | 25 ++ .../Views/LightSwitchPage.xaml.cs | 2 +- 9 files changed, 386 insertions(+), 331 deletions(-) create mode 100644 src/modules/LightSwitch/LightSwitchService/LightSwitchStateManager.cpp create mode 100644 src/modules/LightSwitch/LightSwitchService/LightSwitchStateManager.h create mode 100644 src/modules/LightSwitch/LightSwitchService/LightSwitchUtils.h diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchService.cpp b/src/modules/LightSwitch/LightSwitchService/LightSwitchService.cpp index 503a98de29..b52add4d85 100644 --- a/src/modules/LightSwitch/LightSwitchService/LightSwitchService.cpp +++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchService.cpp @@ -11,19 +11,23 @@ #include #include #include +#include "LightSwitchStateManager.h" +#include SERVICE_STATUS g_ServiceStatus = {}; SERVICE_STATUS_HANDLE g_StatusHandle = nullptr; HANDLE g_ServiceStopEvent = nullptr; extern int g_lastUpdatedDay = -1; static ScheduleMode prevMode = ScheduleMode::Off; -static std::wstring prevLat, prevLon; +static std::wstring prevLat = L"0"; +static std::wstring prevLon = L"0"; static int prevMinutes = -1; static bool lastOverrideStatus = false; VOID WINAPI ServiceMain(DWORD argc, LPTSTR* argv); VOID WINAPI ServiceCtrlHandler(DWORD dwCtrl); DWORD WINAPI ServiceWorkerThread(LPVOID lpParam); +void ApplyTheme(bool shouldBeLight); // Entry point for the executable int _tmain(int argc, TCHAR* argv[]) @@ -124,31 +128,66 @@ VOID WINAPI ServiceCtrlHandler(DWORD dwCtrl) } } -static void update_sun_times(auto& settings) +void ApplyTheme(bool shouldBeLight) { - double latitude = std::stod(settings.latitude); - double longitude = std::stod(settings.longitude); + const auto& s = LightSwitchSettings::settings(); + + if (s.changeSystem) + { + bool isSystemCurrentlyLight = GetCurrentSystemTheme(); + if (shouldBeLight != isSystemCurrentlyLight) + { + SetSystemTheme(shouldBeLight); + Logger::info(L"[LightSwitchService] Changed system theme to {}.", shouldBeLight ? L"light" : L"dark"); + } + } + + if (s.changeApps) + { + bool isAppsCurrentlyLight = GetCurrentAppsTheme(); + if (shouldBeLight != isAppsCurrentlyLight) + { + SetAppsTheme(shouldBeLight); + Logger::info(L"[LightSwitchService] Changed apps theme to {}.", shouldBeLight ? L"light" : L"dark"); + } + } +} + +static void DetectAndHandleExternalThemeChange(LightSwitchStateManager stateManager) +{ + const auto& s = LightSwitchSettings::settings(); + if (s.scheduleMode == ScheduleMode::Off) + return; SYSTEMTIME st; GetLocalTime(&st); + int nowMinutes = st.wHour * 60 + st.wMinute; - SunTimes newTimes = CalculateSunriseSunset(latitude, longitude, st.wYear, st.wMonth, st.wDay); + // Compute effective boundaries (with offsets if needed) + int effectiveLight = s.lightTime; + int effectiveDark = s.darkTime; - int newLightTime = newTimes.sunriseHour * 60 + newTimes.sunriseMinute; - int newDarkTime = newTimes.sunsetHour * 60 + newTimes.sunsetMinute; - try + if (s.scheduleMode == ScheduleMode::SunsetToSunrise) { - auto values = PowerToysSettings::PowerToyValues::load_from_settings_file(L"LightSwitch"); - values.add_property(L"lightTime", newLightTime); - values.add_property(L"darkTime", newDarkTime); - values.save_to_settings_file(); - - Logger::info(L"[LightSwitchService] Updated sun times and saved to config."); + effectiveLight = (s.lightTime + s.sunrise_offset) % 1440; + effectiveDark = (s.darkTime + s.sunset_offset) % 1440; } - catch (const std::exception& e) + + // Use shared helper (handles wraparound logic) + bool shouldBeLight = ShouldBeLight(nowMinutes, effectiveLight, effectiveDark); + + // Compare current system/apps theme + bool currentSystemLight = GetCurrentSystemTheme(); + bool currentAppsLight = GetCurrentAppsTheme(); + + bool systemMismatch = s.changeSystem && (currentSystemLight != shouldBeLight); + bool appsMismatch = s.changeApps && (currentAppsLight != shouldBeLight); + + // Trigger manual override only if mismatch and not already active + if ((systemMismatch || appsMismatch) && !stateManager.GetState().isManualOverride) { - std::wstring wmsg(e.what(), e.what() + strlen(e.what())); - Logger::error(L"[LightSwitchService] Exception during sun time update: {}", wmsg); + Logger::info(L"[LightSwitchService] External theme change detected (Windows Settings). Entering manual override mode."); + stateManager.OnManualOverride(); } } @@ -162,307 +201,98 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam) Logger::info(L"[LightSwitchService] Worker thread starting..."); Logger::info(L"[LightSwitchService] Parent PID: {}", parentPid); + // ──────────────────────────────────────────────────────────────── + // Initialization + // ──────────────────────────────────────────────────────────────── + static LightSwitchStateManager stateManager; + LightSwitchSettings::instance().InitFileWatcher(); HANDLE hManualOverride = OpenEventW(SYNCHRONIZE | EVENT_MODIFY_STATE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE"); + HANDLE hSettingsChanged = LightSwitchSettings::instance().GetSettingsChangedEvent(); LightSwitchSettings::instance().LoadSettings(); - auto& settings = LightSwitchSettings::instance().settings(); + const auto& settings = LightSwitchSettings::instance().settings(); SYSTEMTIME st; GetLocalTime(&st); int nowMinutes = st.wHour * 60 + st.wMinute; - // Handle initial theme application if necessary - if (settings.scheduleMode != ScheduleMode::Off) - { - 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 set to Off."); - } + Logger::info(L"[LightSwitchService] Initialized at {:02d}:{:02d}.", st.wHour, st.wMinute); + stateManager.OnTick(nowMinutes); - g_lastUpdatedDay = st.wDay; - Logger::info(L"[LightSwitchService] Initializing g_lastUpdatedDay to {}.", g_lastUpdatedDay); - ULONGLONG lastSettingsReload = 0; - - // ticker loop + // ──────────────────────────────────────────────────────────────── + // Worker Loop + // ──────────────────────────────────────────────────────────────── for (;;) { - HANDLE waits[2] = { g_ServiceStopEvent, hParent }; - DWORD count = hParent ? 2 : 1; - bool skipRest = false; - - const auto& settings = LightSwitchSettings::instance().settings(); - - // 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 is OFF - suspending scheduler but keeping service alive."); - - if (!hManualOverride) - hManualOverride = OpenEventW(SYNCHRONIZE | EVENT_MODIFY_STATE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE"); - - HANDLE waitsOff[4]; - DWORD countOff = 0; - waitsOff[countOff++] = g_ServiceStopEvent; - if (hParent) - waitsOff[countOff++] = hParent; - if (hManualOverride) - waitsOff[countOff++] = hManualOverride; - waitsOff[countOff++] = LightSwitchSettings::instance().GetSettingsChangedEvent(); - - for (;;) - { - DWORD wait = WaitForMultipleObjects(countOff, waitsOff, FALSE, INFINITE); - - if (wait == WAIT_OBJECT_0) - { - Logger::info(L"[LightSwitchService] Stop event triggered - exiting worker loop."); - goto cleanup; - } - if (hParent && wait == WAIT_OBJECT_0 + 1) - { - Logger::info(L"[LightSwitchService] Parent exited - stopping service."); - goto cleanup; - } - - if (wait == WAIT_OBJECT_0 + (hParent ? 2 : 1)) - { - Logger::info(L"[LightSwitchService] Manual override received while schedule OFF."); - ResetEvent(hManualOverride); - continue; - } - - if (wait == WAIT_OBJECT_0 + (hParent ? 3 : 2)) - { - Logger::trace(L"[LightSwitchService] Settings change event triggered, reloading settings..."); - ResetEvent(LightSwitchSettings::instance().GetSettingsChangedEvent()); - const auto& newSettings = LightSwitchSettings::instance().settings(); - lastSettingsReload = GetTickCount64(); - - if (newSettings.scheduleMode != ScheduleMode::Off) - { - Logger::info(L"[LightSwitchService] Schedule re-enabled, resuming normal loop."); - break; - } - } - } - - continue; - } - - bool scheduleJustEnabled = (prevMode == ScheduleMode::Off && settings.scheduleMode != ScheduleMode::Off); - prevMode = settings.scheduleMode; - - ULONGLONG nowTick = GetTickCount64(); - bool recentSettingsReload = (nowTick - lastSettingsReload < 2000); - - 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) - { - Logger::debug(L"[LightSwitchService] Manual override active = {}", manualOverrideActive); - lastOverrideStatus = manualOverrideActive; - } - - 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) - { - lightBoundary = (settings.lightTime + settings.sunrise_offset) % 1440; - darkBoundary = (settings.darkTime + settings.sunset_offset) % 1440; - } - else - { - 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)."); - } - + HANDLE waits[4]; + DWORD count = 0; + waits[count++] = g_ServiceStopEvent; + if (hParent) + waits[count++] = hParent; if (hManualOverride) - manualOverrideActive = (WaitForSingleObject(hManualOverride, 0) == WAIT_OBJECT_0); + waits[count++] = hManualOverride; + waits[count++] = hSettingsChanged; - 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 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) - { - 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; - prevLon = settings.longitude; - } - - SYSTEMTIME st; - GetLocalTime(&st); - int nowMinutes = st.wHour * 60 + st.wMinute; - - if ((g_lastUpdatedDay != st.wDay) && (settings.scheduleMode == ScheduleMode::SunsetToSunrise)) - { - update_sun_times(settings); - g_lastUpdatedDay = st.wDay; - prevMinutes = -1; - 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=%s", - st.wHour, - st.wMinute, - currentSettings.lightTime / 60, - currentSettings.lightTime % 60, - currentSettings.darkTime / 60, - currentSettings.darkTime % 60, - ToString(currentSettings.scheduleMode).c_str()); - Logger::info(msg); - - LightSwitchSettings::instance().ApplyThemeIfNecessary(); - } - - // ─── Wait For Next Minute Tick Or Stop Event ──────────────────────────────── + // Wait for one of these to trigger or for a new minute tick SYSTEMTIME st; GetLocalTime(&st); int msToNextMinute = (60 - st.wSecond) * 1000 - st.wMilliseconds; if (msToNextMinute < 50) msToNextMinute = 50; - prevMinutes = nowMinutes; - DWORD wait = WaitForMultipleObjects(count, waits, FALSE, msToNextMinute); + + if (wait == WAIT_TIMEOUT) + { + // regular minute tick + GetLocalTime(&st); + nowMinutes = st.wHour * 60 + st.wMinute; + DetectAndHandleExternalThemeChange(stateManager); + stateManager.OnTick(nowMinutes); + continue; + } + if (wait == WAIT_OBJECT_0) { - Logger::info(L"[LightSwitchService] Stop event triggered - exiting worker loop."); + Logger::info(L"[LightSwitchService] Stop event triggered — exiting."); break; } + if (hParent && wait == WAIT_OBJECT_0 + 1) { - Logger::info(L"[LightSwitchService] Parent process exited - stopping service."); + Logger::info(L"[LightSwitchService] Parent process exited — stopping service."); break; } + + if (hManualOverride && wait == WAIT_OBJECT_0 + (hParent ? 2 : 1)) + { + Logger::info(L"[LightSwitchService] Manual override event detected."); + stateManager.OnManualOverride(); + ResetEvent(hManualOverride); + continue; + } + + if (wait == WAIT_OBJECT_0 + (hParent ? (hManualOverride ? 3 : 2) : 2)) + { + Logger::info(L"[LightSwitchService] Settings file changed event detected."); + ResetEvent(hSettingsChanged); + LightSwitchSettings::instance().LoadSettings(); + stateManager.OnSettingsChanged(); + continue; + } } -cleanup: + // ──────────────────────────────────────────────────────────────── + // Cleanup + // ──────────────────────────────────────────────────────────────── if (hManualOverride) CloseHandle(hManualOverride); if (hParent) CloseHandle(hParent); + Logger::info(L"[LightSwitchService] Worker thread exiting cleanly."); return 0; } diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj b/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj index b082250f61..a3a505f897 100644 --- a/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj +++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj @@ -75,6 +75,7 @@ + @@ -85,6 +86,8 @@ + + diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj.filters b/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj.filters index f5aa05afc3..795df99aba 100644 --- a/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj.filters +++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj.filters @@ -33,6 +33,9 @@ Source Files + + Source Files + @@ -53,6 +56,12 @@ Header Files + + Header Files + + + Header Files + diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.cpp b/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.cpp index 8105b0ab3a..5221a197fe 100644 --- a/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.cpp +++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.cpp @@ -2,10 +2,8 @@ #include #include #include "SettingsObserver.h" -#include "ThemeHelper.h" #include #include -#include #include using namespace std; @@ -69,7 +67,6 @@ void LightSwitchSettings::InitFileWatcher() try { LoadSettings(); - ApplyThemeIfNecessary(); SetEvent(m_settingsChangedEvent); } catch (const std::exception& e) @@ -250,48 +247,3 @@ 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 25f60cec82..d4029d072d 100644 --- a/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.h +++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.h @@ -81,7 +81,6 @@ public: void RemoveObserver(SettingsObserver& observer); void LoadSettings(); - void ApplyThemeIfNecessary(); HANDLE GetSettingsChangedEvent() const; diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchStateManager.cpp b/src/modules/LightSwitch/LightSwitchService/LightSwitchStateManager.cpp new file mode 100644 index 0000000000..5ede2f8965 --- /dev/null +++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchStateManager.cpp @@ -0,0 +1,195 @@ +#include "pch.h" +#include "LightSwitchStateManager.h" +#include +#include +#include "ThemeScheduler.h" +#include + +void ApplyTheme(bool shouldBeLight); + +// Constructor +LightSwitchStateManager::LightSwitchStateManager() +{ + Logger::info(L"[LightSwitchStateManager] Initialized"); +} + +// Called when settings.json changes +void LightSwitchStateManager::OnSettingsChanged() +{ + Logger::info(L"[LightSwitchStateManager] Settings changed event received"); + + // If manual override was active, clear it so new settings take effect + if (_state.isManualOverride) + { + Logger::info(L"[LightSwitchStateManager] Clearing manual override due to settings update."); + _state.isManualOverride = false; + } + + EvaluateAndApplyIfNeeded(); +} + + +// Called once per minute +void LightSwitchStateManager::OnTick(int currentMinutes) +{ + Logger::debug(L"[LightSwitchStateManager] Tick received: {}", currentMinutes); + EvaluateAndApplyIfNeeded(); +} + +// Called when manual override is triggered +void LightSwitchStateManager::OnManualOverride() +{ + Logger::info(L"[LightSwitchStateManager] Manual override triggered"); + _state.isManualOverride = !_state.isManualOverride; + + // When entering manual override, sync internal theme state to match the current system + if (_state.isManualOverride) + { + bool systemLight = GetCurrentSystemTheme(); + _state.isLightThemeActive = systemLight; + Logger::info(L"[LightSwitchStateManager] Synced internal theme state to current system theme ({})", + systemLight ? L"light" : L"dark"); + } + + EvaluateAndApplyIfNeeded(); +} + +// Helpers +bool LightSwitchStateManager::CoordinatesAreValid(const std::wstring& lat, const std::wstring& lon) +{ + try + { + double latVal = std::stod(lat); + double lonVal = std::stod(lon); + return !(latVal == 0 && lonVal == 0); + } + catch (...) + { + return false; + } +} + +static std::pair update_sun_times(auto& settings) +{ + double latitude = std::stod(settings.latitude); + double longitude = std::stod(settings.longitude); + + SYSTEMTIME st; + GetLocalTime(&st); + + SunTimes newTimes = CalculateSunriseSunset(latitude, longitude, st.wYear, st.wMonth, st.wDay); + + int newLightTime = newTimes.sunriseHour * 60 + newTimes.sunriseMinute; + int newDarkTime = newTimes.sunsetHour * 60 + newTimes.sunsetMinute; + + try + { + auto values = PowerToysSettings::PowerToyValues::load_from_settings_file(L"LightSwitch"); + values.add_property(L"lightTime", newLightTime); + values.add_property(L"darkTime", newDarkTime); + values.save_to_settings_file(); + + Logger::info(L"[LightSwitchService] Updated sun times and saved to config."); + } + catch (const std::exception& e) + { + std::wstring wmsg(e.what(), e.what() + strlen(e.what())); + Logger::error(L"[LightSwitchService] Exception during sun time update: {}", wmsg); + } + + return { newLightTime, newDarkTime }; +} + +// Internal: decide what should happen now +void LightSwitchStateManager::EvaluateAndApplyIfNeeded() +{ + const auto& _currentSettings = LightSwitchSettings::settings(); + auto now = GetNowMinutes(); + + // Early exit: OFF mode just pauses activity + if (_currentSettings.scheduleMode == ScheduleMode::Off) + { + Logger::debug(L"[LightSwitchStateManager] Mode is OFF — pausing service logic."); + _state.lastTickMinutes = now; + return; + } + + bool coordsValid = CoordinatesAreValid(_currentSettings.latitude, _currentSettings.longitude); + + // Handle Sun Mode recalculation + if (_currentSettings.scheduleMode == ScheduleMode::SunsetToSunrise && coordsValid) + { + SYSTEMTIME st; + GetLocalTime(&st); + bool newDay = (_state.lastEvaluatedDay != st.wDay); + bool modeChangedToSun = (_state.lastAppliedMode != ScheduleMode::SunsetToSunrise && + _currentSettings.scheduleMode == ScheduleMode::SunsetToSunrise); + + if (newDay || modeChangedToSun) + { + Logger::info(L"[LightSwitchStateManager] Recalculating sun times (mode/day change)."); + auto [newLightTime, newDarkTime] = update_sun_times(_currentSettings); + _state.lastEvaluatedDay = st.wDay; + _state.effectiveLightMinutes = newLightTime + _currentSettings.sunrise_offset; + _state.effectiveDarkMinutes = newDarkTime + _currentSettings.sunset_offset; + } + } + else if (_currentSettings.scheduleMode == ScheduleMode::FixedHours) + { + _state.effectiveLightMinutes = _currentSettings.lightTime; + _state.effectiveDarkMinutes = _currentSettings.darkTime; + } + + // Handle manual override logic + if (_state.isManualOverride) + { + bool crossedBoundary = false; + if (_state.lastTickMinutes != -1) + { + int prev = _state.lastTickMinutes; + + // Handle midnight wraparound safely + if (now < prev) + { + crossedBoundary = + (prev <= _state.effectiveLightMinutes || now >= _state.effectiveLightMinutes) || + (prev <= _state.effectiveDarkMinutes || now >= _state.effectiveDarkMinutes); + } + else + { + crossedBoundary = + (prev < _state.effectiveLightMinutes && now >= _state.effectiveLightMinutes) || + (prev < _state.effectiveDarkMinutes && now >= _state.effectiveDarkMinutes); + } + } + + if (crossedBoundary) + { + Logger::info(L"[LightSwitchStateManager] Manual override cleared after crossing boundary."); + _state.isManualOverride = false; + } + else + { + Logger::debug(L"[LightSwitchStateManager] Manual override active — skipping auto apply."); + _state.lastTickMinutes = now; + return; + } + } + + _state.lastAppliedMode = _currentSettings.scheduleMode; + + bool shouldBeLight = ShouldBeLight(now, _state.effectiveLightMinutes, _state.effectiveDarkMinutes); + + // Only apply theme if there's a change or no override active + if (!_state.isManualOverride && _state.isLightThemeActive != shouldBeLight) + { + Logger::info(L"[LightSwitchStateManager] Applying {} theme", shouldBeLight ? L"light" : L"dark"); + ApplyTheme(shouldBeLight); + _state.isLightThemeActive = shouldBeLight; + } + + _state.lastTickMinutes = now; +} + + + diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchStateManager.h b/src/modules/LightSwitch/LightSwitchService/LightSwitchStateManager.h new file mode 100644 index 0000000000..eb9294d8d6 --- /dev/null +++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchStateManager.h @@ -0,0 +1,42 @@ +#pragma once +#include "LightSwitchSettings.h" +#include + +// Represents runtime-only information (not saved in settings.json) +struct LightSwitchState +{ + ScheduleMode lastAppliedMode = ScheduleMode::Off; + bool isManualOverride = false; + bool isLightThemeActive = false; + int lastEvaluatedDay = -1; + int lastTickMinutes = -1; + + // Derived, runtime-resolved times + int effectiveLightMinutes = 0; // the boundary we actually act on + int effectiveDarkMinutes = 0; // includes offsets if needed +}; + +// The controller that reacts to settings changes, time ticks, and manual overrides. +class LightSwitchStateManager +{ +public: + LightSwitchStateManager(); + + // Called when settings.json changes or stabilizes. + void OnSettingsChanged(); + + // Called every minute (from service worker tick). + void OnTick(int currentMinutes); + + // Called when manual override is toggled (via shortcut or system change). + void OnManualOverride(); + + // Accessor for current state (optional, for debugging or telemetry) + const LightSwitchState& GetState() const { return _state; } + +private: + LightSwitchState _state; + + void EvaluateAndApplyIfNeeded(); + bool CoordinatesAreValid(const std::wstring& lat, const std::wstring& lon); +}; diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchUtils.h b/src/modules/LightSwitch/LightSwitchService/LightSwitchUtils.h new file mode 100644 index 0000000000..10b5bbdab0 --- /dev/null +++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchUtils.h @@ -0,0 +1,25 @@ +#pragma once +#include + +constexpr bool ShouldBeLight(int nowMinutes, int lightTime, int darkTime) +{ + // Normalize values into [0, 1439] + lightTime = (lightTime % 1440 + 1440) % 1440; + darkTime = (darkTime % 1440 + 1440) % 1440; + nowMinutes = (nowMinutes % 1440 + 1440) % 1440; + + // Case 1: Normal range, e.g. light mode comes before dark mode in the same day + if (lightTime < darkTime) + return nowMinutes >= lightTime && nowMinutes < darkTime; + + // Case 2: Wrap-around range, e.g. light mode starts in the evening and dark mode starts in the morning + return nowMinutes >= lightTime || nowMinutes < darkTime; +} + + +inline int GetNowMinutes() +{ + SYSTEMTIME st; + GetLocalTime(&st); + return st.wHour * 60 + st.wMinute; +} diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/LightSwitchPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/LightSwitchPage.xaml.cs index 1a039b213f..b9311d4c7e 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/LightSwitchPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/LightSwitchPage.xaml.cs @@ -37,7 +37,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views private readonly IFileSystemWatcher _fileSystemWatcher; private readonly DispatcherQueue _dispatcherQueue; private bool _suppressViewModelUpdates; - private bool _suppressLatLonChange; + private bool _suppressLatLonChange = true; private LightSwitchViewModel ViewModel { get; set; }