[Awake]Refactor and update version - DAISY023_04102024 (#32378)

Improves the following:

- Consolidates different code paths for easier maintenance.
- Removes the dependency on Windows Forms and creates the system tray
icon and handling through native Win32 APIs (massive thank you to
@BrianPeek for helping write the window creation logic and diagnosing
threading issues).
- Changing modes in Awake now triggers icon changes in the tray
(#11996). Massive thank you to @niels9001 for creating the icons.

Fixes the following:

- When in the UI and you select `0` as hours and `0` as minutes in
`TIMED` awake mode, the UI becomes non-responsive whenever you try to
get back to timed after it rolls back to `PASSIVE`. (#33630)
- Adds the option to keep track of Awake state through tray tooltip.
(#12714)

---------

Co-authored-by: Clint Rutkas <clint@rutkas.com>
Co-authored-by: Jaime Bernardo <jaime@janeasystems.com>
This commit is contained in:
Den Delimarsky 2024-07-25 09:09:17 -07:00 committed by GitHub
parent 63625a1cee
commit 1be3b6c087
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 1054 additions and 640 deletions

View File

@ -221,7 +221,6 @@ comdlg
comexp
cominterop
commandline
COMMANDTITLE
commctrl
commdlg
compmgmt
@ -430,8 +429,8 @@ ENDSESSION
ENTERSIZEMOVE
ENU
EOAC
epu
EPO
epu
ERASEBKGND
EREOF
EResize
@ -486,7 +485,6 @@ FILEFLAGSMASK
FILELOCKSMITH
FILELOCKSMITHCONTEXTMENU
FILELOCKSMITHEXT
FILELOCKSMITHLIB
FILELOCKSMITHLIBINTEROP
FILEMUSTEXIST
FILEOP
@ -502,6 +500,7 @@ findfast
FIXEDFILEINFO
flac
flyouts
FMask
FOF
FOFX
FOLDERID
@ -523,7 +522,6 @@ GCLP
gdi
gdiplus
GDISCALED
gdnbaselines
GEmoji
GETCLIENTAREAANIMATION
GETDESKWALLPAPER
@ -543,7 +541,6 @@ gpedit
gpo
GPOCA
gpp
GPT
gpu
GSM
gtm
@ -703,7 +700,6 @@ INSTALLSTARTMENUSHORTCUT
INSTALLSTATE
Inste
Intelli
interactable
Interlop
INTRESOURCE
INVALIDARG
@ -732,7 +728,6 @@ IWeb
IWIC
iwr
IYUV
JArray
jfi
jfif
jgeosdfsdsgmkedfgdfgdfgbkmhcgcflmi
@ -764,7 +759,6 @@ KILLFOCUS
killrunner
kmph
Knownfolders
ksh
KSPROPERTY
Kybd
languagesjson
@ -800,7 +794,6 @@ LOADFROMFILE
LOBYTE
LOCALDISPLAY
LOCALPACKAGE
localport
LOCALSYSTEM
LOCATIONCHANGE
LOGFONT
@ -814,6 +807,7 @@ LOWORD
lparam
LPBITMAPINFOHEADER
LPCITEMIDLIST
lpcmi
LPCMINVOKECOMMANDINFO
LPCREATESTRUCT
LPCRECT
@ -838,6 +832,7 @@ lptpm
LPTR
LPTSTR
LPW
lpwcx
lpwndpl
LReader
LRESULT
@ -855,10 +850,10 @@ lwin
LZero
majortype
makecab
MAKELANGID
MAKEINTRESOURCE
MAKEINTRESOURCEA
MAKEINTRESOURCEW
MAKELANGID
makepri
manifestdependency
MAPPEDTOSAMEKEY
@ -878,7 +873,6 @@ mdwn
MEDIASUBTYPE
mediatype
mef
MENUBREAK
MENUITEMINFO
MENUITEMINFOW
MERGECOPY
@ -1041,6 +1035,7 @@ NOSEARCH
NOSENDCHANGING
NOSIZE
NOTIFICATIONSDLL
NOTIFYICONDATA
NOTIFYICONDATAW
NOTIMPL
notlike
@ -1064,7 +1059,6 @@ numberbox
nwc
Objbase
objidl
occurrence
ocr
Ocrsettings
odbccp
@ -1078,7 +1072,6 @@ oldtheme
oleaut
OLECHAR
onebranch
OOBEPT
opencode
OPENFILENAME
opensource
@ -1102,7 +1095,6 @@ OVERLAPPEDWINDOW
overlaywindow
Oversampling
OWNDC
OWNERDRAW
Packagemanager
PACL
PAINTSTRUCT
@ -1116,7 +1108,6 @@ PARTIALCONFIRMATIONDIALOGTITLE
PATCOPY
pathcch
PATHMUSTEXIST
Pathto
PATINVERT
PATPAINT
PAUDIO
@ -1165,6 +1156,7 @@ ploca
plocm
pluginsmodel
PMSIHANDLE
pnid
Pnp
Popups
POPUPWINDOW
@ -1257,8 +1249,6 @@ QUERYENDSESSION
QUERYOPEN
QUEUESYNC
QUNS
qwertyuiopasdfghjklzxcvbnm
qwrtyuiopsghjklzxvnm
raf
RAII
RAlt
@ -1275,7 +1265,6 @@ RECTDESTINATION
rectp
RECTSOURCE
recyclebin
redirectedfrom
Redist
redistributable
reencode
@ -1357,8 +1346,6 @@ runas
rundll
rungameid
RUNLEVEL
runsettings
runspace
runtimeclass
runtimeobject
runtimepack
@ -1592,7 +1579,6 @@ TDevice
telem
telephon
templatenamespace
testhost
testprocess
TEXCOORD
TEXTEXTRACTOR
@ -1617,7 +1603,6 @@ tlb
tlbimp
TMPVAR
TNP
toggleswitch
Toolhelp
toolkitconverters
Toolset
@ -1647,9 +1632,11 @@ TYPESHORTCUT
UAC
UAL
uap
UCallback
udit
uefi
uesc
UFlags
UHash
UIA
UIEx

View File

@ -85,7 +85,7 @@
<UsePrecompiledHeaders Condition="'$(TF_BUILD)' != ''">false</UsePrecompiledHeaders>
<!-- Change this to bust the cache -->
<MSBuildCacheCacheUniverse Condition="'$(MSBuildCacheCacheUniverse)' == ''">202407100737</MSBuildCacheCacheUniverse>
<MSBuildCacheCacheUniverse Condition="'$(MSBuildCacheCacheUniverse)' == ''">202407200737</MSBuildCacheCacheUniverse>
<!--
Visual Studio telemetry reads various ApplicationInsights.config files and other files after the project is finished, likely in a detached process.

View File

@ -79,7 +79,7 @@
<PackageVersion Include="System.IO.Abstractions" Version="17.2.3" />
<PackageVersion Include="System.IO.Abstractions.TestingHelpers" Version="17.2.3" />
<PackageVersion Include="System.Management" Version="8.0.0" />
<PackageVersion Include="System.Reactive" Version="6.0.0-preview.9" />
<PackageVersion Include="System.Reactive" Version="6.0.1" />
<PackageVersion Include="System.Runtime.Caching" Version="8.0.0" />
<!-- Package System.Security.Cryptography.ProtectedData added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.1. This is a dependency of System.Data.OleDb but the 8.0.1 version wasn't published to nuget. -->
<PackageVersion Include="System.Security.Cryptography.ProtectedData" Version="8.0.0" />

View File

@ -1359,7 +1359,7 @@ EXHIBIT A -Mozilla Public License.
- System.IO.Abstractions 17.2.3
- System.IO.Abstractions.TestingHelpers 17.2.3
- System.Management 8.0.0
- System.Reactive 6.0.0-preview.9
- System.Reactive 6.0.1
- System.Runtime.Caching 8.0.0
- System.Security.Cryptography.ProtectedData 8.0.0
- System.ServiceProcess.ServiceController 8.0.0

View File

@ -1,21 +1,34 @@
---
last-update: 3-20-2022
last-update: 7-16-2024
---
# PowerToys Awake Changelog
## Builds
The build ID can be found in `Program.cs` in the `BuildId` variable - it is a unique identifier for the current builds that allows better diagnostics (we can look up the build ID from the logs) and offers a way to triage Awake-specific issues faster independent of the PowerToys version. The build ID does not carry any significance beyond that within the PowerToys code base.
The build ID can be found in `Core\Constants.cs` in the `BuildId` variable - it is a unique identifier for the current builds that allows better diagnostics (we can look up the build ID from the logs) and offers a way to triage Awake-specific issues faster independent of the PowerToys version. The build ID does not carry any significance beyond that within the PowerToys code base.
The build ID moniker is made up of two components - a reference to a [Halo](https://en.wikipedia.org/wiki/Halo_(franchise)) character, and the date when the work on the specific build started in the format of `MMDDYYYY`.
| Build ID | Build Date |
|:----------------------------------------------------------|:-----------------|
| [`DAISY023_04102024`](#DAISY023_04102024-april-10-2024) | April 10, 2024 |
| [`ATRIOX_04132023`](#ATRIOX_04132023-april-13-2023) | April 13, 2023 |
| [`LIBRARIAN_03202022`](#librarian_03202022-march-20-2022) | March 20, 2022 |
| `ARBITER_01312022` | January 31, 2022 |
### `DAISY023_04102024` (April 10, 2024)
>[!NOTE]
>See pull request: [Awake Update - `DAISY023_04102024`](https://github.com/microsoft/PowerToys/pull/32378)
- [#33630](https://github.com/microsoft/PowerToys/issues/33630) When in the UI and you select `0` as hours and `0` as minutes in `TIMED` awake mode, the UI becomes non-responsive whenever you try to get back to timed after it rolls back to `PASSIVE`.
- [#12714](https://github.com/microsoft/PowerToys/issues/12714) Adds the option to keep track of Awake state through tray tooltip.
- [#11996](https://github.com/microsoft/PowerToys/issues/11996) Adds custom icons support for mode changes in Awake.
- Removes the dependency on `System.Windows.Forms` and instead uses native Windows APIs to create the tray icon.
- Removes redundant/unused code that impacted application performance.
- Updates dependent packages to their latest versions (`Microsoft.Windows.CsWinRT` and `System.Reactive`).
### `ATRIOX_04132023` (April 13, 2023)
- Moves from using `Task.Run` to spin up threads to actually using a blocking queue that properly sets thread parameters on the same thread.

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

View File

@ -10,7 +10,7 @@
<Nullable>enable</Nullable>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<UseWindowsForms>true</UseWindowsForms>
<UseWindowsForms>False</UseWindowsForms>
<!--Per documentation: https://learn.microsoft.com/dotnet/core/compatibility/windows-forms/5.0/automatically-infer-winexe-output-type#outputtype-set-to-winexe-for-wpf-and-winforms-apps -->
<DisableWinExeOutputInference>true</DisableWinExeOutputInference>
<AssemblyName>PowerToys.Awake</AssemblyName>
@ -59,6 +59,12 @@
<ItemGroup>
<None Remove="Assets\Awake\Awake.ico" />
<None Remove="Assets\Awake\disabled.ico" />
<None Remove="Assets\Awake\expirable.ico" />
<None Remove="Assets\Awake\indefinite.ico" />
<None Remove="Assets\Awake\normal.ico" />
<None Remove="Assets\Awake\scheduled.ico" />
<None Remove="Assets\Awake\timed.ico" />
</ItemGroup>
<ItemGroup>
@ -90,6 +96,24 @@
<Content Include="Assets\Awake\Awake.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Assets\Awake\disabled.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Assets\Awake\expirable.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Assets\Awake\indefinite.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Assets\Awake\normal.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Assets\Awake\scheduled.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Assets\Awake\timed.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>

View File

@ -8,7 +8,15 @@ namespace Awake.Core
{
internal const string AppName = "Awake";
internal const string FullAppName = "PowerToys " + AppName;
internal const string TrayWindowId = "WindowsForms10.Window.0.app.0.";
internal const string TrayWindowId = "Awake.MessageWindow";
internal const string BuildRegistryLocation = @"SOFTWARE\Microsoft\Windows NT\CurrentVersion";
// PowerToys Awake build code name. Used for exact logging
// that does not map to PowerToys broad version schema to pinpoint
// internal issues easier.
// Format of the build ID is: CODENAME_MMDDYYYY, where MMDDYYYY
// is representative of the date when the last change was made before
// the pull request is issued.
internal const string BuildId = "DAISY023_04102024";
}
}

View File

@ -12,7 +12,6 @@ namespace Awake.Core
public static void AddRange<T>(this ICollection<T> target, IEnumerable<T> source)
{
ArgumentNullException.ThrowIfNull(target);
ArgumentNullException.ThrowIfNull(source);
foreach (var element in source)

View File

@ -5,7 +5,7 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Drawing;
using System.Globalization;
using System.IO;
using System.Reactive.Linq;
@ -30,25 +30,29 @@ namespace Awake.Core
/// </summary>
public class Manager
{
private static readonly CompositeFormat AwakeMinutes = System.Text.CompositeFormat.Parse(Properties.Resources.AWAKE_MINUTES);
private static readonly CompositeFormat AwakeHours = System.Text.CompositeFormat.Parse(Properties.Resources.AWAKE_HOURS);
private static bool _isUsingPowerToysConfig;
private static BlockingCollection<ExecutionState> _stateQueue;
internal static bool IsUsingPowerToysConfig { get => _isUsingPowerToysConfig; set => _isUsingPowerToysConfig = value; }
private static readonly CompositeFormat AwakeMinutes = CompositeFormat.Parse(Resources.AWAKE_MINUTES);
private static readonly CompositeFormat AwakeHours = CompositeFormat.Parse(Resources.AWAKE_HOURS);
private static readonly BlockingCollection<ExecutionState> _stateQueue;
private static CancellationTokenSource _tokenSource;
private static SettingsUtils? _moduleSettings;
private static SettingsUtils? ModuleSettings { get => _moduleSettings; set => _moduleSettings = value; }
internal static SettingsUtils? ModuleSettings { get => _moduleSettings; set => _moduleSettings = value; }
static Manager()
{
_tokenSource = new CancellationTokenSource();
_stateQueue = new BlockingCollection<ExecutionState>();
_stateQueue = [];
ModuleSettings = new SettingsUtils();
}
public static void StartMonitor()
internal static void StartMonitor()
{
Thread monitorThread = new(() =>
{
@ -70,7 +74,7 @@ namespace Awake.Core
Bridge.SetConsoleCtrlHandler(handler, addHandler);
}
public static void AllocateConsole()
internal static void AllocateConsole()
{
Bridge.AllocConsole();
@ -103,17 +107,12 @@ namespace Awake.Core
private static ExecutionState ComputeAwakeState(bool keepDisplayOn)
{
if (keepDisplayOn)
{
return ExecutionState.ES_SYSTEM_REQUIRED | ExecutionState.ES_DISPLAY_REQUIRED | ExecutionState.ES_CONTINUOUS;
}
else
{
return ExecutionState.ES_SYSTEM_REQUIRED | ExecutionState.ES_CONTINUOUS;
}
return keepDisplayOn
? ExecutionState.ES_SYSTEM_REQUIRED | ExecutionState.ES_DISPLAY_REQUIRED | ExecutionState.ES_CONTINUOUS
: ExecutionState.ES_SYSTEM_REQUIRED | ExecutionState.ES_CONTINUOUS;
}
public static void CancelExistingThread()
internal static void CancelExistingThread()
{
Logger.LogInfo($"Attempting to ensure that the thread is properly cleaned up...");
@ -128,81 +127,156 @@ namespace Awake.Core
Logger.LogInfo("Instantiating of new token source and thread token completed.");
}
public static void SetIndefiniteKeepAwake(bool keepDisplayOn = false)
internal static void SetIndefiniteKeepAwake(bool keepDisplayOn = false)
{
PowerToysTelemetry.Log.WriteEvent(new Telemetry.AwakeIndefinitelyKeepAwakeEvent());
CancelExistingThread();
_stateQueue.Add(ComputeAwakeState(keepDisplayOn));
TrayHelper.SetShellIcon(TrayHelper.HiddenWindowHandle, $"{Constants.FullAppName} [{Resources.AWAKE_TRAY_TEXT_INDEFINITE}]", new Icon(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assets/Awake/indefinite.ico")), TrayIconAction.Update);
if (IsUsingPowerToysConfig)
{
try
{
var currentSettings = ModuleSettings!.GetSettings<AwakeSettings>(Constants.AppName) ?? new AwakeSettings();
var settingsChanged = currentSettings.Properties.Mode != AwakeMode.INDEFINITE ||
currentSettings.Properties.KeepDisplayOn != keepDisplayOn;
if (settingsChanged)
{
currentSettings.Properties.Mode = AwakeMode.INDEFINITE;
currentSettings.Properties.KeepDisplayOn = keepDisplayOn;
ModuleSettings!.SaveSettings(JsonSerializer.Serialize(currentSettings), Constants.AppName);
}
}
catch (Exception ex)
{
Logger.LogError($"Failed to handle indefinite keep awake command: {ex.Message}");
}
}
}
public static void SetNoKeepAwake()
internal static void SetExpirableKeepAwake(DateTimeOffset expireAt, bool keepDisplayOn = true)
{
PowerToysTelemetry.Log.WriteEvent(new Telemetry.AwakeNoKeepAwakeEvent());
Logger.LogInfo($"Expirable keep-awake. Expected expiration date/time: {expireAt} with display on setting set to {keepDisplayOn}.");
CancelExistingThread();
}
public static void SetExpirableKeepAwake(DateTimeOffset expireAt, bool keepDisplayOn = true)
{
PowerToysTelemetry.Log.WriteEvent(new Telemetry.AwakeExpirableKeepAwakeEvent());
CancelExistingThread();
if (expireAt > DateTime.Now && expireAt != null)
if (expireAt > DateTimeOffset.Now)
{
Logger.LogInfo($"Starting expirable log for {expireAt}");
_stateQueue.Add(ComputeAwakeState(keepDisplayOn));
Observable.Timer(expireAt).Subscribe(
TrayHelper.SetShellIcon(TrayHelper.HiddenWindowHandle, $"{Constants.FullAppName} [{Resources.AWAKE_TRAY_TEXT_EXPIRATION}]", new Icon(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assets/Awake/expirable.ico")), TrayIconAction.Update);
Observable.Timer(expireAt - DateTimeOffset.Now).Subscribe(
_ =>
{
Logger.LogInfo($"Completed expirable keep-awake.");
CancelExistingThread();
SetPassiveKeepAwakeMode(Constants.AppName);
SetPassiveKeepAwake();
},
_tokenSource.Token);
}
else
{
// The target date is not in the future.
Logger.LogError("The specified target date and time is not in the future.");
Logger.LogError($"Current time: {DateTime.Now}\tTarget time: {expireAt}");
Logger.LogError($"Current time: {DateTimeOffset.Now}\tTarget time: {expireAt}");
}
if (IsUsingPowerToysConfig)
{
try
{
var currentSettings = ModuleSettings!.GetSettings<AwakeSettings>(Constants.AppName) ?? new AwakeSettings();
var settingsChanged = currentSettings.Properties.Mode != AwakeMode.EXPIRABLE ||
currentSettings.Properties.ExpirationDateTime != expireAt ||
currentSettings.Properties.KeepDisplayOn != keepDisplayOn;
if (settingsChanged)
{
currentSettings.Properties.Mode = AwakeMode.EXPIRABLE;
currentSettings.Properties.KeepDisplayOn = keepDisplayOn;
currentSettings.Properties.ExpirationDateTime = expireAt;
ModuleSettings!.SaveSettings(JsonSerializer.Serialize(currentSettings), Constants.AppName);
}
}
catch (Exception ex)
{
Logger.LogError($"Failed to handle indefinite keep awake command: {ex.Message}");
}
}
}
public static void SetTimedKeepAwake(uint seconds, bool keepDisplayOn = true)
internal static void SetTimedKeepAwake(uint seconds, bool keepDisplayOn = true)
{
Logger.LogInfo($"Timed keep-awake. Expected runtime: {seconds} seconds with display on setting set to {keepDisplayOn}.");
PowerToysTelemetry.Log.WriteEvent(new Telemetry.AwakeTimedKeepAwakeEvent());
CancelExistingThread();
Logger.LogInfo($"Timed keep awake started for {seconds} seconds.");
_stateQueue.Add(ComputeAwakeState(keepDisplayOn));
TrayHelper.SetShellIcon(TrayHelper.HiddenWindowHandle, $"{Constants.FullAppName} [{Resources.AWAKE_TRAY_TEXT_TIMED}]", new Icon(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assets/Awake/timed.ico")), TrayIconAction.Update);
Observable.Timer(TimeSpan.FromSeconds(seconds)).Subscribe(
_ =>
{
Logger.LogInfo($"Completed timed thread.");
CancelExistingThread();
SetPassiveKeepAwakeMode(Constants.AppName);
SetPassiveKeepAwake();
},
_tokenSource.Token);
if (IsUsingPowerToysConfig)
{
try
{
var currentSettings = ModuleSettings!.GetSettings<AwakeSettings>(Constants.AppName) ?? new AwakeSettings();
var timeSpan = TimeSpan.FromSeconds(seconds);
var settingsChanged = currentSettings.Properties.Mode != AwakeMode.TIMED ||
currentSettings.Properties.IntervalHours != (uint)timeSpan.Hours ||
currentSettings.Properties.IntervalMinutes != (uint)timeSpan.Minutes;
if (settingsChanged)
{
currentSettings.Properties.Mode = AwakeMode.TIMED;
currentSettings.Properties.IntervalHours = (uint)timeSpan.Hours;
currentSettings.Properties.IntervalMinutes = (uint)timeSpan.Minutes;
ModuleSettings!.SaveSettings(JsonSerializer.Serialize(currentSettings), Constants.AppName);
}
}
catch (Exception ex)
{
Logger.LogError($"Failed to handle timed keep awake command: {ex.Message}");
}
}
}
/// <summary>
/// Performs a clean exit from Awake.
/// </summary>
/// <param name="exitCode">Exit code to exit with.</param>
/// <param name="exitSignal">Exit signal tracking the state.</param>
/// <param name="force">Determines whether to force exit and post a quitting message.</param>
internal static void CompleteExit(int exitCode, ManualResetEvent? exitSignal, bool force = false)
{
SetNoKeepAwake();
SetPassiveKeepAwake(updateSettings: false);
IntPtr windowHandle = GetHiddenWindow();
if (windowHandle != IntPtr.Zero)
if (TrayHelper.HiddenWindowHandle != IntPtr.Zero)
{
Bridge.SendMessage(windowHandle, Native.Constants.WM_CLOSE, 0, 0);
// Delete the icon.
TrayHelper.SetShellIcon(TrayHelper.HiddenWindowHandle, string.Empty, null, TrayIconAction.Delete);
// Close the message window that we used for the tray.
Bridge.SendMessage(TrayHelper.HiddenWindowHandle, Native.Constants.WM_CLOSE, 0, 0);
}
if (force)
@ -213,7 +287,7 @@ namespace Awake.Core
try
{
exitSignal?.Set();
Bridge.DestroyWindow(windowHandle);
Bridge.DestroyWindow(TrayHelper.HiddenWindowHandle);
}
catch (Exception ex)
{
@ -221,7 +295,11 @@ namespace Awake.Core
}
}
public static string GetOperatingSystemBuild()
/// <summary>
/// Gets the operating system for logging purposes.
/// </summary>
/// <returns>Returns the string representing the current OS build.</returns>
internal static string GetOperatingSystemBuild()
{
try
{
@ -245,83 +323,71 @@ namespace Awake.Core
}
}
[SuppressMessage("Performance", "CA1806:Do not ignore method results", Justification = "Function returns DWORD value that identifies the current thread, but we do not need it.")]
internal static IEnumerable<IntPtr> EnumerateWindowsForProcess(int processId)
/// <summary>
/// Generates the default system tray options in situations where no custom options are provided.
/// </summary>
/// <returns>Returns a dictionary of default Awake timed interval options.</returns>
internal static Dictionary<string, int> GetDefaultTrayOptions()
{
var handles = new List<IntPtr>();
var hCurrentWnd = IntPtr.Zero;
do
{
hCurrentWnd = Bridge.FindWindowEx(IntPtr.Zero, hCurrentWnd, null as string, null);
Bridge.GetWindowThreadProcessId(hCurrentWnd, out uint targetProcessId);
if (targetProcessId == processId)
{
handles.Add(hCurrentWnd);
}
}
while (hCurrentWnd != IntPtr.Zero);
return handles;
}
[SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "In this context, the string is only converted to a hex value.")]
internal static IntPtr GetHiddenWindow()
{
IEnumerable<IntPtr> windowHandles = EnumerateWindowsForProcess(Environment.ProcessId);
var domain = AppDomain.CurrentDomain.GetHashCode().ToString("x");
string targetClass = $"{Constants.TrayWindowId}{domain}";
foreach (var handle in windowHandles)
{
StringBuilder className = new(256);
int classQueryResult = Bridge.GetClassName(handle, className, className.Capacity);
if (classQueryResult != 0 && className.ToString().StartsWith(targetClass, StringComparison.InvariantCultureIgnoreCase))
{
return handle;
}
}
return IntPtr.Zero;
}
public static Dictionary<string, int> GetDefaultTrayOptions()
{
Dictionary<string, int> optionsList = new Dictionary<string, int>
Dictionary<string, int> optionsList = new()
{
{ string.Format(CultureInfo.InvariantCulture, AwakeMinutes, 30), 1800 },
{ Resources.AWAKE_1_HOUR, 3600 },
{ string.Format(CultureInfo.InvariantCulture, AwakeHours, 1), 3600 },
{ string.Format(CultureInfo.InvariantCulture, AwakeHours, 2), 7200 },
};
return optionsList;
}
public static void SetPassiveKeepAwakeMode(string moduleName)
/// <summary>
/// Resets the computer to standard power settings.
/// </summary>
/// <param name="updateSettings">In certain cases, such as exits, we want to make sure that settings are not reset for the passive mode but rather retained based on previous execution. Default is to save settings, but otherwise it can be overridden.</param>
internal static void SetPassiveKeepAwake(bool updateSettings = true)
{
AwakeSettings currentSettings;
Logger.LogInfo($"Operating in passive mode (computer's standard power plan). No custom keep awake settings enabled.");
PowerToysTelemetry.Log.WriteEvent(new Telemetry.AwakeNoKeepAwakeEvent());
CancelExistingThread();
TrayHelper.SetShellIcon(TrayHelper.HiddenWindowHandle, $"{Constants.FullAppName} [{Resources.AWAKE_TRAY_TEXT_OFF}]", new Icon(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assets/Awake/disabled.ico")), TrayIconAction.Update);
if (IsUsingPowerToysConfig && updateSettings)
{
try
{
currentSettings = ModuleSettings!.GetSettings<AwakeSettings>(moduleName);
}
catch (Exception ex)
{
string? errorString = $"Failed to reset Awake mode GetSettings: {ex.Message}";
Logger.LogError(errorString);
currentSettings = new AwakeSettings();
}
var currentSettings = ModuleSettings!.GetSettings<AwakeSettings>(Constants.AppName) ?? new AwakeSettings();
if (currentSettings.Properties.Mode != AwakeMode.PASSIVE)
{
currentSettings.Properties.Mode = AwakeMode.PASSIVE;
try
{
ModuleSettings!.SaveSettings(JsonSerializer.Serialize(currentSettings), moduleName);
ModuleSettings!.SaveSettings(JsonSerializer.Serialize(currentSettings), Constants.AppName);
}
}
catch (Exception ex)
{
string? errorString = $"Failed to reset Awake mode SaveSettings: {ex.Message}";
Logger.LogError(errorString);
Logger.LogError($"Failed to reset Awake mode: {ex.Message}");
}
}
}
/// <summary>
/// Sets the display settings.
/// </summary>
internal static void SetDisplay()
{
if (IsUsingPowerToysConfig)
{
try
{
var currentSettings = ModuleSettings!.GetSettings<AwakeSettings>(Constants.AppName) ?? new AwakeSettings();
currentSettings.Properties.KeepDisplayOn = !currentSettings.Properties.KeepDisplayOn;
ModuleSettings!.SaveSettings(JsonSerializer.Serialize(currentSettings), Constants.AppName);
}
catch (Exception ex)
{
Logger.LogError($"Failed to handle display setting command: {ex.Message}");
}
}
}
}

View File

@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
namespace Awake.Core.Models
{
internal struct Msg
{
public IntPtr HWnd;
public uint Message;
public IntPtr WParam;
public IntPtr LParam;
public uint Time;
public Point Pt;
}
}

View File

@ -0,0 +1,21 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Runtime.InteropServices;
namespace Awake.Core.Models
{
[StructLayout(LayoutKind.Sequential)]
public struct MenuInfo
{
public uint CbSize; // Size of the structure, in bytes
public uint FMask; // Specifies which members of the structure are valid
public uint DwStyle; // Style of the menu
public uint CyMax; // Maximum height of the menu, in pixels
public IntPtr HbrBack; // Handle to the brush used for the menu's background
public uint DwContextHelpID; // Context help ID
public IntPtr DwMenuData; // Pointer to the menu's user data
}
}

View File

@ -0,0 +1,22 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Runtime.InteropServices;
namespace Awake.Core.Models
{
[StructLayout(LayoutKind.Sequential)]
public struct NotifyIconData
{
public int CbSize;
public IntPtr HWnd;
public int UId;
public int UFlags;
public int UCallbackMessage;
public IntPtr HIcon;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
public string SzTip;
}
}

View File

@ -0,0 +1,15 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
namespace Awake.Core.Models
{
[StructLayout(LayoutKind.Sequential)]
public struct Point
{
public int X;
public int Y;
}
}

View File

@ -0,0 +1,62 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Threading;
namespace Awake.Core.Models
{
internal sealed class SingleThreadSynchronizationContext : SynchronizationContext
{
private readonly Queue<Tuple<SendOrPostCallback, object>> queue =
new();
#pragma warning disable CS8765 // Nullability of type of parameter doesn't match overridden member (possibly because of nullability attributes).
public override void Post(SendOrPostCallback d, object state)
#pragma warning restore CS8765 // Nullability of type of parameter doesn't match overridden member (possibly because of nullability attributes).
{
lock (queue)
{
queue.Enqueue(Tuple.Create(d, state));
Monitor.Pulse(queue);
}
}
public void BeginMessageLoop()
{
while (true)
{
Tuple<SendOrPostCallback, object> work;
lock (queue)
{
while (queue.Count == 0)
{
Monitor.Wait(queue);
}
work = queue.Dequeue();
}
if (work == null)
{
break;
}
work.Item1(work.Item2);
}
}
public void EndMessageLoop()
{
lock (queue)
{
#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
queue.Enqueue(null); // Signal the end of the message loop
#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type.
Monitor.Pulse(queue);
}
}
}
}

View File

@ -6,11 +6,11 @@ namespace Awake.Core.Models
{
internal enum TrayCommands : uint
{
TC_DISPLAY_SETTING = Native.Constants.WM_USER + 1,
TC_MODE_PASSIVE = Native.Constants.WM_USER + 2,
TC_MODE_INDEFINITE = Native.Constants.WM_USER + 3,
TC_MODE_EXPIRABLE = Native.Constants.WM_USER + 4,
TC_EXIT = Native.Constants.WM_USER + 100,
TC_TIME = Native.Constants.WM_USER + 101,
TC_DISPLAY_SETTING = Native.Constants.WM_USER + 0x2,
TC_MODE_PASSIVE = Native.Constants.WM_USER + 0x3,
TC_MODE_INDEFINITE = Native.Constants.WM_USER + 0x4,
TC_MODE_EXPIRABLE = Native.Constants.WM_USER + 0x5,
TC_EXIT = Native.Constants.WM_USER + 0x64,
TC_TIME = Native.Constants.WM_USER + 0x65,
}
}

View File

@ -0,0 +1,13 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Awake.Core.Models
{
internal enum TrayIconAction
{
Add,
Update,
Delete,
}
}

View File

@ -0,0 +1,26 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Runtime.InteropServices;
namespace Awake.Core.Models
{
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
internal struct WndClassEx
{
public uint CbSize;
public uint Style;
public IntPtr LpfnWndProc;
public int CbClsExtra;
public int CbWndExtra;
public IntPtr HInstance;
public IntPtr HIcon;
public IntPtr HCursor;
public IntPtr HbrBackground;
public string LpszMenuName;
public string LpszClassName;
public IntPtr HIconSm;
}
}

View File

@ -5,14 +5,14 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using Awake.Core.Models;
namespace Awake.Core.Native
{
internal sealed class Bridge
{
internal delegate bool EnumThreadDelegate(IntPtr hWnd, IntPtr lParam);
[UnmanagedFunctionPointer(CallingConvention.Winapi, SetLastError = true)]
internal delegate int WndProcDelegate(IntPtr hWnd, uint message, IntPtr wParam, IntPtr lParam);
[DllImport("Powrprof.dll", SetLastError = true)]
internal static extern bool GetPwrCapabilities(out SystemPowerCapabilities lpSystemPowerCapabilities);
@ -30,9 +30,6 @@ namespace Awake.Core.Native
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern bool SetStdHandle(int nStdHandle, IntPtr hHandle);
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern uint GetCurrentThreadId();
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
internal static extern IntPtr CreateFile(
[MarshalAs(UnmanagedType.LPWStr)] string filename,
@ -50,25 +47,13 @@ namespace Awake.Core.Native
internal static extern bool InsertMenu(IntPtr hMenu, uint uPosition, uint uFlags, uint uIDNewItem, string lpNewItem);
[DllImport("user32.dll", SetLastError = true)]
internal static extern bool TrackPopupMenuEx(IntPtr hmenu, uint fuFlags, int x, int y, IntPtr hwnd, IntPtr lptpm);
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
internal static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
internal static extern IntPtr FindWindowEx(IntPtr parentHandle, IntPtr hWndChildAfter, string? className, string? windowTitle);
[DllImport("user32.dll", SetLastError = true)]
internal static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool SetForegroundWindow(IntPtr hWnd);
public static extern bool TrackPopupMenuEx(IntPtr hMenu, uint uFlags, int x, int y, IntPtr hWnd, IntPtr lptpm);
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
internal static extern IntPtr SendMessage(IntPtr hWnd, uint msg, nuint wParam, nint lParam);
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool DestroyMenu(IntPtr hMenu);
[DllImport("user32.dll")]
@ -76,5 +61,46 @@ namespace Awake.Core.Native
[DllImport("user32.dll")]
internal static extern void PostQuitMessage(int nExitCode);
[DllImport("shell32.dll")]
internal static extern bool Shell_NotifyIcon(int dwMessage, ref NotifyIconData pnid);
[DllImport("user32.dll", SetLastError = true)]
internal static extern bool TranslateMessage(ref Msg lpMsg);
[DllImport("user32.dll", SetLastError = true)]
internal static extern IntPtr DispatchMessage(ref Msg lpMsg);
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
internal static extern IntPtr RegisterClassEx(ref WndClassEx lpwcx);
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
internal static extern IntPtr CreateWindowEx(uint dwExStyle, string lpClassName, string lpWindowName, uint dwStyle, int x, int y, int nWidth, int nHeight, IntPtr hWndParent, IntPtr hMenu, IntPtr hInstance, IntPtr lpParam);
[DllImport("user32.dll", SetLastError = true)]
internal static extern int DefWindowProc(IntPtr hWnd, uint message, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll", SetLastError = true)]
internal static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool GetCursorPos(out Point lpPoint);
[DllImport("user32.dll")]
internal static extern bool ScreenToClient(IntPtr hWnd, ref Point lpPoint);
[DllImport("user32.dll")]
internal static extern bool GetMessage(out Msg lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax);
[DllImport("user32.dll", SetLastError = true)]
internal static extern bool UpdateWindow(IntPtr hWnd);
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool SetMenuInfo(IntPtr hMenu, ref MenuInfo lpcmi);
[DllImport("user32.dll")]
internal static extern bool SetForegroundWindow(IntPtr hWnd);
}
}

View File

@ -7,26 +7,46 @@ namespace Awake.Core.Native
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "Win32 API convention.")]
internal sealed class Constants
{
internal const uint WM_COMMAND = 0x111;
internal const uint WM_USER = 0x400;
internal const uint WM_GETTEXT = 0x000D;
// Window Messages
internal const uint WM_COMMAND = 0x0111;
internal const uint WM_USER = 0x0400U;
internal const uint WM_CLOSE = 0x0010;
internal const int WM_DESTROY = 0x0002;
internal const int WM_LBUTTONDOWN = 0x0201;
internal const int WM_RBUTTONDOWN = 0x0204;
// Popup menu constants.
// Menu Flags
internal const uint MF_BYPOSITION = 1024;
internal const uint MF_STRING = 0;
internal const uint MF_MENUBREAK = 0x00000040;
internal const uint MF_SEPARATOR = 0x00000800;
internal const uint MF_POPUP = 0x00000010;
internal const uint MF_UNCHECKED = 0x00000000;
internal const uint MF_CHECKED = 0x00000008;
internal const uint MF_OWNERDRAW = 0x00000100;
internal const uint MF_ENABLED = 0x00000000;
internal const uint MF_DISABLED = 0x00000002;
// Standard Handles
internal const int STD_OUTPUT_HANDLE = -11;
// Generic Access Rights
internal const uint GENERIC_WRITE = 0x40000000;
internal const uint GENERIC_READ = 0x80000000;
// Notification Icons
internal const int NIF_ICON = 0x00000002;
internal const int NIF_MESSAGE = 0x00000001;
internal const int NIF_TIP = 0x00000004;
internal const int NIM_ADD = 0x00000000;
internal const int NIM_DELETE = 0x00000002;
internal const int NIM_MODIFY = 0x00000001;
// Track Popup Menu Flags
internal const uint TPM_LEFT_ALIGN = 0x0000;
internal const uint TPM_BOTTOMALIGN = 0x0020;
internal const uint TPM_LEFT_BUTTON = 0x0000;
// Menu Item Info Flags
internal const uint MNS_AUTO_DISMISS = 0x10000000;
internal const uint MIM_STYLE = 0x00000010;
}
}

View File

@ -4,12 +4,12 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Linq;
using System.Text.RegularExpressions;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using Awake.Core.Models;
using Awake.Core.Native;
using Awake.Properties;
@ -27,147 +27,348 @@ namespace Awake.Core
/// </remarks>
internal static class TrayHelper
{
private static NotifyIconData _notifyIconData;
private static ManualResetEvent? _exitSignal;
private static IntPtr _trayMenu;
private static IntPtr TrayMenu { get => _trayMenu; set => _trayMenu = value; }
private static NotifyIcon TrayIcon { get; set; }
private static IntPtr _hiddenWindowHandle;
internal static IntPtr HiddenWindowHandle { get => _hiddenWindowHandle; private set => _hiddenWindowHandle = value; }
static TrayHelper()
{
TrayIcon = new NotifyIcon();
TrayMenu = IntPtr.Zero;
HiddenWindowHandle = IntPtr.Zero;
}
public static void InitializeTray(string text, Icon icon, ManualResetEvent? exitSignal, ContextMenuStrip? contextMenu = null)
public static void InitializeTray(string text, Icon icon, ManualResetEvent? exitSignal)
{
Task.Factory.StartNew(
(tray) =>
_exitSignal = exitSignal;
CreateHiddenWindow(icon, text);
}
private static void ShowContextMenu(IntPtr hWnd)
{
Bridge.SetForegroundWindow(hWnd);
// Get the handle to the context menu associated with the tray icon
IntPtr hMenu = TrayMenu;
// Get the current cursor position
Bridge.GetCursorPos(out Models.Point cursorPos);
Bridge.ScreenToClient(hWnd, ref cursorPos);
MenuInfo menuInfo = new()
{
CbSize = (uint)Marshal.SizeOf(typeof(MenuInfo)),
FMask = Native.Constants.MIM_STYLE,
DwStyle = Native.Constants.MNS_AUTO_DISMISS,
};
Bridge.SetMenuInfo(hMenu, ref menuInfo);
// Display the context menu at the cursor position
Bridge.TrackPopupMenuEx(
hMenu,
Native.Constants.TPM_LEFT_ALIGN | Native.Constants.TPM_BOTTOMALIGN | Native.Constants.TPM_LEFT_BUTTON,
cursorPos.X,
cursorPos.Y,
hWnd,
IntPtr.Zero);
}
private static void CreateHiddenWindow(Icon icon, string text)
{
IntPtr hWnd = IntPtr.Zero;
// Start the message loop asynchronously
Task.Run(() =>
{
RunOnMainThread(() =>
{
WndClassEx wcex = new()
{
CbSize = (uint)Marshal.SizeOf(typeof(WndClassEx)),
Style = 0,
LpfnWndProc = Marshal.GetFunctionPointerForDelegate<Bridge.WndProcDelegate>(WndProc),
CbClsExtra = 0,
CbWndExtra = 0,
HInstance = Marshal.GetHINSTANCE(typeof(Program).Module),
HIcon = IntPtr.Zero,
HCursor = IntPtr.Zero,
HbrBackground = IntPtr.Zero,
LpszMenuName = string.Empty,
LpszClassName = Constants.TrayWindowId,
HIconSm = IntPtr.Zero,
};
Bridge.RegisterClassEx(ref wcex);
hWnd = Bridge.CreateWindowEx(
0,
Constants.TrayWindowId,
text,
0x00CF0000 | 0x00000001 | 0x00000008, // WS_OVERLAPPEDWINDOW | WS_VISIBLE | WS_MINIMIZEBOX
0,
0,
0,
0,
unchecked(-3),
IntPtr.Zero,
Marshal.GetHINSTANCE(typeof(Program).Module),
IntPtr.Zero);
if (hWnd == IntPtr.Zero)
{
int errorCode = Marshal.GetLastWin32Error();
throw new Win32Exception(errorCode, "Failed to add tray icon. Error code: " + errorCode);
}
// Keep this as a reference because we will need it when we update
// the tray icon in the future.
HiddenWindowHandle = hWnd;
Bridge.ShowWindow(hWnd, 0); // SW_HIDE
Bridge.UpdateWindow(hWnd);
SetShellIcon(hWnd, text, icon);
RunMessageLoop();
});
});
}
internal static void SetShellIcon(IntPtr hWnd, string text, Icon? icon, TrayIconAction action = TrayIconAction.Add)
{
int message = Native.Constants.NIM_ADD;
switch (action)
{
case TrayIconAction.Update:
message = Native.Constants.NIM_MODIFY;
break;
case TrayIconAction.Delete:
message = Native.Constants.NIM_DELETE;
break;
case TrayIconAction.Add:
default:
break;
}
_notifyIconData = action == TrayIconAction.Add || action == TrayIconAction.Update
? new NotifyIconData
{
CbSize = Marshal.SizeOf(typeof(NotifyIconData)),
HWnd = hWnd,
UId = 1000,
UFlags = Native.Constants.NIF_ICON | Native.Constants.NIF_TIP | Native.Constants.NIF_MESSAGE,
UCallbackMessage = (int)Native.Constants.WM_USER,
HIcon = icon!.Handle,
SzTip = text,
}
: new NotifyIconData
{
CbSize = Marshal.SizeOf(typeof(NotifyIconData)),
HWnd = hWnd,
UId = 1000,
UFlags = 0,
};
if (!Bridge.Shell_NotifyIcon(message, ref _notifyIconData))
{
int errorCode = Marshal.GetLastWin32Error();
throw new Win32Exception(errorCode, $"Failed to change tray icon. Action: {action} and error code: {errorCode}");
}
}
private static void RunMessageLoop()
{
while (Bridge.GetMessage(out Msg msg, IntPtr.Zero, 0, 0))
{
Bridge.TranslateMessage(ref msg);
Bridge.DispatchMessage(ref msg);
}
}
private static int WndProc(IntPtr hWnd, uint message, IntPtr wParam, IntPtr lParam)
{
switch (message)
{
case Native.Constants.WM_USER:
if (lParam == (IntPtr)Native.Constants.WM_LBUTTONDOWN || lParam == (IntPtr)Native.Constants.WM_RBUTTONDOWN)
{
// Show the context menu associated with the tray icon
ShowContextMenu(hWnd);
}
break;
case Native.Constants.WM_DESTROY:
// Clean up resources when the window is destroyed
Bridge.PostQuitMessage(0);
break;
case Native.Constants.WM_COMMAND:
int trayCommandsSize = Enum.GetNames(typeof(TrayCommands)).Length;
long targetCommandIndex = wParam.ToInt64() & 0xFFFF;
switch (targetCommandIndex)
{
case (uint)TrayCommands.TC_EXIT:
Manager.CompleteExit(0, _exitSignal, true);
break;
case (uint)TrayCommands.TC_DISPLAY_SETTING:
Manager.SetDisplay();
break;
case (uint)TrayCommands.TC_MODE_INDEFINITE:
Manager.SetIndefiniteKeepAwake();
break;
case (uint)TrayCommands.TC_MODE_PASSIVE:
Manager.SetPassiveKeepAwake();
break;
default:
if (targetCommandIndex >= trayCommandsSize)
{
AwakeSettings settings = Manager.ModuleSettings!.GetSettings<AwakeSettings>(Constants.AppName);
if (settings.Properties.CustomTrayTimes.Count == 0)
{
settings.Properties.CustomTrayTimes.AddRange(Manager.GetDefaultTrayOptions());
}
int index = (int)targetCommandIndex - (int)TrayCommands.TC_TIME;
uint targetTime = (uint)settings.Properties.CustomTrayTimes.ElementAt(index).Value;
Manager.SetTimedKeepAwake(targetTime);
}
break;
}
break;
default:
// Let the default window procedure handle other messages
return Bridge.DefWindowProc(hWnd, message, wParam, lParam);
}
return Bridge.DefWindowProc(hWnd, message, wParam, lParam);
}
internal static void RunOnMainThread(Action action)
{
var syncContext = new SingleThreadSynchronizationContext();
SynchronizationContext.SetSynchronizationContext(syncContext);
#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
syncContext.Post(
_ =>
{
try
{
Logger.LogInfo("Setting up the tray.");
if (tray != null)
{
((NotifyIcon)tray).Text = text;
((NotifyIcon)tray).Icon = icon;
((NotifyIcon)tray).ContextMenuStrip = contextMenu;
((NotifyIcon)tray).Visible = true;
((NotifyIcon)tray).MouseClick += TrayClickHandler;
Application.AddMessageFilter(new TrayMessageFilter(exitSignal));
Application.Run();
Logger.LogInfo("Tray setup complete.");
action();
}
}
catch (Exception ex)
catch (Exception e)
{
Logger.LogError($"An error occurred initializing the tray. {ex.Message}");
Logger.LogError($"{ex.StackTrace}");
Console.WriteLine("Error: " + e.Message);
}
finally
{
syncContext.EndMessageLoop();
}
},
TrayIcon);
null);
#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type.
syncContext.BeginMessageLoop();
}
/// <summary>
/// Function used to construct the context menu in the tray natively.
/// </summary>
/// <remarks>
/// We need to use the Windows API here instead of the common control exposed
/// by NotifyIcon because the one that is built into the Windows Forms stack
/// hasn't been updated in a while and is looking like Office XP. That introduces
/// scalability and coloring changes on any OS past Windows XP.
/// </remarks>
/// <param name="sender">The sender that triggers the handler.</param>
/// <param name="e">MouseEventArgs instance containing mouse click event information.</param>
private static void TrayClickHandler(object? sender, MouseEventArgs e)
{
IntPtr windowHandle = Manager.GetHiddenWindow();
if (windowHandle != IntPtr.Zero)
{
Bridge.SetForegroundWindow(windowHandle);
Bridge.TrackPopupMenuEx(TrayMenu, 0, Cursor.Position.X, Cursor.Position.Y, windowHandle, IntPtr.Zero);
}
}
internal static void SetTray(string text, AwakeSettings settings, bool startedFromPowerToys)
internal static void SetTray(AwakeSettings settings, bool startedFromPowerToys)
{
SetTray(
text,
settings.Properties.KeepDisplayOn,
settings.Properties.Mode,
settings.Properties.CustomTrayTimes,
startedFromPowerToys);
}
public static void SetTray(string text, bool keepDisplayOn, AwakeMode mode, Dictionary<string, int> trayTimeShortcuts, bool startedFromPowerToys)
public static void SetTray(bool keepDisplayOn, AwakeMode mode, Dictionary<string, int> trayTimeShortcuts, bool startedFromPowerToys)
{
if (TrayMenu != IntPtr.Zero)
ClearExistingTrayMenu();
CreateNewTrayMenu(startedFromPowerToys, keepDisplayOn, mode);
InsertAwakeModeMenuItems(mode);
EnsureDefaultTrayTimeShortcuts(trayTimeShortcuts);
CreateAwakeTimeSubMenu(trayTimeShortcuts);
}
private static void ClearExistingTrayMenu()
{
var destructionStatus = Bridge.DestroyMenu(TrayMenu);
if (destructionStatus != true)
if (TrayMenu != IntPtr.Zero && !Bridge.DestroyMenu(TrayMenu))
{
Logger.LogError("Failed to destroy menu.");
int errorCode = Marshal.GetLastWin32Error();
Logger.LogError($"Failed to destroy menu: {errorCode}");
}
}
private static void CreateNewTrayMenu(bool startedFromPowerToys, bool keepDisplayOn, AwakeMode mode)
{
TrayMenu = Bridge.CreatePopupMenu();
if (TrayMenu != IntPtr.Zero)
if (TrayMenu == IntPtr.Zero)
{
return;
}
if (!startedFromPowerToys)
{
// If Awake is started from PowerToys, the correct way to exit it is disabling it from Settings.
Bridge.InsertMenu(TrayMenu, 0, Native.Constants.MF_BYPOSITION | Native.Constants.MF_STRING, (uint)TrayCommands.TC_EXIT, Resources.AWAKE_EXIT);
Bridge.InsertMenu(TrayMenu, 0, Native.Constants.MF_BYPOSITION | Native.Constants.MF_SEPARATOR, 0, string.Empty);
InsertMenuItem(0, TrayCommands.TC_EXIT, Resources.AWAKE_EXIT);
}
Bridge.InsertMenu(TrayMenu, 0, Native.Constants.MF_BYPOSITION | Native.Constants.MF_STRING | (keepDisplayOn ? Native.Constants.MF_CHECKED : Native.Constants.MF_UNCHECKED) | (mode == AwakeMode.PASSIVE ? Native.Constants.MF_DISABLED : Native.Constants.MF_ENABLED), (uint)TrayCommands.TC_DISPLAY_SETTING, Resources.AWAKE_KEEP_SCREEN_ON);
InsertMenuItem(0, TrayCommands.TC_DISPLAY_SETTING, Resources.AWAKE_KEEP_SCREEN_ON, keepDisplayOn, mode == AwakeMode.PASSIVE);
InsertSeparator(1);
}
// In case there are no tray shortcuts defined for the application default to a
// reasonable initial set.
private static void InsertMenuItem(int position, TrayCommands command, string text, bool checkedState = false, bool disabled = false)
{
uint state = Native.Constants.MF_BYPOSITION | Native.Constants.MF_STRING;
state |= checkedState ? Native.Constants.MF_CHECKED : Native.Constants.MF_UNCHECKED;
state |= disabled ? Native.Constants.MF_DISABLED : Native.Constants.MF_ENABLED;
Bridge.InsertMenu(TrayMenu, (uint)position, state, (uint)command, text);
}
private static void InsertSeparator(int position)
{
Bridge.InsertMenu(TrayMenu, (uint)position, Native.Constants.MF_BYPOSITION | Native.Constants.MF_SEPARATOR, 0, string.Empty);
}
private static void EnsureDefaultTrayTimeShortcuts(Dictionary<string, int> trayTimeShortcuts)
{
if (trayTimeShortcuts.Count == 0)
{
trayTimeShortcuts.AddRange(Manager.GetDefaultTrayOptions());
}
}
private static void CreateAwakeTimeSubMenu(Dictionary<string, int> trayTimeShortcuts)
{
var awakeTimeMenu = Bridge.CreatePopupMenu();
for (int i = 0; i < trayTimeShortcuts.Count; i++)
{
Bridge.InsertMenu(awakeTimeMenu, (uint)i, Native.Constants.MF_BYPOSITION | Native.Constants.MF_STRING, (uint)TrayCommands.TC_TIME + (uint)i, trayTimeShortcuts.ElementAt(i).Key);
}
Bridge.InsertMenu(TrayMenu, 0, Native.Constants.MF_BYPOSITION | Native.Constants.MF_SEPARATOR, 0, string.Empty);
Bridge.InsertMenu(TrayMenu, 0, Native.Constants.MF_BYPOSITION | Native.Constants.MF_STRING | (mode == AwakeMode.PASSIVE ? Native.Constants.MF_CHECKED : Native.Constants.MF_UNCHECKED), (uint)TrayCommands.TC_MODE_PASSIVE, Resources.AWAKE_OFF);
Bridge.InsertMenu(TrayMenu, 0, Native.Constants.MF_BYPOSITION | Native.Constants.MF_STRING | (mode == AwakeMode.INDEFINITE ? Native.Constants.MF_CHECKED : Native.Constants.MF_UNCHECKED), (uint)TrayCommands.TC_MODE_INDEFINITE, Resources.AWAKE_KEEP_INDEFINITELY);
Bridge.InsertMenu(TrayMenu, 0, Native.Constants.MF_BYPOSITION | Native.Constants.MF_POPUP | (mode == AwakeMode.TIMED ? Native.Constants.MF_CHECKED : Native.Constants.MF_UNCHECKED), (uint)awakeTimeMenu, Resources.AWAKE_KEEP_ON_INTERVAL);
Bridge.InsertMenu(TrayMenu, 0, Native.Constants.MF_BYPOSITION | Native.Constants.MF_STRING | Native.Constants.MF_DISABLED | (mode == AwakeMode.EXPIRABLE ? Native.Constants.MF_CHECKED : Native.Constants.MF_UNCHECKED), (uint)TrayCommands.TC_MODE_EXPIRABLE, Resources.AWAKE_KEEP_UNTIL_EXPIRATION);
TrayIcon.Text = text;
Bridge.InsertMenu(TrayMenu, 0, Native.Constants.MF_BYPOSITION | Native.Constants.MF_POPUP, (uint)awakeTimeMenu, Resources.AWAKE_KEEP_ON_INTERVAL);
}
private sealed class CheckButtonToolStripMenuItemAccessibleObject : ToolStripItem.ToolStripItemAccessibleObject
private static void InsertAwakeModeMenuItems(AwakeMode mode)
{
private readonly CheckButtonToolStripMenuItem _menuItem;
InsertSeparator(0);
public CheckButtonToolStripMenuItemAccessibleObject(CheckButtonToolStripMenuItem menuItem)
: base(menuItem)
{
_menuItem = menuItem;
}
public override AccessibleRole Role => AccessibleRole.CheckButton;
public override string Name => _menuItem.Text + ", " + Role + ", " + (_menuItem.Checked ? Resources.AWAKE_CHECKED : Resources.AWAKE_UNCHECKED);
}
private sealed class CheckButtonToolStripMenuItem : ToolStripMenuItem
{
protected override AccessibleObject CreateAccessibilityInstance()
{
return new CheckButtonToolStripMenuItemAccessibleObject(this);
}
InsertMenuItem(0, TrayCommands.TC_MODE_PASSIVE, Resources.AWAKE_OFF, mode == AwakeMode.PASSIVE);
InsertMenuItem(0, TrayCommands.TC_MODE_INDEFINITE, Resources.AWAKE_KEEP_INDEFINITELY, mode == AwakeMode.INDEFINITE);
InsertMenuItem(0, TrayCommands.TC_MODE_EXPIRABLE, Resources.AWAKE_KEEP_UNTIL_EXPIRATION, mode == AwakeMode.EXPIRABLE, true);
}
}
}

View File

@ -1,172 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Windows.Forms;
using Awake.Core.Models;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
namespace Awake.Core
{
public class TrayMessageFilter : IMessageFilter
{
private static SettingsUtils? _moduleSettings;
private static SettingsUtils? ModuleSettings { get => _moduleSettings; set => _moduleSettings = value; }
private static ManualResetEvent? _exitSignal;
public TrayMessageFilter(ManualResetEvent? exitSignal)
{
_exitSignal = exitSignal;
ModuleSettings = new SettingsUtils();
}
public bool PreFilterMessage(ref Message m)
{
var trayCommandsSize = Enum.GetNames(typeof(TrayCommands)).Length;
switch (m.Msg)
{
case (int)Native.Constants.WM_COMMAND:
var targetCommandIndex = m.WParam.ToInt64() & 0xFFFF;
switch (targetCommandIndex)
{
case (long)TrayCommands.TC_EXIT:
ExitCommandHandler(_exitSignal);
break;
case (long)TrayCommands.TC_DISPLAY_SETTING:
DisplaySettingCommandHandler(Constants.AppName);
break;
case (long)TrayCommands.TC_MODE_INDEFINITE:
IndefiniteKeepAwakeCommandHandler(Constants.AppName);
break;
case (long)TrayCommands.TC_MODE_PASSIVE:
PassiveKeepAwakeCommandHandler(Constants.AppName);
break;
case var _ when targetCommandIndex >= trayCommandsSize:
// Format for the timer block:
// TrayCommands.TC_TIME + ZERO_BASED_INDEX_IN_SETTINGS
AwakeSettings settings = ModuleSettings!.GetSettings<AwakeSettings>(Constants.AppName);
if (settings.Properties.CustomTrayTimes.Count == 0)
{
settings.Properties.CustomTrayTimes.AddRange(Manager.GetDefaultTrayOptions());
}
int index = (int)targetCommandIndex - (int)TrayCommands.TC_TIME;
var targetTime = settings.Properties.CustomTrayTimes.ElementAt(index).Value;
TimedKeepAwakeCommandHandler(Constants.AppName, targetTime);
break;
}
break;
}
return false;
}
private static void ExitCommandHandler(ManualResetEvent? exitSignal)
{
Manager.CompleteExit(0, exitSignal, true);
}
private static void DisplaySettingCommandHandler(string moduleName)
{
AwakeSettings currentSettings;
try
{
currentSettings = ModuleSettings!.GetSettings<AwakeSettings>(moduleName);
}
catch (Exception ex)
{
string? errorString = $"Failed GetSettings: {ex.Message}";
Logger.LogError(errorString);
currentSettings = new AwakeSettings();
}
currentSettings.Properties.KeepDisplayOn = !currentSettings.Properties.KeepDisplayOn;
try
{
ModuleSettings!.SaveSettings(JsonSerializer.Serialize(currentSettings), moduleName);
}
catch (Exception ex)
{
string? errorString = $"Failed SaveSettings: {ex.Message}";
Logger.LogError(errorString);
}
}
private static void TimedKeepAwakeCommandHandler(string moduleName, int seconds)
{
TimeSpan timeSpan = TimeSpan.FromSeconds(seconds);
AwakeSettings currentSettings;
try
{
currentSettings = ModuleSettings!.GetSettings<AwakeSettings>(moduleName);
}
catch (Exception ex)
{
string? errorString = $"Failed GetSettings: {ex.Message}";
Logger.LogError(errorString);
currentSettings = new AwakeSettings();
}
currentSettings.Properties.Mode = AwakeMode.TIMED;
currentSettings.Properties.IntervalHours = (uint)timeSpan.Hours;
currentSettings.Properties.IntervalMinutes = (uint)timeSpan.Minutes;
try
{
ModuleSettings!.SaveSettings(JsonSerializer.Serialize(currentSettings), moduleName);
}
catch (Exception ex)
{
string? errorString = $"Failed SaveSettings: {ex.Message}";
Logger.LogError(errorString);
}
}
private static void PassiveKeepAwakeCommandHandler(string moduleName)
{
Manager.SetPassiveKeepAwakeMode(moduleName);
}
private static void IndefiniteKeepAwakeCommandHandler(string moduleName)
{
AwakeSettings currentSettings;
try
{
currentSettings = ModuleSettings!.GetSettings<AwakeSettings>(moduleName);
}
catch (Exception ex)
{
string? errorString = $"Failed GetSettings: {ex.Message}";
Logger.LogError(errorString);
currentSettings = new AwakeSettings();
}
currentSettings.Properties.Mode = AwakeMode.INDEFINITE;
try
{
ModuleSettings!.SaveSettings(JsonSerializer.Serialize(currentSettings), moduleName);
}
catch (Exception ex)
{
string? errorString = $"Failed SaveSettings: {ex.Message}";
Logger.LogError(errorString);
}
}
}
}

View File

@ -18,6 +18,7 @@ using System.Threading.Tasks;
using Awake.Core;
using Awake.Core.Models;
using Awake.Core.Native;
using Awake.Properties;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
@ -25,13 +26,7 @@ namespace Awake
{
internal sealed class Program
{
// PowerToys Awake build code name. Used for exact logging
// that does not map to PowerToys broad version schema to pinpoint
// internal issues easier.
// Format of the build ID is: CODENAME_MMDDYYYY, where MMDDYYYY
// is representative of the date when the last change was made before
// the pull request is issued.
private static readonly string BuildId = "ATRIOX_04132023";
private static readonly ManualResetEvent _exitSignal = new(false);
private static Mutex? _mutex;
private static FileSystemWatcher? _watcher;
@ -46,12 +41,11 @@ namespace Awake
private static SystemPowerCapabilities _powerCapabilities;
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
private static ManualResetEvent _exitSignal = new ManualResetEvent(false);
internal static readonly string[] AliasesConfigOption = new[] { "--use-pt-config", "-c" };
internal static readonly string[] AliasesDisplayOption = new[] { "--display-on", "-d" };
internal static readonly string[] AliasesTimeOption = new[] { "--time-limit", "-t" };
internal static readonly string[] AliasesPidOption = new[] { "--pid", "-p" };
internal static readonly string[] AliasesExpireAtOption = new[] { "--expire-at", "-e" };
internal static readonly string[] AliasesConfigOption = ["--use-pt-config", "-c"];
internal static readonly string[] AliasesDisplayOption = ["--display-on", "-d"];
internal static readonly string[] AliasesTimeOption = ["--time-limit", "-t"];
internal static readonly string[] AliasesPidOption = ["--pid", "-p"];
internal static readonly string[] AliasesExpireAtOption = ["--expire-at", "-e"];
private static int Main(string[] args)
{
@ -73,7 +67,7 @@ namespace Awake
Logger.LogInfo($"Launching {Core.Constants.AppName}...");
Logger.LogInfo(FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).FileVersion);
Logger.LogInfo($"Build: {BuildId}");
Logger.LogInfo($"Build: {Core.Constants.BuildId}");
Logger.LogInfo($"OS: {Environment.OSVersion}");
Logger.LogInfo($"OS Build: {Manager.GetOperatingSystemBuild()}");
@ -90,69 +84,47 @@ namespace Awake
Logger.LogInfo("Parsing parameters...");
Option<bool> configOption = new(
aliases: AliasesConfigOption,
getDefaultValue: () => false,
description: $"Specifies whether {Core.Constants.AppName} will be using the PowerToys configuration file for managing the state.")
var configOption = new Option<bool>(AliasesConfigOption, () => false, Resources.AWAKE_CMD_HELP_CONFIG_OPTION)
{
Arity = ArgumentArity.ZeroOrOne,
IsRequired = false,
};
Option<bool> displayOption = new(
aliases: AliasesDisplayOption,
getDefaultValue: () => true,
description: "Determines whether the display should be kept awake.")
var displayOption = new Option<bool>(AliasesDisplayOption, () => true, Resources.AWAKE_CMD_HELP_DISPLAY_OPTION)
{
Arity = ArgumentArity.ZeroOrOne,
IsRequired = false,
};
Option<uint> timeOption = new(
aliases: AliasesTimeOption,
getDefaultValue: () => 0,
description: "Determines the interval, in seconds, during which the computer is kept awake.")
var timeOption = new Option<uint>(AliasesTimeOption, () => 0, Resources.AWAKE_CMD_HELP_TIME_OPTION)
{
Arity = ArgumentArity.ExactlyOne,
IsRequired = false,
};
Option<int> pidOption = new(
aliases: AliasesPidOption,
getDefaultValue: () => 0,
description: $"Bind the execution of {Core.Constants.AppName} to another process. When the process ends, the system will resume managing the current sleep and display state.")
var pidOption = new Option<int>(AliasesPidOption, () => 0, Resources.AWAKE_CMD_HELP_PID_OPTION)
{
Arity = ArgumentArity.ZeroOrOne,
IsRequired = false,
};
Option<string> expireAtOption = new(
aliases: AliasesExpireAtOption,
getDefaultValue: () => string.Empty,
description: $"Determines the end date/time when {Core.Constants.AppName} will back off and let the system manage the current sleep and display state.")
var expireAtOption = new Option<string>(AliasesExpireAtOption, () => string.Empty, Resources.AWAKE_CMD_HELP_EXPIRE_AT_OPTION)
{
Arity = ArgumentArity.ZeroOrOne,
IsRequired = false,
};
RootCommand? rootCommand = new()
{
RootCommand? rootCommand =
[
configOption,
displayOption,
timeOption,
pidOption,
expireAtOption,
};
];
rootCommand.Description = Core.Constants.AppName;
rootCommand.SetHandler(
HandleCommandLineArguments,
configOption,
displayOption,
timeOption,
pidOption,
expireAtOption);
rootCommand.SetHandler(HandleCommandLineArguments, configOption, displayOption, timeOption, pidOption, expireAtOption);
return rootCommand.InvokeAsync(args).Result;
}
@ -160,7 +132,7 @@ namespace Awake
private static bool ExitHandler(ControlType ctrlType)
{
Logger.LogInfo($"Exited through handler with control type: {ctrlType}");
Exit("Exiting from the internal termination handler.", Environment.ExitCode, _exitSignal);
Exit(Resources.AWAKE_EXIT_MESSAGE, Environment.ExitCode, _exitSignal);
return false;
}
@ -201,27 +173,30 @@ namespace Awake
{
// Configuration file is used, therefore we disregard any other command-line parameter
// and instead watch for changes in the file.
Manager.IsUsingPowerToysConfig = true;
try
{
var eventHandle = new EventWaitHandle(false, EventResetMode.ManualReset, interop.Constants.AwakeExitEvent());
new Thread(() =>
{
if (WaitHandle.WaitAny(new WaitHandle[] { _exitSignal, eventHandle }) == 1)
if (WaitHandle.WaitAny([_exitSignal, eventHandle]) == 1)
{
Exit("Received a signal to end the process. Making sure we quit...", 0, _exitSignal, true);
Exit(Resources.AWAKE_EXIT_SIGNAL_MESSAGE, 0, _exitSignal, true);
}
}).Start();
TrayHelper.InitializeTray(Core.Constants.FullAppName, new Icon(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assets/Awake/awake.ico")), _exitSignal);
string? settingsPath = _settingsUtils!.GetSettingsFilePath(Core.Constants.AppName);
Logger.LogInfo($"Reading configuration file: {settingsPath}");
if (!File.Exists(settingsPath))
{
string? errorString = $"The settings file does not exist. Scaffolding default configuration...";
Logger.LogError("The settings file does not exist. Scaffolding default configuration...");
AwakeSettings scaffoldSettings = new AwakeSettings();
AwakeSettings scaffoldSettings = new();
_settingsUtils.SaveSettings(JsonSerializer.Serialize(scaffoldSettings), Core.Constants.AppName);
}
@ -229,8 +204,7 @@ namespace Awake
}
catch (Exception ex)
{
string? errorString = $"There was a problem with the configuration file. Make sure it exists.\n{ex.Message}";
Logger.LogError(errorString);
Logger.LogError($"There was a problem with the configuration file. Make sure it exists.\n{ex.Message}");
}
}
else
@ -241,24 +215,13 @@ namespace Awake
{
try
{
DateTime expirationDateTime = DateTime.Parse(expireAt, CultureInfo.CurrentCulture);
if (expirationDateTime > DateTime.Now)
{
// We want to have a dedicated expirable keep-awake logic instead of
// converting the target date to seconds and then passing to SetupTimedKeepAwake
// because that way we're accounting for the user potentially changing their clock
// while Awake is running.
DateTimeOffset expirationDateTime = DateTimeOffset.Parse(expireAt, CultureInfo.CurrentCulture);
Logger.LogInfo($"Operating in thread ID {Environment.CurrentManagedThreadId}.");
SetupExpirableKeepAwake(expirationDateTime, displayOn);
}
else
{
Logger.LogInfo($"Target date is not in the future, therefore there is nothing to wait for.");
}
Manager.SetExpirableKeepAwake(expirationDateTime, displayOn);
}
catch (Exception ex)
{
Logger.LogError($"Could not parse date string {expireAt} into a viable date.");
Logger.LogError($"Could not parse date string {expireAt} into a DateTimeOffset object.");
Logger.LogError(ex.Message);
}
}
@ -268,11 +231,11 @@ namespace Awake
if (mode == AwakeMode.INDEFINITE)
{
SetupIndefiniteKeepAwake(displayOn);
Manager.SetIndefiniteKeepAwake(displayOn);
}
else
{
SetupTimedKeepAwake(timeLimit, displayOn);
Manager.SetTimedKeepAwake(timeLimit, displayOn);
}
}
}
@ -282,7 +245,7 @@ namespace Awake
RunnerHelper.WaitForPowerToysRunner(pid, () =>
{
Logger.LogInfo($"Triggered PID-based exit handler for PID {pid}.");
Exit("Terminating from process binding hook.", 0, _exitSignal, true);
Exit(Resources.AWAKE_EXIT_BINDING_HOOK_MESSAGE, 0, _exitSignal, true);
});
}
@ -293,33 +256,34 @@ namespace Awake
{
try
{
var directory = Path.GetDirectoryName(settingsPath)!;
var fileName = Path.GetFileName(settingsPath);
_watcher = new FileSystemWatcher
{
Path = Path.GetDirectoryName(settingsPath)!,
Path = directory,
EnableRaisingEvents = true,
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.CreationTime,
Filter = Path.GetFileName(settingsPath),
Filter = fileName,
};
IObservable<System.Reactive.EventPattern<FileSystemEventArgs>>? changedObservable = Observable.FromEventPattern<FileSystemEventHandler, FileSystemEventArgs>(
var mergedObservable = Observable.Merge(
Observable.FromEventPattern<FileSystemEventHandler, FileSystemEventArgs>(
h => _watcher.Changed += h,
h => _watcher.Changed -= h);
h => _watcher.Changed -= h),
Observable.FromEventPattern<FileSystemEventHandler, FileSystemEventArgs>(
h => _watcher.Created += h,
h => _watcher.Created -= h));
IObservable<System.Reactive.EventPattern<FileSystemEventArgs>>? createdObservable = Observable.FromEventPattern<FileSystemEventHandler, FileSystemEventArgs>(
cre => _watcher.Created += cre,
cre => _watcher.Created -= cre);
IObservable<System.Reactive.EventPattern<FileSystemEventArgs>>? mergedObservable = Observable.Merge(changedObservable, createdObservable);
mergedObservable.Throttle(TimeSpan.FromMilliseconds(25))
mergedObservable
.Throttle(TimeSpan.FromMilliseconds(25))
.SubscribeOn(TaskPoolScheduler.Default)
.Select(e => e.EventArgs)
.Subscribe(HandleAwakeConfigChange);
TrayHelper.SetTray(Core.Constants.FullAppName, new AwakeSettings(), _startedFromPowerToys);
var settings = Manager.ModuleSettings!.GetSettings<AwakeSettings>(Core.Constants.AppName) ?? new AwakeSettings();
TrayHelper.SetTray(settings, _startedFromPowerToys);
// Initially the file might not be updated, so we need to start processing
// settings right away.
ProcessSettings();
}
catch (Exception ex)
@ -328,99 +292,64 @@ namespace Awake
}
}
private static void SetupIndefiniteKeepAwake(bool displayOn)
{
Manager.SetIndefiniteKeepAwake(displayOn);
}
private static void HandleAwakeConfigChange(FileSystemEventArgs fileEvent)
{
try
{
Logger.LogInfo("Detected a settings file change. Updating configuration...");
Logger.LogInfo("Resetting keep-awake to normal state due to settings change.");
ProcessSettings();
}
catch (Exception e)
{
Logger.LogError($"Could not handle Awake configuration change. Error: {e.Message}");
}
}
private static void ProcessSettings()
{
try
{
AwakeSettings settings = _settingsUtils!.GetSettings<AwakeSettings>(Core.Constants.AppName);
if (settings != null)
{
var settings = _settingsUtils!.GetSettings<AwakeSettings>(Core.Constants.AppName) ?? throw new InvalidOperationException("Settings are null.");
Logger.LogInfo($"Identified custom time shortcuts for the tray: {settings.Properties.CustomTrayTimes.Count}");
switch (settings.Properties.Mode)
{
case AwakeMode.PASSIVE:
{
SetupNoKeepAwake();
Manager.SetPassiveKeepAwake();
break;
}
case AwakeMode.INDEFINITE:
{
SetupIndefiniteKeepAwake(settings.Properties.KeepDisplayOn);
Manager.SetIndefiniteKeepAwake(settings.Properties.KeepDisplayOn);
break;
}
case AwakeMode.TIMED:
{
uint computedTime = (settings.Properties.IntervalHours * 60 * 60) + (settings.Properties.IntervalMinutes * 60);
SetupTimedKeepAwake(computedTime, settings.Properties.KeepDisplayOn);
Manager.SetTimedKeepAwake(computedTime, settings.Properties.KeepDisplayOn);
break;
}
case AwakeMode.EXPIRABLE:
// When we are loading from the settings file, let's make sure that we never
// get users in a state where the expirable keep-awake is in the past.
if (settings.Properties.ExpirationDateTime <= DateTimeOffset.Now)
{
SetupExpirableKeepAwake(settings.Properties.ExpirationDateTime, settings.Properties.KeepDisplayOn);
break;
settings.Properties.ExpirationDateTime = DateTimeOffset.Now.AddMinutes(5);
_settingsUtils.SaveSettings(JsonSerializer.Serialize(settings), Core.Constants.AppName);
}
Manager.SetExpirableKeepAwake(settings.Properties.ExpirationDateTime, settings.Properties.KeepDisplayOn);
break;
default:
{
string? errorMessage = "Unknown mode of operation. Check config file.";
Logger.LogError(errorMessage);
Logger.LogError("Unknown mode of operation. Check config file.");
break;
}
}
TrayHelper.SetTray(Core.Constants.FullAppName, settings, _startedFromPowerToys);
}
else
{
string? errorMessage = "Settings are null.";
Logger.LogError(errorMessage);
}
TrayHelper.SetTray(settings, _startedFromPowerToys);
}
catch (Exception ex)
{
string? errorMessage = $"There was a problem reading the configuration file. Error: {ex.GetType()} {ex.Message}";
Logger.LogError(errorMessage);
Logger.LogError($"There was a problem reading the configuration file. Error: {ex.GetType()} {ex.Message}");
}
}
private static void SetupNoKeepAwake()
{
Logger.LogInfo($"Operating in passive mode (computer's standard power plan). No custom keep awake settings enabled.");
Manager.SetNoKeepAwake();
}
private static void SetupExpirableKeepAwake(DateTimeOffset expireAt, bool displayOn)
{
Logger.LogInfo($"Expirable keep-awake. Expected expiration date/time: {expireAt} with display on setting set to {displayOn}.");
Manager.SetExpirableKeepAwake(expireAt, displayOn);
}
private static void SetupTimedKeepAwake(uint time, bool displayOn)
{
Logger.LogInfo($"Timed keep-awake. Expected runtime: {time} seconds with display on setting set to {displayOn}.");
Manager.SetTimedKeepAwake(time, displayOn);
}
}
}

View File

@ -60,24 +60,6 @@ namespace Awake.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to 1 hour.
/// </summary>
internal static string AWAKE_1_HOUR {
get {
return ResourceManager.GetString("AWAKE_1_HOUR", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to 1 minute.
/// </summary>
internal static string AWAKE_1_MINUTE {
get {
return ResourceManager.GetString("AWAKE_1_MINUTE", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Checked.
/// </summary>
@ -87,6 +69,51 @@ namespace Awake.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Specifies whether Awake will be using the PowerToys configuration file for managing the state..
/// </summary>
internal static string AWAKE_CMD_HELP_CONFIG_OPTION {
get {
return ResourceManager.GetString("AWAKE_CMD_HELP_CONFIG_OPTION", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Determines whether the display should be kept awake..
/// </summary>
internal static string AWAKE_CMD_HELP_DISPLAY_OPTION {
get {
return ResourceManager.GetString("AWAKE_CMD_HELP_DISPLAY_OPTION", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Determines the end date and time when Awake will back off and let the system manage the current sleep and display state..
/// </summary>
internal static string AWAKE_CMD_HELP_EXPIRE_AT_OPTION {
get {
return ResourceManager.GetString("AWAKE_CMD_HELP_EXPIRE_AT_OPTION", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Bind the execution of Awake to another process. When the process ends, the system will resume managing the current sleep and display state..
/// </summary>
internal static string AWAKE_CMD_HELP_PID_OPTION {
get {
return ResourceManager.GetString("AWAKE_CMD_HELP_PID_OPTION", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Determines the interval (in seconds) during which the computer is kept awake..
/// </summary>
internal static string AWAKE_CMD_HELP_TIME_OPTION {
get {
return ResourceManager.GetString("AWAKE_CMD_HELP_TIME_OPTION", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Exit.
/// </summary>
@ -96,6 +123,33 @@ namespace Awake.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Terminating from process binding hook..
/// </summary>
internal static string AWAKE_EXIT_BINDING_HOOK_MESSAGE {
get {
return ResourceManager.GetString("AWAKE_EXIT_BINDING_HOOK_MESSAGE", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Exiting from the internal termination handler..
/// </summary>
internal static string AWAKE_EXIT_MESSAGE {
get {
return ResourceManager.GetString("AWAKE_EXIT_MESSAGE", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Received a signal to end the process. Making sure we quit....
/// </summary>
internal static string AWAKE_EXIT_SIGNAL_MESSAGE {
get {
return ResourceManager.GetString("AWAKE_EXIT_SIGNAL_MESSAGE", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} hours.
/// </summary>
@ -159,6 +213,42 @@ namespace Awake.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Expiring.
/// </summary>
internal static string AWAKE_TRAY_TEXT_EXPIRATION {
get {
return ResourceManager.GetString("AWAKE_TRAY_TEXT_EXPIRATION", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Indefinite.
/// </summary>
internal static string AWAKE_TRAY_TEXT_INDEFINITE {
get {
return ResourceManager.GetString("AWAKE_TRAY_TEXT_INDEFINITE", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Passive.
/// </summary>
internal static string AWAKE_TRAY_TEXT_OFF {
get {
return ResourceManager.GetString("AWAKE_TRAY_TEXT_OFF", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Timed.
/// </summary>
internal static string AWAKE_TRAY_TEXT_TIMED {
get {
return ResourceManager.GetString("AWAKE_TRAY_TEXT_TIMED", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Unchecked.
/// </summary>

View File

@ -123,9 +123,6 @@
<data name="AWAKE_EXIT" xml:space="preserve">
<value>Exit</value>
</data>
<data name="AWAKE_1_HOUR" xml:space="preserve">
<value>1 hour</value>
</data>
<data name="AWAKE_HOURS" xml:space="preserve">
<value>{0} hours</value>
<comment>{0} shouldn't be removed. It will be replaced by a number greater than 1 at runtime by the application. Used for defining a period to keep the PC awake.</comment>
@ -145,9 +142,6 @@
<value>Keep awake until expiration date and time</value>
<comment>Keep the system awake until expiration date and time</comment>
</data>
<data name="AWAKE_1_MINUTE" xml:space="preserve">
<value>1 minute</value>
</data>
<data name="AWAKE_MINUTES" xml:space="preserve">
<value>{0} minutes</value>
<comment>{0} shouldn't be removed. It will be replaced by a number greater than 1 at runtime by the application. Used for defining a period to keep the PC awake.</comment>
@ -159,4 +153,40 @@
<data name="AWAKE_UNCHECKED" xml:space="preserve">
<value>Unchecked</value>
</data>
<data name="AWAKE_CMD_HELP_CONFIG_OPTION" xml:space="preserve">
<value>Specifies whether Awake will be using the PowerToys configuration file for managing the state.</value>
</data>
<data name="AWAKE_CMD_HELP_DISPLAY_OPTION" xml:space="preserve">
<value>Determines whether the display should be kept awake.</value>
</data>
<data name="AWAKE_CMD_HELP_EXPIRE_AT_OPTION" xml:space="preserve">
<value>Determines the end date and time when Awake will back off and let the system manage the current sleep and display state.</value>
</data>
<data name="AWAKE_CMD_HELP_PID_OPTION" xml:space="preserve">
<value>Bind the execution of Awake to another process. When the process ends, the system will resume managing the current sleep and display state.</value>
</data>
<data name="AWAKE_CMD_HELP_TIME_OPTION" xml:space="preserve">
<value>Determines the interval (in seconds) during which the computer is kept awake.</value>
</data>
<data name="AWAKE_EXIT_BINDING_HOOK_MESSAGE" xml:space="preserve">
<value>Terminating from process binding hook.</value>
</data>
<data name="AWAKE_EXIT_MESSAGE" xml:space="preserve">
<value>Exiting from the internal termination handler.</value>
</data>
<data name="AWAKE_EXIT_SIGNAL_MESSAGE" xml:space="preserve">
<value>Received a signal to end the process. Making sure we quit...</value>
</data>
<data name="AWAKE_TRAY_TEXT_EXPIRATION" xml:space="preserve">
<value>Expiring</value>
</data>
<data name="AWAKE_TRAY_TEXT_INDEFINITE" xml:space="preserve">
<value>Indefinite</value>
</data>
<data name="AWAKE_TRAY_TEXT_OFF" xml:space="preserve">
<value>Passive</value>
</data>
<data name="AWAKE_TRAY_TEXT_TIMED" xml:space="preserve">
<value>Timed</value>
</data>
</root>

View File

@ -0,0 +1,14 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.PowerToys.Settings.UI.Library
{
public enum AwakeMode
{
PASSIVE = 0,
INDEFINITE = 1,
TIMED = 2,
EXPIRABLE = 3,
}
}

View File

@ -18,7 +18,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
IntervalHours = 0;
IntervalMinutes = 1;
ExpirationDateTime = DateTimeOffset.Now;
CustomTrayTimes = new Dictionary<string, int>();
CustomTrayTimes = [];
}
[JsonPropertyName("keepDisplayOn")]
@ -40,12 +40,4 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[CmdConfigureIgnoreAttribute]
public Dictionary<string, int> CustomTrayTimes { get; set; }
}
public enum AwakeMode
{
PASSIVE = 0,
INDEFINITE = 1,
TIMED = 2,
EXPIRABLE = 3,
}
}

View File

@ -4,6 +4,7 @@
using System;
using System.Linq;
using System.Reflection;
using System.Text.Json.Serialization;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
@ -12,12 +13,11 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public class AwakeSettings : BasePTModuleSettings, ISettingsConfig, ICloneable
{
public const string ModuleName = "Awake";
public const string ModuleVersion = "0.0.2";
public AwakeSettings()
{
Name = ModuleName;
Version = ModuleVersion;
Version = Assembly.GetExecutingAssembly().GetName().Version.ToString();
Properties = new AwakeProperties();
}

View File

@ -27,7 +27,7 @@
x:Uid="Awake_EnableSettingsCard"
HeaderIcon="{ui:BitmapIcon Source=/Assets/Settings/Icons/Awake.png}"
IsEnabled="{x:Bind ViewModel.IsEnabledGpoConfigured, Mode=OneWay, Converter={StaticResource BoolNegationConverter}}">
<ToggleSwitch x:Uid="ToggleSwitch" IsOn="{x:Bind ViewModel.IsEnabled, Mode=TwoWay}" />
<ToggleSwitch IsOn="{x:Bind ViewModel.IsEnabled, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<InfoBar
x:Uid="GPO_SettingIsManaged"

View File

@ -7,7 +7,6 @@ using System.Runtime.CompilerServices;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Utilities;
namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
@ -66,20 +65,11 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
public bool IsExpirationConfigurationEnabled
{
get => ModuleSettings.Properties.Mode == AwakeMode.EXPIRABLE && IsEnabled;
}
public bool IsExpirationConfigurationEnabled => ModuleSettings.Properties.Mode == AwakeMode.EXPIRABLE && IsEnabled;
public bool IsTimeConfigurationEnabled
{
get => ModuleSettings.Properties.Mode == AwakeMode.TIMED && IsEnabled;
}
public bool IsTimeConfigurationEnabled => ModuleSettings.Properties.Mode == AwakeMode.TIMED && IsEnabled;
public bool IsScreenConfigurationPossibleEnabled
{
get => ModuleSettings.Properties.Mode != AwakeMode.PASSIVE && IsEnabled;
}
public bool IsScreenConfigurationPossibleEnabled => ModuleSettings.Properties.Mode != AwakeMode.PASSIVE && IsEnabled;
public AwakeMode Mode
{
@ -90,6 +80,26 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
ModuleSettings.Properties.Mode = value;
if (value == AwakeMode.TIMED && IntervalMinutes == 0 && IntervalHours == 0)
{
// Handle the special case where both hours and minutes are zero.
// Otherwise, this will reset to passive very quickly in the UI.
ModuleSettings.Properties.IntervalMinutes = 1;
OnPropertyChanged(nameof(IntervalMinutes));
}
else if (value == AwakeMode.EXPIRABLE && ExpirationDateTime <= DateTimeOffset.Now)
{
// To make sure that we're not tracking expirable keep-awake in the past,
// let's make sure that every time it's enabled from the settings UI, it's
// five (5) minutes into the future.
ExpirationDateTime = DateTimeOffset.Now.AddMinutes(5);
// The expiration date/time is updated and will send the notification
// but we need to do this manually for the expiration time that is
// bound to the time control on the settings page.
OnPropertyChanged(nameof(ExpirationTime));
}
OnPropertyChanged(nameof(IsTimeConfigurationEnabled));
OnPropertyChanged(nameof(IsScreenConfigurationPossibleEnabled));
OnPropertyChanged(nameof(IsExpirationConfigurationEnabled));