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  }