LightSwitchSettings.cpp
1 #include "LightSwitchSettings.h" 2 #include <common/utils/json.h> 3 #include <common/SettingsAPI/settings_helpers.h> 4 #include "SettingsObserver.h" 5 #include <filesystem> 6 #include <fstream> 7 #include <logger.h> 8 #include <LightSwitchService/trace.h> 9 10 using namespace std; 11 12 LightSwitchSettings& LightSwitchSettings::instance() 13 { 14 static LightSwitchSettings inst; 15 return inst; 16 } 17 18 LightSwitchSettings::LightSwitchSettings() 19 { 20 LoadSettings(); 21 } 22 23 std::wstring LightSwitchSettings::GetSettingsFileName() 24 { 25 return PTSettingsHelper::get_module_save_file_location(L"LightSwitch"); 26 } 27 28 void LightSwitchSettings::InitFileWatcher() 29 { 30 if (!m_settingsChangedEvent) 31 { 32 m_settingsChangedEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr); 33 } 34 35 if (!m_settingsFileWatcher) 36 { 37 m_settingsFileWatcher = std::make_unique<FileWatcher>( 38 GetSettingsFileName(), 39 [this]() { 40 using namespace std::chrono; 41 42 { 43 std::lock_guard<std::mutex> lock(m_debounceMutex); 44 m_lastChangeTime = steady_clock::now(); 45 if (m_debouncePending) 46 return; 47 m_debouncePending = true; 48 } 49 50 m_debounceThread = std::jthread([this](std::stop_token stop) { 51 using namespace std::chrono; 52 while (!stop.stop_requested()) 53 { 54 std::this_thread::sleep_for(seconds(3)); 55 56 auto elapsed = steady_clock::now() - m_lastChangeTime; 57 if (elapsed >= seconds(1)) 58 break; 59 } 60 61 { 62 std::lock_guard<std::mutex> lock(m_debounceMutex); 63 m_debouncePending = false; 64 } 65 66 Logger::info(L"[LightSwitchSettings] Settings file stabilized, reloading."); 67 68 try 69 { 70 LoadSettings(); 71 SetEvent(m_settingsChangedEvent); 72 } 73 catch (const std::exception& e) 74 { 75 std::wstring wmsg; 76 wmsg.assign(e.what(), e.what() + strlen(e.what())); 77 Logger::error(L"[LightSwitchSettings] Exception during debounced reload: {}", wmsg); 78 } 79 }); 80 }); 81 } 82 } 83 84 LightSwitchSettings::~LightSwitchSettings() 85 { 86 Logger::info(L"[LightSwitchSettings] Cleaning up settings resources..."); 87 88 // Stop and join the debounce thread (std::jthread auto-joins, but we can signal stop too) 89 if (m_debounceThread.joinable()) 90 { 91 m_debounceThread.request_stop(); 92 } 93 94 // Release the file watcher so it closes file handles and background threads 95 if (m_settingsFileWatcher) 96 { 97 m_settingsFileWatcher.reset(); 98 Logger::info(L"[LightSwitchSettings] File watcher stopped."); 99 } 100 101 // Close the Windows event handle 102 if (m_settingsChangedEvent) 103 { 104 CloseHandle(m_settingsChangedEvent); 105 m_settingsChangedEvent = nullptr; 106 Logger::info(L"[LightSwitchSettings] Settings changed event closed."); 107 } 108 109 Logger::info(L"[LightSwitchSettings] Cleanup complete."); 110 } 111 112 113 void LightSwitchSettings::AddObserver(SettingsObserver& observer) 114 { 115 m_observers.insert(&observer); 116 } 117 118 void LightSwitchSettings::RemoveObserver(SettingsObserver& observer) 119 { 120 m_observers.erase(&observer); 121 } 122 123 void LightSwitchSettings::NotifyObservers(SettingId id) const 124 { 125 for (auto observer : m_observers) 126 { 127 if (observer->WantsToBeNotified(id)) 128 { 129 observer->SettingsUpdate(id); 130 } 131 } 132 } 133 134 HANDLE LightSwitchSettings::GetSettingsChangedEvent() const 135 { 136 return m_settingsChangedEvent; 137 } 138 139 void LightSwitchSettings::LoadSettings() 140 { 141 std::lock_guard<std::mutex> guard(m_settingsMutex); 142 try 143 { 144 PowerToysSettings::PowerToyValues values = 145 PowerToysSettings::PowerToyValues::load_from_settings_file(L"LightSwitch"); 146 147 148 if (const auto jsonVal = values.get_string_value(L"scheduleMode")) 149 { 150 auto val = *jsonVal; 151 auto newMode = FromString(val); 152 if (m_settings.scheduleMode != newMode) 153 { 154 m_settings.scheduleMode = newMode; 155 Trace::LightSwitch::ScheduleModeToggled(val); 156 NotifyObservers(SettingId::ScheduleMode); 157 } 158 } 159 160 // Latitude 161 if (const auto jsonVal = values.get_string_value(L"latitude")) 162 { 163 auto val = *jsonVal; 164 if (m_settings.latitude != val) 165 { 166 m_settings.latitude = val; 167 NotifyObservers(SettingId::Latitude); 168 } 169 } 170 171 // Longitude 172 if (const auto jsonVal = values.get_string_value(L"longitude")) 173 { 174 auto val = *jsonVal; 175 if (m_settings.longitude != val) 176 { 177 m_settings.longitude = val; 178 NotifyObservers(SettingId::Longitude); 179 } 180 } 181 182 // LightTime 183 if (const auto jsonVal = values.get_int_value(L"lightTime")) 184 { 185 auto val = *jsonVal; 186 if (m_settings.lightTime != val) 187 { 188 m_settings.lightTime = val; 189 NotifyObservers(SettingId::LightTime); 190 } 191 } 192 193 // DarkTime 194 if (const auto jsonVal = values.get_int_value(L"darkTime")) 195 { 196 auto val = *jsonVal; 197 if (m_settings.darkTime != val) 198 { 199 m_settings.darkTime = val; 200 NotifyObservers(SettingId::DarkTime); 201 } 202 } 203 204 // Offset 205 if (const auto jsonVal = values.get_int_value(L"sunrise_offset")) 206 { 207 auto val = *jsonVal; 208 if (m_settings.sunrise_offset != val) 209 { 210 m_settings.sunrise_offset = val; 211 NotifyObservers(SettingId::Sunrise_Offset); 212 } 213 } 214 215 if (const auto jsonVal = values.get_int_value(L"sunset_offset")) 216 { 217 auto val = *jsonVal; 218 if (m_settings.sunset_offset != val) 219 { 220 m_settings.sunset_offset = val; 221 NotifyObservers(SettingId::Sunset_Offset); 222 } 223 } 224 225 bool themeTargetChanged = false; 226 227 // ChangeSystem 228 if (const auto jsonVal = values.get_bool_value(L"changeSystem")) 229 { 230 auto val = *jsonVal; 231 if (m_settings.changeSystem != val) 232 { 233 m_settings.changeSystem = val; 234 themeTargetChanged = true; 235 NotifyObservers(SettingId::ChangeSystem); 236 } 237 } 238 239 // ChangeApps 240 if (const auto jsonVal = values.get_bool_value(L"changeApps")) 241 { 242 auto val = *jsonVal; 243 if (m_settings.changeApps != val) 244 { 245 m_settings.changeApps = val; 246 themeTargetChanged = true; 247 NotifyObservers(SettingId::ChangeApps); 248 } 249 } 250 251 // EnableDarkModeProfile 252 if (const auto jsonVal = values.get_bool_value(L"enableDarkModeProfile")) 253 { 254 auto val = *jsonVal; 255 if (m_settings.enableDarkModeProfile != val) 256 { 257 m_settings.enableDarkModeProfile = val; 258 } 259 } 260 261 // EnableLightModeProfile 262 if (const auto jsonVal = values.get_bool_value(L"enableLightModeProfile")) 263 { 264 auto val = *jsonVal; 265 if (m_settings.enableLightModeProfile != val) 266 { 267 m_settings.enableLightModeProfile = val; 268 } 269 } 270 271 // DarkModeProfile 272 if (const auto jsonVal = values.get_string_value(L"darkModeProfile")) 273 { 274 auto val = *jsonVal; 275 if (m_settings.darkModeProfile != val) 276 { 277 m_settings.darkModeProfile = val; 278 } 279 } 280 281 // LightModeProfile 282 if (const auto jsonVal = values.get_string_value(L"lightModeProfile")) 283 { 284 auto val = *jsonVal; 285 if (m_settings.lightModeProfile != val) 286 { 287 m_settings.lightModeProfile = val; 288 } 289 } 290 291 // For ChangeSystem/ChangeApps changes, log telemetry 292 if (themeTargetChanged) 293 { 294 Trace::LightSwitch::ThemeTargetChanged(m_settings.changeApps, m_settings.changeSystem); 295 } 296 } 297 catch (...) 298 { 299 // Keeps defaults if load fails 300 } 301 }