dllmain.cpp
1 #include "pch.h" 2 #include <interface/powertoy_module_interface.h> 3 #include "trace.h" 4 #include <common/logger/logger.h> 5 #include <common/SettingsAPI/settings_objects.h> 6 #include <common/SettingsAPI/settings_helpers.h> 7 #include <locale> 8 #include <codecvt> 9 #include <common/utils/logger_helper.h> 10 #include "ThemeHelper.h" 11 #include <thread> 12 #include <atomic> 13 14 extern "C" IMAGE_DOS_HEADER __ImageBase; 15 16 namespace 17 { 18 const wchar_t JSON_KEY_PROPERTIES[] = L"properties"; 19 const wchar_t JSON_KEY_WIN[] = L"win"; 20 const wchar_t JSON_KEY_ALT[] = L"alt"; 21 const wchar_t JSON_KEY_CTRL[] = L"ctrl"; 22 const wchar_t JSON_KEY_SHIFT[] = L"shift"; 23 const wchar_t JSON_KEY_CODE[] = L"code"; 24 const wchar_t JSON_KEY_TOGGLE_THEME_HOTKEY[] = L"toggle-theme-hotkey"; 25 const wchar_t JSON_KEY_VALUE[] = L"value"; 26 } 27 28 BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) 29 { 30 switch (ul_reason_for_call) 31 { 32 case DLL_PROCESS_ATTACH: 33 Trace::RegisterProvider(); 34 break; 35 case DLL_THREAD_ATTACH: 36 case DLL_THREAD_DETACH: 37 break; 38 case DLL_PROCESS_DETACH: 39 Trace::UnregisterProvider(); 40 break; 41 } 42 return TRUE; 43 } 44 45 // The PowerToy name that will be shown in the settings. 46 const static wchar_t* MODULE_NAME = L"LightSwitch"; 47 // Add a description that will we shown in the module settings page. 48 const static wchar_t* MODULE_DESC = L"This is a module that allows you to control light/dark theming via set times, sun rise, or directly invoking the change."; 49 50 enum class ScheduleMode 51 { 52 Off, 53 FixedHours, 54 SunsetToSunrise, 55 FollowNightLight, 56 // add more later 57 }; 58 59 inline std::wstring ToString(ScheduleMode mode) 60 { 61 switch (mode) 62 { 63 case ScheduleMode::SunsetToSunrise: 64 return L"SunsetToSunrise"; 65 case ScheduleMode::FixedHours: 66 return L"FixedHours"; 67 case ScheduleMode::FollowNightLight: 68 return L"FollowNightLight"; 69 default: 70 return L"Off"; 71 } 72 } 73 74 inline ScheduleMode FromString(const std::wstring& str) 75 { 76 if (str == L"SunsetToSunrise") 77 return ScheduleMode::SunsetToSunrise; 78 if (str == L"FixedHours") 79 return ScheduleMode::FixedHours; 80 if (str == L"FollowNightLight") 81 return ScheduleMode::FollowNightLight; 82 return ScheduleMode::Off; 83 } 84 85 // These are the properties shown in the Settings page. 86 struct ModuleSettings 87 { 88 bool m_changeSystem = true; 89 bool m_changeApps = true; 90 ScheduleMode m_scheduleMode = ScheduleMode::Off; 91 int m_lightTime = 480; 92 int m_darkTime = 1200; 93 int m_sunrise_offset = 0; 94 int m_sunset_offset = 0; 95 std::wstring m_latitude = L"0.0"; 96 std::wstring m_longitude = L"0.0"; 97 } g_settings; 98 99 class LightSwitchInterface : public PowertoyModuleIface 100 { 101 private: 102 bool m_enabled = false; 103 104 HANDLE m_process{ nullptr }; 105 HANDLE m_force_light_event_handle; 106 HANDLE m_force_dark_event_handle; 107 HANDLE m_manual_override_event_handle; 108 HANDLE m_toggle_event_handle{ nullptr }; 109 std::thread m_toggle_thread; 110 std::atomic<bool> m_toggle_thread_running{ false }; 111 112 static const constexpr int NUM_DEFAULT_HOTKEYS = 4; 113 114 Hotkey m_toggle_theme_hotkey = { .win = true, .ctrl = true, .shift = true, .alt = false, .key = 'D' }; 115 116 void init_settings(); 117 void ToggleTheme(); 118 void StartToggleListener(); 119 void StopToggleListener(); 120 121 public: 122 LightSwitchInterface() 123 { 124 LoggerHelpers::init_logger(L"LightSwitch", L"ModuleInterface", LogSettings::lightSwitchLoggerName); 125 126 m_force_light_event_handle = CreateDefaultEvent(L"POWERTOYS_LIGHTSWITCH_FORCE_LIGHT"); 127 m_force_dark_event_handle = CreateDefaultEvent(L"POWERTOYS_LIGHTSWITCH_FORCE_DARK"); 128 m_manual_override_event_handle = CreateEventW(nullptr, TRUE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE"); 129 m_toggle_event_handle = CreateDefaultEvent(L"Local\\PowerToys-LightSwitch-ToggleEvent-d8dc2f29-8c94-4ca1-8c5f-3e2b1e3c4f5a"); 130 131 init_settings(); 132 }; 133 134 virtual const wchar_t* get_key() override 135 { 136 return L"LightSwitch"; 137 } 138 139 // Destroy the powertoy and free memory 140 virtual void destroy() override 141 { 142 // Ensure worker threads/process handles are cleaned up before destruction 143 disable(); 144 delete this; 145 } 146 147 // Return the display name of the powertoy, this will be cached by the runner 148 virtual const wchar_t* get_name() override 149 { 150 return MODULE_NAME; 151 } 152 153 // Return the configured status for the gpo policy for the module 154 virtual powertoys_gpo::gpo_rule_configured_t gpo_policy_enabled_configuration() override 155 { 156 return powertoys_gpo::getConfiguredLightSwitchEnabledValue(); 157 } 158 159 // Return JSON with the configuration options. 160 virtual bool get_config(wchar_t* buffer, int* buffer_size) override 161 { 162 HINSTANCE hinstance = reinterpret_cast<HINSTANCE>(&__ImageBase); 163 164 // Create a Settings object with your module name 165 PowerToysSettings::Settings settings(hinstance, get_name()); 166 settings.set_description(MODULE_DESC); 167 settings.set_overview_link(L"https://aka.ms/powertoys"); 168 169 // Boolean toggles 170 settings.add_bool_toggle( 171 L"changeSystem", 172 L"Change System Theme", 173 g_settings.m_changeSystem); 174 175 settings.add_bool_toggle( 176 L"changeApps", 177 L"Change Apps Theme", 178 g_settings.m_changeApps); 179 180 settings.add_choice_group( 181 L"scheduleMode", 182 L"Theme schedule mode", 183 ToString(g_settings.m_scheduleMode), 184 { { L"Off", L"Disable the schedule" }, 185 { L"FixedHours", L"Set hours manually" }, 186 { L"SunsetToSunrise", L"Use sunrise/sunset times" }, 187 { L"FollowNightLight", L"Follow Windows Night Light state" } 188 }); 189 190 // Integer spinners 191 settings.add_int_spinner( 192 L"lightTime", 193 L"Time to switch to light theme (minutes after midnight).", 194 g_settings.m_lightTime, 195 0, 196 1439, 197 1); 198 199 settings.add_int_spinner( 200 L"darkTime", 201 L"Time to switch to dark theme (minutes after midnight).", 202 g_settings.m_darkTime, 203 0, 204 1439, 205 1); 206 207 settings.add_int_spinner( 208 L"sunrise_offset", 209 L"Time to offset turning on your light theme.", 210 g_settings.m_sunrise_offset, 211 0, 212 1439, 213 1); 214 215 settings.add_int_spinner( 216 L"sunset_offset", 217 L"Time to offset turning on your dark theme.", 218 g_settings.m_sunset_offset, 219 0, 220 1439, 221 1); 222 223 // Strings for latitude and longitude 224 settings.add_string( 225 L"latitude", 226 L"Your latitude in decimal degrees (e.g. 39.95).", 227 g_settings.m_latitude); 228 229 settings.add_string( 230 L"longitude", 231 L"Your longitude in decimal degrees (e.g. -75.16).", 232 g_settings.m_longitude); 233 234 // One-shot actions (buttons) 235 settings.add_custom_action( 236 L"forceLight", 237 L"Switch immediately to light theme", 238 L"Force Light", 239 L"{}"); 240 241 settings.add_custom_action( 242 L"forceDark", 243 L"Switch immediately to dark theme", 244 L"Force Dark", 245 L"{}"); 246 247 // Hotkeys 248 PowerToysSettings::HotkeyObject dm_hk = PowerToysSettings::HotkeyObject::from_settings( 249 m_toggle_theme_hotkey.win, 250 m_toggle_theme_hotkey.ctrl, 251 m_toggle_theme_hotkey.alt, 252 m_toggle_theme_hotkey.shift, 253 m_toggle_theme_hotkey.key); 254 255 settings.add_hotkey( 256 L"toggle-theme-hotkey", 257 L"Shortcut to toggle theme immediately", 258 dm_hk); 259 260 // Serialize to buffer for the PowerToys runner 261 return settings.serialize_to_buffer(buffer, buffer_size); 262 } 263 264 // Signal from the Settings editor to call a custom action. 265 // This can be used to spawn more complex editors. 266 void call_custom_action(const wchar_t* action) override 267 { 268 try 269 { 270 auto action_object = PowerToysSettings::CustomActionObject::from_json_string(action); 271 272 if (action_object.get_name() == L"forceLight") 273 { 274 Logger::info(L"[Light Switch] Custom action triggered: Force Light"); 275 SetSystemTheme(true); 276 SetAppsTheme(true); 277 } 278 else if (action_object.get_name() == L"forceDark") 279 { 280 Logger::info(L"[Light Switch] Custom action triggered: Force Dark"); 281 SetSystemTheme(false); 282 SetAppsTheme(false); 283 } 284 } 285 catch (...) 286 { 287 Logger::error(L"[Light Switch] Invalid custom action JSON"); 288 } 289 } 290 291 // Called by the runner to pass the updated settings values as a serialized JSON. 292 virtual void set_config(const wchar_t* config) override 293 { 294 try 295 { 296 auto values = PowerToysSettings::PowerToyValues::from_json_string(config, get_key()); 297 298 parse_hotkey(values); 299 300 if (auto v = values.get_bool_value(L"changeSystem")) 301 { 302 g_settings.m_changeSystem = *v; 303 } 304 305 if (auto v = values.get_bool_value(L"changeApps")) 306 { 307 g_settings.m_changeApps = *v; 308 } 309 310 auto previousMode = g_settings.m_scheduleMode; 311 312 if (auto v = values.get_string_value(L"scheduleMode")) 313 { 314 auto newMode = FromString(*v); 315 if (newMode != g_settings.m_scheduleMode) 316 { 317 Logger::info(L"[LightSwitchInterface] Schedule mode changed from {} to {}", 318 ToString(g_settings.m_scheduleMode), 319 ToString(newMode)); 320 g_settings.m_scheduleMode = newMode; 321 322 start_service_if_needed(); 323 } 324 } 325 326 if (auto v = values.get_int_value(L"lightTime")) 327 { 328 g_settings.m_lightTime = *v; 329 } 330 331 if (auto v = values.get_int_value(L"darkTime")) 332 { 333 g_settings.m_darkTime = *v; 334 } 335 336 if (auto v = values.get_int_value(L"sunrise_offset")) 337 { 338 g_settings.m_sunrise_offset = *v; 339 } 340 341 if (auto v = values.get_int_value(L"sunset_offset")) 342 { 343 g_settings.m_sunset_offset = *v; 344 } 345 346 if (auto v = values.get_string_value(L"latitude")) 347 { 348 g_settings.m_latitude = *v; 349 } 350 if (auto v = values.get_string_value(L"longitude")) 351 { 352 g_settings.m_longitude = *v; 353 } 354 355 values.save_to_settings_file(); 356 } 357 catch (const std::exception&) 358 { 359 Logger::error("[Light Switch] set_config: Failed to parse or apply config."); 360 } 361 } 362 363 virtual void start_service_if_needed() 364 { 365 if (!m_process || WaitForSingleObject(m_process, 0) != WAIT_TIMEOUT) 366 { 367 Logger::info(L"[LightSwitchInterface] Starting LightSwitchService due to active schedule mode."); 368 enable(); 369 } 370 else 371 { 372 Logger::debug(L"[LightSwitchInterface] Service already running, skipping start."); 373 } 374 } 375 376 /*virtual void stop_worker_only() 377 { 378 if (m_process) 379 { 380 Logger::info(L"[LightSwitchInterface] Stopping LightSwitchService (worker only)."); 381 constexpr DWORD timeout_ms = 1500; 382 DWORD result = WaitForSingleObject(m_process, timeout_ms); 383 384 if (result == WAIT_TIMEOUT) 385 { 386 Logger::warn("Light Switch: Process didn't exit in time. Forcing termination."); 387 TerminateProcess(m_process, 0); 388 } 389 390 CloseHandle(m_process); 391 m_process = nullptr; 392 } 393 }*/ 394 395 /*virtual void stop_service_if_running() 396 { 397 if (m_process) 398 { 399 Logger::info(L"[LightSwitchInterface] Stopping LightSwitchService due to schedule OFF."); 400 stop_worker_only(); 401 } 402 }*/ 403 404 virtual void enable() 405 { 406 m_enabled = true; 407 Logger::info(L"Enabling Light Switch module..."); 408 Trace::Enable(true); 409 410 unsigned long powertoys_pid = GetCurrentProcessId(); 411 std::wstring args = L"--pid " + std::to_wstring(powertoys_pid); 412 std::wstring exe_name = L"LightSwitchService\\PowerToys.LightSwitchService.exe"; 413 414 std::wstring resolved_path(MAX_PATH, L'\0'); 415 DWORD result = SearchPathW( 416 nullptr, 417 exe_name.c_str(), 418 nullptr, 419 static_cast<DWORD>(resolved_path.size()), 420 resolved_path.data(), 421 nullptr); 422 423 if (result == 0 || result >= resolved_path.size()) 424 { 425 Logger::error( 426 L"Failed to locate Light Switch executable named '{}' at location '{}'", 427 exe_name, 428 resolved_path.c_str()); 429 return; 430 } 431 432 resolved_path.resize(result); 433 Logger::debug(L"Resolved executable path: {}", resolved_path); 434 435 std::wstring command_line = L"\"" + resolved_path + L"\" " + args; 436 437 STARTUPINFO si = { sizeof(si) }; 438 PROCESS_INFORMATION pi; 439 440 if (!CreateProcessW( 441 resolved_path.c_str(), 442 command_line.data(), 443 nullptr, 444 nullptr, 445 TRUE, 446 0, 447 nullptr, 448 nullptr, 449 &si, 450 &pi)) 451 { 452 Logger::error(L"Failed to launch Light Switch process. {}", get_last_error_or_default(GetLastError())); 453 return; 454 } 455 456 Logger::info(L"Light Switch process launched successfully (PID: {}).", pi.dwProcessId); 457 m_process = pi.hProcess; 458 CloseHandle(pi.hThread); 459 460 StartToggleListener(); 461 } 462 463 // Disable the powertoy 464 virtual void disable() 465 { 466 Logger::info("Light Switch disabling"); 467 m_enabled = false; 468 469 if (m_process) 470 { 471 constexpr DWORD timeout_ms = 1500; 472 DWORD result = WaitForSingleObject(m_process, timeout_ms); 473 474 if (result == WAIT_TIMEOUT) 475 { 476 Logger::warn("Light Switch: Process didn't exit in time. Forcing termination."); 477 TerminateProcess(m_process, 0); 478 } 479 480 CloseHandle(m_manual_override_event_handle); 481 m_manual_override_event_handle = nullptr; 482 483 CloseHandle(m_process); 484 m_process = nullptr; 485 } 486 487 Trace::Enable(false); 488 StopToggleListener(); 489 } 490 491 // Returns if the powertoys is enabled 492 virtual bool is_enabled() override 493 { 494 return m_enabled; 495 } 496 497 // Returns whether the PowerToys should be enabled by default 498 virtual bool is_enabled_by_default() const override 499 { 500 return false; 501 } 502 503 void parse_hotkey(PowerToysSettings::PowerToyValues& settings) 504 { 505 auto settingsObject = settings.get_raw_json(); 506 if (settingsObject.GetView().Size()) 507 { 508 try 509 { 510 Hotkey _temp_toggle_theme; 511 auto jsonHotkeyObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_TOGGLE_THEME_HOTKEY).GetNamedObject(JSON_KEY_VALUE); 512 _temp_toggle_theme.win = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_WIN); 513 _temp_toggle_theme.alt = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_ALT); 514 _temp_toggle_theme.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT); 515 _temp_toggle_theme.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL); 516 _temp_toggle_theme.key = static_cast<unsigned char>(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE)); 517 m_toggle_theme_hotkey = _temp_toggle_theme; 518 } 519 catch (...) 520 { 521 Logger::error("Failed to initialize Light Switch force dark mode shortcut from settings. Value will keep unchanged."); 522 } 523 } 524 else 525 { 526 Logger::info("Light Switch settings are empty"); 527 } 528 } 529 530 virtual size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size) override 531 { 532 if (hotkeys && buffer_size >= 1) 533 { 534 hotkeys[0] = m_toggle_theme_hotkey; 535 } 536 return 1; 537 } 538 539 virtual bool on_hotkey(size_t hotkeyId) override 540 { 541 if (m_enabled) 542 { 543 Logger::trace(L"Light Switch hotkey pressed"); 544 Trace::ShortcutInvoked(); 545 546 if (!is_process_running()) 547 { 548 enable(); 549 } 550 else if (hotkeyId == 0) 551 { 552 Logger::info(L"[Light Switch] Hotkey triggered: Toggle Theme"); 553 ToggleTheme(); 554 } 555 556 return true; 557 } 558 559 return false; 560 } 561 562 bool is_process_running() 563 { 564 return WaitForSingleObject(m_process, 0) == WAIT_TIMEOUT; 565 } 566 567 }; 568 569 void LightSwitchInterface::ToggleTheme() 570 { 571 if (g_settings.m_changeSystem) 572 { 573 SetSystemTheme(!GetCurrentSystemTheme()); 574 } 575 if (g_settings.m_changeApps) 576 { 577 SetAppsTheme(!GetCurrentAppsTheme()); 578 } 579 580 if (!m_manual_override_event_handle) 581 { 582 m_manual_override_event_handle = OpenEventW(SYNCHRONIZE | EVENT_MODIFY_STATE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE"); 583 if (!m_manual_override_event_handle) 584 { 585 m_manual_override_event_handle = CreateEventW(nullptr, TRUE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE"); 586 } 587 } 588 589 if (m_manual_override_event_handle) 590 { 591 SetEvent(m_manual_override_event_handle); 592 Logger::debug(L"[Light Switch] Manual override event set"); 593 } 594 } 595 596 void LightSwitchInterface::StartToggleListener() 597 { 598 if (m_toggle_thread_running || !m_toggle_event_handle) 599 { 600 return; 601 } 602 603 m_toggle_thread_running = true; 604 m_toggle_thread = std::thread([this]() { 605 while (m_toggle_thread_running) 606 { 607 const DWORD wait_result = WaitForSingleObject(m_toggle_event_handle, 500); 608 if (!m_toggle_thread_running) 609 { 610 break; 611 } 612 613 if (wait_result == WAIT_OBJECT_0) 614 { 615 ToggleTheme(); 616 ResetEvent(m_toggle_event_handle); 617 } 618 } 619 }); 620 } 621 622 void LightSwitchInterface::StopToggleListener() 623 { 624 if (!m_toggle_thread_running) 625 { 626 return; 627 } 628 629 m_toggle_thread_running = false; 630 if (m_toggle_event_handle) 631 { 632 SetEvent(m_toggle_event_handle); 633 } 634 if (m_toggle_thread.joinable()) 635 { 636 m_toggle_thread.join(); 637 } 638 } 639 640 std::wstring utf8_to_wstring(const std::string& str) 641 { 642 if (str.empty()) 643 return std::wstring(); 644 645 int size_needed = MultiByteToWideChar( 646 CP_UTF8, 647 0, 648 str.c_str(), 649 static_cast<int>(str.size()), 650 nullptr, 651 0); 652 653 std::wstring wstr(size_needed, 0); 654 655 MultiByteToWideChar( 656 CP_UTF8, 657 0, 658 str.c_str(), 659 static_cast<int>(str.size()), 660 &wstr[0], 661 size_needed); 662 663 return wstr; 664 } 665 666 // Load the settings file. 667 void LightSwitchInterface::init_settings() 668 { 669 Logger::info(L"[Light Switch] init_settings: starting to load settings for module"); 670 671 try 672 { 673 PowerToysSettings::PowerToyValues settings = 674 PowerToysSettings::PowerToyValues::load_from_settings_file(get_name()); 675 676 parse_hotkey(settings); 677 678 if (auto v = settings.get_bool_value(L"changeSystem")) 679 g_settings.m_changeSystem = *v; 680 if (auto v = settings.get_bool_value(L"changeApps")) 681 g_settings.m_changeApps = *v; 682 if (auto v = settings.get_string_value(L"scheduleMode")) 683 g_settings.m_scheduleMode = FromString(*v); 684 if (auto v = settings.get_int_value(L"lightTime")) 685 g_settings.m_lightTime = *v; 686 if (auto v = settings.get_int_value(L"darkTime")) 687 g_settings.m_darkTime = *v; 688 if (auto v = settings.get_int_value(L"sunrise_offset")) 689 g_settings.m_sunrise_offset = *v; 690 if (auto v = settings.get_int_value(L"sunset_offset")) 691 g_settings.m_sunset_offset = *v; 692 if (auto v = settings.get_string_value(L"latitude")) 693 g_settings.m_latitude = *v; 694 if (auto v = settings.get_string_value(L"longitude")) 695 g_settings.m_longitude = *v; 696 697 Logger::info(L"[Light Switch] init_settings: loaded successfully"); 698 } 699 catch (const winrt::hresult_error& e) 700 { 701 Logger::error(L"[Light Switch] init_settings: hresult_error 0x{:08X} - {}", e.code(), e.message().c_str()); 702 } 703 catch (const std::exception& e) 704 { 705 std::wstring whatStr = utf8_to_wstring(e.what()); 706 Logger::error(L"[Light Switch] init_settings: std::exception - {}", whatStr); 707 } 708 catch (...) 709 { 710 Logger::error(L"[Light Switch] init_settings: unknown exception while loading settings"); 711 } 712 } 713 714 extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create() 715 { 716 return new LightSwitchInterface(); 717 }