Skip to content
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
b745f61
Update LightSwitchPage.xaml
niels9001 Oct 28, 2025
b3e09e6
Adding manual lat lon UI
niels9001 Nov 4, 2025
be87a44
added in logic
Jaylyn-Barbee Nov 4, 2025
814eb78
XAML styling
Jaylyn-Barbee Nov 4, 2025
3b1dfeb
Disable input boxes if using location services to fetch location
Jaylyn-Barbee Nov 5, 2025
e8ee996
enable button on manual entry
Jaylyn-Barbee Nov 5, 2025
26b22b5
xaml styling
Jaylyn-Barbee Nov 5, 2025
fd99820
trying to fix infinite settings updates
Jaylyn-Barbee Nov 5, 2025
e749f21
fixed default state issue and circular dependency causing looping
Jaylyn-Barbee Nov 5, 2025
b71140e
Merge branch 'main' into feature/lightswitch-manual-location
Jaylyn-Barbee Nov 5, 2025
ac0b56a
Xaml styling
Jaylyn-Barbee Nov 5, 2025
eec7940
Copilot Suggestions
Jaylyn-Barbee Nov 5, 2025
d172c21
Merge branch 'main' into feature/lightswitch-manual-location
Jaylyn-Barbee Nov 6, 2025
da0fe7a
Resolve merge: keep ours for ShortcutConflictControl.xaml
niels9001 Nov 6, 2025
5cf0b4d
Merge branch 'main' into feature/lightswitch-manual-location
Jaylyn-Barbee Nov 7, 2025
55f28f8
Refactored to use MVC layout in service
Jaylyn-Barbee Nov 7, 2025
48ddffa
manual boxes should be working
Jaylyn-Barbee Nov 7, 2025
89d58aa
Copilot suggestions
Jaylyn-Barbee Nov 7, 2025
95b9236
xaml clean up
Jaylyn-Barbee Nov 7, 2025
bd73f4a
added locks to prevent race conditions
Jaylyn-Barbee Nov 7, 2025
62ea01e
Fixed initial theme desync
Jaylyn-Barbee Nov 7, 2025
29de283
Merge branch 'main' of https://github.com/microsoft/PowerToys
niels9001 Nov 9, 2025
64dafdc
Merge branch 'main' into feature/lightswitch-manual-location
niels9001 Nov 9, 2025
ae2f817
removed unused vars
Jaylyn-Barbee Nov 10, 2025
b01c819
Merge branch 'feature/lightswitch-manual-location' of https://github.…
Jaylyn-Barbee Nov 10, 2025
c0bc441
Made LightSwitchPage Navigable
Jaylyn-Barbee Nov 10, 2025
7bb7d92
UI Tests
Jaylyn-Barbee Nov 10, 2025
0d52787
better responsiveness from lat/lon boxes
Jaylyn-Barbee Nov 11, 2025
a19de59
Fixing lat/lon boxes
Jaylyn-Barbee Nov 11, 2025
eb41e17
Everything working as expected
Jaylyn-Barbee Nov 11, 2025
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
393 changes: 109 additions & 284 deletions src/modules/LightSwitch/LightSwitchService/LightSwitchService.cpp

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
<ItemGroup>
<ClCompile Include="LightSwitchService.cpp" />
<ClCompile Include="LightSwitchSettings.cpp" />
<ClCompile Include="LightSwitchStateManager.cpp" />
<ClCompile Include="SettingsConstants.cpp" />
<ClCompile Include="ThemeHelper.cpp" />
<ClCompile Include="ThemeScheduler.cpp" />
Expand All @@ -85,6 +86,8 @@
</ItemGroup>
<ItemGroup>
<ClInclude Include="LightSwitchSettings.h" />
<ClInclude Include="LightSwitchStateManager.h" />
<ClInclude Include="LightSwitchUtils.h" />
<ClInclude Include="SettingsConstants.h" />
<ClInclude Include="SettingsObserver.h" />
<ClInclude Include="ThemeHelper.h" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
<ClCompile Include="WinHookEventIDs.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="LightSwitchStateManager.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="ThemeScheduler.h">
Expand All @@ -53,6 +56,12 @@
<ClInclude Include="WinHookEventIDs.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="LightSwitchStateManager.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="LightSwitchUtils.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natvis" />
Expand Down
48 changes: 0 additions & 48 deletions src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@
#include <common/utils/json.h>
#include <common/SettingsAPI/settings_helpers.h>
#include "SettingsObserver.h"
#include "ThemeHelper.h"
#include <filesystem>
#include <fstream>
#include <WinHookEventIDs.h>
#include <logger.h>

using namespace std;
Expand Down Expand Up @@ -69,7 +67,6 @@ void LightSwitchSettings::InitFileWatcher()
try
{
LoadSettings();
ApplyThemeIfNecessary();
SetEvent(m_settingsChangedEvent);
}
catch (const std::exception& e)
Expand Down Expand Up @@ -250,48 +247,3 @@ void LightSwitchSettings::LoadSettings()
// Keeps defaults if load fails
}
}

void LightSwitchSettings::ApplyThemeIfNecessary()
{
std::lock_guard<std::mutex> 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.");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ class LightSwitchSettings
void RemoveObserver(SettingsObserver& observer);

void LoadSettings();
void ApplyThemeIfNecessary();

HANDLE GetSettingsChangedEvent() const;

Expand Down
226 changes: 226 additions & 0 deletions src/modules/LightSwitch/LightSwitchService/LightSwitchStateManager.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
#include "pch.h"
#include "LightSwitchStateManager.h"
#include <logger.h>
#include <LightSwitchUtils.h>
#include "ThemeScheduler.h"
#include <ThemeHelper.h>

void ApplyTheme(bool shouldBeLight);

// Constructor
LightSwitchStateManager::LightSwitchStateManager()
{
Logger::info(L"[LightSwitchStateManager] Initialized");
}

// Called when settings.json changes
void LightSwitchStateManager::OnSettingsChanged()
{
std::lock_guard<std::mutex> lock(_stateMutex);
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)
{
std::lock_guard<std::mutex> lock(_stateMutex);
Logger::debug(L"[LightSwitchStateManager] Tick received: {}", currentMinutes);
EvaluateAndApplyIfNeeded();
}

// Called when manual override is triggered
void LightSwitchStateManager::OnManualOverride()
{
std::lock_guard<std::mutex> lock(_stateMutex);
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)
{
_state.isSystemLightActive = GetCurrentSystemTheme();

_state.isAppsLightActive = GetCurrentAppsTheme();

Logger::info(L"[LightSwitchStateManager] Synced internal theme state to current system theme ({}) and apps theme ({}).",
(_state.isSystemLightActive ? L"light" : L"dark"),
(_state.isAppsLightActive ? 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) && (latVal >= -90.0 && latVal <= 90.0) && (lonVal >= -180.0 && lonVal <= 180.0);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why can't latitude and longitude both be equal to zero? not sure this is a problem but struck me

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's what we use as the defaults and it just prevents updates from happening when they aren't supposed to. Luckily, I don't think anyone lives here
image

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we just have null as defaults?

}
catch (...)
{
return false;
}
}

void LightSwitchStateManager::SyncInitialThemeState()
{
std::lock_guard<std::mutex> lock(_stateMutex);
_state.isSystemLightActive = GetCurrentSystemTheme();
_state.isAppsLightActive = GetCurrentAppsTheme();
Logger::info(L"[LightSwitchStateManager] Synced initial state to current system theme ({})",
_state.isSystemLightActive ? L"light" : L"dark");
Logger::info(L"[LightSwitchStateManager] Synced initial state to current apps theme ({})",
_state.isAppsLightActive ? L"light" : L"dark");
}

static std::pair<int, int> 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::string msg = e.what();
std::wstring wmsg(msg.begin(), msg.end());
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);

bool appsNeedsToChange = _currentSettings.changeApps && (_state.isAppsLightActive != shouldBeLight);
bool systemNeedsToChange = _currentSettings.changeSystem && (_state.isSystemLightActive != shouldBeLight);

Logger::debug("should be light = {}, apps needs change = {}, system needs change = {}",
shouldBeLight ? "true" : "false",
appsNeedsToChange ? "true" : "false",
systemNeedsToChange ? "true" : "false");

// Only apply theme if there's a change or no override active
if (!_state.isManualOverride && (appsNeedsToChange || systemNeedsToChange))
{
Logger::info(L"[LightSwitchStateManager] Applying {} theme", shouldBeLight ? L"light" : L"dark");
ApplyTheme(shouldBeLight);

_state.isSystemLightActive = GetCurrentSystemTheme();
_state.isAppsLightActive = GetCurrentAppsTheme();

Logger::debug(L"[LightSwitchStateManager] Synced post-apply theme state — System: {}, Apps: {}",
_state.isSystemLightActive ? L"light" : L"dark",
_state.isAppsLightActive ? L"light" : L"dark");
}

_state.lastTickMinutes = now;
}



Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#pragma once
#include "LightSwitchSettings.h"
#include <optional>

// Represents runtime-only information (not saved in settings.json)
struct LightSwitchState
{
ScheduleMode lastAppliedMode = ScheduleMode::Off;
bool isManualOverride = false;
bool isSystemLightActive = false;
bool isAppsLightActive = 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();

// Initial sync at startup to align internal state with system theme
void SyncInitialThemeState();

// Accessor for current state (optional, for debugging or telemetry)
const LightSwitchState& GetState() const { return _state; }

private:
LightSwitchState _state;
std::mutex _stateMutex;

void EvaluateAndApplyIfNeeded();
bool CoordinatesAreValid(const std::wstring& lat, const std::wstring& lon);
};
Loading
Loading