diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index c85d35fc51c9..966e7ad8fcf2 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -59,7 +59,6 @@ APeriod api APIENTRY APIIs -apos APPBARDATA appcontainer appdata @@ -87,6 +86,7 @@ args argv Arial arik +Arity arjunbalgovind ARPINSTALLLOCATION ARPPRODUCTICON @@ -124,13 +124,13 @@ Autorun AUTOSIZECOLUMNS autoupdate AValid +AWAYMODE azurecr azurewebsites backend backtracer bak bbwe -bc bcc bck Bcl @@ -140,7 +140,6 @@ betsegaw BGR bgra BGSOUNDS -bh bhid Bicubic bigbar @@ -217,7 +216,6 @@ CHECKCANCELED CHILDACTIVATE CHILDWINDOW chrdavis -Chris's chrono Chrzan CHT @@ -226,7 +224,6 @@ cinttypes cla clangformat CLASSDC -classmethod classname CLASSNOTAVAILABLE clickable @@ -237,7 +234,7 @@ clientside CLIPCHILDREN CLIPSIBLINGS clrcall -cls +Cls CLSCTX clsid CMDARG @@ -251,7 +248,6 @@ CMINVOKECOMMANDINFOEX CMock CMONITORS cmyk -cn cnt coclass codebase @@ -285,6 +281,7 @@ config CONFLICTINGMODIFIERKEY CONFLICTINGMODIFIERSHORTCUT Connectquickaction +CONOUT Consolas constexpr contentdialog @@ -355,7 +352,6 @@ CUSTOMACTIONTEST cvd cwchar cwd -cx cxfksword CXSMICON CXVIRTUALSCREEN @@ -363,7 +359,6 @@ cxx cxxopts CYMK CYSMICON -cz cziplib Dac dacl @@ -428,7 +423,6 @@ devblogs devdocs devenum DEVMON -df DFactory diffing difftime @@ -481,7 +475,6 @@ dutil DVASPECT DVASPECTINFO DVTARGETDEVICE -dw DWindow DWINRT DWLP @@ -500,7 +493,6 @@ dworigin dwrite dxgi Easeof -EB ecef ecount EDB @@ -540,12 +532,12 @@ errorlevel ERRORMESSAGE ERRORTITLE esize +espressoversion estdir etcore etl etw EUQ -ev evenodd eventlog everytime @@ -586,7 +578,6 @@ FANCYZONESDRAWLAYOUTTEST FANCYZONESEDITOR Farbraum FARPROC -fd feimage ffcd FFDDDDDD @@ -614,7 +605,7 @@ Fle fluentui flyout fmtlib -fody +Fody FOF FOFX FOLDERID @@ -634,7 +625,6 @@ fullscreen func fwlink fwrite -fx fxcop gabime GAC @@ -652,12 +642,10 @@ GETDPISCALEDSIZE GETEMPTYMARKUP GETICON getline -getmembers GETMINMAXINFO GETSTATE GETTEXT GETTEXTLENGTH -gh github githubusercontent gitignore @@ -684,7 +672,6 @@ hbitmap hbmp hbr HBRUSH -hc hcblack hcwhite hdc @@ -698,7 +685,6 @@ HDS HEB helptext HGLOBAL -hh hhk HHmmss HHOOK @@ -710,7 +696,6 @@ hinst hinstance hitinfo HIWORD -hk HKCC HKCR HKCU @@ -755,7 +740,6 @@ Htmdid html htt http -hu hwb HWINEVENTHOOK hwnd @@ -908,7 +892,6 @@ iobjectwithsitesetsite IOle iolewindowcontextsensitivehelp iostream -ip IPackage IPath ipc @@ -945,7 +928,6 @@ isfinite IShell ISingle ISmart -ismethod isocpp IStorage IStream @@ -982,7 +964,6 @@ jfif jgeosdfsdsgmkedfgdfgdfgbkmhcgcflmi jjw jobject -jp jpe jpeg jpg @@ -1017,17 +998,16 @@ keystokes Keystool Keytool keyup -Kf KILLFOCUS Knownfolders Kybd LAlt -lambson +Lambson lamotile langword Lastdevice LASTEXITCODE -laute +Laute laviusmotileng LAYOUTRTL LBUTTON @@ -1071,7 +1051,6 @@ lmcons LMEM LMENU lnk -loadingbar LOCALAPPDATA LOCALDISPLAY localhost @@ -1080,6 +1059,8 @@ localport localtime LOCATIONCHANGE Lockyour +logconsole +logfile LOGFONT LOGMSG logon @@ -1178,7 +1159,6 @@ Mensching menuitem MENUITEMINFO MENUITEMINFOW -messagebox messageboxes METACHARSET metadata @@ -1194,6 +1174,7 @@ mimetype Minimizeallwindows MINIMIZEBOX miniz +minlevel MINMAXINFO Miracast MJPG @@ -1306,7 +1287,7 @@ niels nielslaute NIF NLD -NLog +nlog NLSTEXT NMLVEMPTYMARKUP NOACTIVATE @@ -1370,7 +1351,6 @@ NUMPAD nunit Nvidia NWSE -NX Objbase OBJID objidl @@ -1413,6 +1393,7 @@ otating OUTOFCONTEXT OUTOFMEMORY Outptr +outputtype outro outsettings OVERLAPPEDWINDOW @@ -1432,7 +1413,6 @@ PARENTRELATIVEPARSING parray PARTIALCONFIRMATIONDIALOGTITLE pathcch -pb pbc Pbgra pcb @@ -1476,7 +1456,6 @@ plugin pluginsmodel plvdi PMSIHANDLE -Pn png pnm pnmdr @@ -1562,15 +1541,11 @@ ptstr pubxml Pui pushd -pv PVOID -pw pwa pwcs PWSTR pwtd -px -QI qianlifeng qit QITAB @@ -1672,7 +1647,7 @@ roadmap Roboto roslyn royvou -rpc +Rpc RRF rshift Rsp @@ -1682,7 +1657,6 @@ RTB rtf Rtl RTLREADING -ru ruleset RUNACTIVEXCTLS runas @@ -1693,12 +1667,10 @@ runsettings runtimeclass runtimeconfig runtimes -rv RUS rvalue rvm rwin -rx ryanbodrug saahmedm sacl @@ -1823,6 +1795,7 @@ spdlog spdo spdth spec'ing +specialfolder spesi splitwstring sppd @@ -1927,8 +1900,7 @@ syslog SYSMENU systemd SYSTEMTIME -sz -tadele +Tadele Tahoma talynone TApp @@ -1950,7 +1922,6 @@ tbody tchar tcscpy TCustom -td TDevice Templated templatenamespace @@ -1987,6 +1958,7 @@ toggleswitch toolbar Toolchain toolset +toolstrip tooltip toolwindow TOPDOWNDIB @@ -2003,8 +1975,6 @@ trl trunc TStr tsx -tt -tw TYMED typedef TYPEKEY @@ -2013,7 +1983,6 @@ typename typeof typeparam TYPESHORTCUT -Tz UAC UAL uap @@ -2027,7 +1996,6 @@ uintptr UIPI UIs UITo -ul ULARGE ULLONG ulong @@ -2086,7 +2054,6 @@ utf utils uuid uuidof -uv uwp UWPUI uxtheme @@ -2108,7 +2075,6 @@ verrsrc VERSIONINFO Versioning VFT -vh vid VIDEOINFOHEADER viewbox @@ -2118,10 +2084,8 @@ visiblecolorformats Visibletrue visualbrush visualstudio -vk VKey VKTAB -vm vmax vmin VOS @@ -2137,8 +2101,6 @@ VSTHRD VSTT VTABLE Vtbl -vw -Vx watsonportal wav WBounds @@ -2193,6 +2155,8 @@ windowsx windowwalker winerror WINEVENT +winexe +winforms winfx winget Winhook @@ -2256,7 +2220,6 @@ WTSAT Wwan www wxs -xa xamarin xaml XAttribute @@ -2281,11 +2244,11 @@ XOffset xpath XResource xsd +xsi XSmall XStr XToolset xunit -XY Yaml YDiff YESNO @@ -2296,13 +2259,9 @@ YOffset YStr YUY YUYV -yy -Zc ZEROINIT -zh ZIndex zipfolder -zm zonable ZONECOLOR ZONEHIGHLIGHTCOLOR diff --git a/.github/actions/spell-check/patterns.txt b/.github/actions/spell-check/patterns.txt index 385cda1c0592..49a73f7b6c48 100644 --- a/.github/actions/spell-check/patterns.txt +++ b/.github/actions/spell-check/patterns.txt @@ -31,6 +31,8 @@ TestCase\("[^"]+" # Windows paths \\native +\\netcoreapp +\\netstandard \\notifications \\recyclebin \\reinstall @@ -41,6 +43,7 @@ TestCase\("[^"]+" \\restore \\result \\runner +\\runtimes \\Telemetry \\telemetry \\testapp diff --git a/.pipelines/pipeline.user.windows.yml b/.pipelines/pipeline.user.windows.yml index b435e4258d4e..d74791fa1640 100644 --- a/.pipelines/pipeline.user.windows.yml +++ b/.pipelines/pipeline.user.windows.yml @@ -75,6 +75,13 @@ build: - 'modules\ColorPicker\PowerToysInterop.dll' - 'modules\ColorPicker\Telemetry.dll' - '**\*.resources.dll' + - 'modules\Espresso\EspressoModuleInterface.dll' + - 'modules\Espresso\ManagedCommon.dll' + - 'modules\Espresso\ManagedTelemetry.dll' + - 'modules\Espresso\Microsoft.PowerToys.Settings.UI.Lib.dll' + - 'modules\Espresso\PowerToys.Espresso.exe' + - 'modules\Espresso\PowerToys.Espresso.dll' + - 'modules\Espresso\PowerToysInterop.dll' - 'modules\FancyZones\fancyzones.dll' - 'modules\FancyZones\FancyZonesEditor.exe' - 'modules\FancyZones\FancyZonesEditor.dll' @@ -102,6 +109,7 @@ build: - 'modules\KeyboardManager\KeyboardManagerEngine\PowerToys.KeyboardManagerEngine.exe' - 'modules\launcher\Microsoft.PowerToys.Settings.UI.Lib.dll' - 'modules\launcher\ManagedCommon.dll' + - 'modules\launcher\ManagedTelemetry.dll' - 'modules\launcher\Microsoft.PowerToys.Common.UI.dll' - 'modules\launcher\Microsoft.Launcher.dll' - 'modules\launcher\Plugins\Microsoft.PowerToys.Run.Plugin.Calculator\Microsoft.PowerToys.Run.Plugin.Calculator.dll' diff --git a/PowerToys.sln b/PowerToys.sln index c0799f590dcb..0aa7e6074b23 100644 --- a/PowerToys.sln +++ b/PowerToys.sln @@ -16,11 +16,13 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "runner", "src\runner\runner {BA58206B-1493-4C75-BFEA-A85768A1E156} = {BA58206B-1493-4C75-BFEA-A85768A1E156} {0B593A6C-4143-4337-860E-DB5710FB87DB} = {0B593A6C-4143-4337-860E-DB5710FB87DB} {E364F67B-BB12-4E91-B639-355866EBCD8B} = {E364F67B-BB12-4E91-B639-355866EBCD8B} + {D940E07F-532C-4FF3-883F-790DA014F19A} = {D940E07F-532C-4FF3-883F-790DA014F19A} {DA425894-6E13-404F-8DCB-78584EC0557A} = {DA425894-6E13-404F-8DCB-78584EC0557A} {2BE46397-4DFA-414C-9BD4-41E4BBF8CB34} = {2BE46397-4DFA-414C-9BD4-41E4BBF8CB34} {A7D5099E-F0FD-4BF3-8522-5A682759F915} = {A7D5099E-F0FD-4BF3-8522-5A682759F915} {0B43679E-EDFA-4DA0-AD30-F4628B308B1B} = {0B43679E-EDFA-4DA0-AD30-F4628B308B1B} {B25AC7A5-FB9F-4789-B392-D5C85E948670} = {B25AC7A5-FB9F-4789-B392-D5C85E948670} + {5E7360A8-D048-4ED3-8F09-0BFD64C5529A} = {5E7360A8-D048-4ED3-8F09-0BFD64C5529A} {AF2349B8-E5B6-4004-9502-687C1C7730B1} = {AF2349B8-E5B6-4004-9502-687C1C7730B1} {17DA04DF-E393-4397-9CF0-84DABE11032E} = {17DA04DF-E393-4397-9CF0-84DABE11032E} {F9C68EDF-AC74-4B77-9AF1-005D9C9F6A99} = {F9C68EDF-AC74-4B77-9AF1-005D9C9F6A99} @@ -321,6 +323,12 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "KeyboardManagerEditorLibrar EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "KeyboardManagerEditorTest", "src\modules\keyboardmanager\KeyboardManagerEditorTest\KeyboardManagerEditorTest.vcxproj", "{62173D9A-6724-4C00-A1C8-FB646480A9EC}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "espresso", "espresso", "{127F38E0-40AA-4594-B955-5616BF206882}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "EspressoModuleInterface", "src\modules\espresso\EspressoModuleInterface\EspressoModuleInterface.vcxproj", "{5E7360A8-D048-4ED3-8F09-0BFD64C5529A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Espresso", "src\modules\espresso\Espresso\Espresso.csproj", "{D940E07F-532C-4FF3-883F-790DA014F19A}" +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "shortcutguide", "shortcutguide", "{106CBECA-0701-4FC3-838C-9DF816A19AE2}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ShortcutGuideModuleInterface", "src\modules\ShortcutGuide\ShortcutGuideModuleInterface\ShortcutGuideModuleInterface.vcxproj", "{2D604C07-51FC-46BB-9EB7-75AECC7F5E81}" @@ -659,6 +667,14 @@ Global {62173D9A-6724-4C00-A1C8-FB646480A9EC}.Debug|x64.Build.0 = Debug|x64 {62173D9A-6724-4C00-A1C8-FB646480A9EC}.Release|x64.ActiveCfg = Release|x64 {62173D9A-6724-4C00-A1C8-FB646480A9EC}.Release|x64.Build.0 = Release|x64 + {5E7360A8-D048-4ED3-8F09-0BFD64C5529A}.Debug|x64.ActiveCfg = Debug|x64 + {5E7360A8-D048-4ED3-8F09-0BFD64C5529A}.Debug|x64.Build.0 = Debug|x64 + {5E7360A8-D048-4ED3-8F09-0BFD64C5529A}.Release|x64.ActiveCfg = Release|x64 + {5E7360A8-D048-4ED3-8F09-0BFD64C5529A}.Release|x64.Build.0 = Release|x64 + {D940E07F-532C-4FF3-883F-790DA014F19A}.Debug|x64.ActiveCfg = Debug|x64 + {D940E07F-532C-4FF3-883F-790DA014F19A}.Debug|x64.Build.0 = Debug|x64 + {D940E07F-532C-4FF3-883F-790DA014F19A}.Release|x64.ActiveCfg = Release|x64 + {D940E07F-532C-4FF3-883F-790DA014F19A}.Release|x64.Build.0 = Release|x64 {2D604C07-51FC-46BB-9EB7-75AECC7F5E81}.Debug|x64.ActiveCfg = Debug|x64 {2D604C07-51FC-46BB-9EB7-75AECC7F5E81}.Debug|x64.Build.0 = Debug|x64 {2D604C07-51FC-46BB-9EB7-75AECC7F5E81}.Release|x64.ActiveCfg = Release|x64 @@ -765,6 +781,9 @@ Global {8DF78B53-200E-451F-9328-01EB907193AE} = {38BDB927-829B-4C65-9CD9-93FB05D66D65} {23D2070D-E4AD-4ADD-85A7-083D9C76AD49} = {38BDB927-829B-4C65-9CD9-93FB05D66D65} {62173D9A-6724-4C00-A1C8-FB646480A9EC} = {38BDB927-829B-4C65-9CD9-93FB05D66D65} + {127F38E0-40AA-4594-B955-5616BF206882} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC} + {5E7360A8-D048-4ED3-8F09-0BFD64C5529A} = {127F38E0-40AA-4594-B955-5616BF206882} + {D940E07F-532C-4FF3-883F-790DA014F19A} = {127F38E0-40AA-4594-B955-5616BF206882} {106CBECA-0701-4FC3-838C-9DF816A19AE2} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC} {2D604C07-51FC-46BB-9EB7-75AECC7F5E81} = {106CBECA-0701-4FC3-838C-9DF816A19AE2} {2EDB3EB4-FA92-4BFF-B2D8-566584837231} = {106CBECA-0701-4FC3-838C-9DF816A19AE2} diff --git a/README.md b/README.md index 38e108c76333..e36998c3a879 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Microsoft PowerToys is a set of utilities for power users to tune and streamline | [Color Picker](https://aka.ms/PowerToysOverview_ColorPicker) | [FancyZones](https://aka.ms/PowerToysOverview_FancyZones) | [File Explorer Add-ons](https://aka.ms/PowerToysOverview_FileExplorerAddOns) | | [Image Resizer](https://aka.ms/PowerToysOverview_ImageResizer) | [Keyboard Manager](https://aka.ms/PowerToysOverview_KeyboardManager) | [PowerRename](https://aka.ms/PowerToysOverview_PowerRename) | | [PowerToys Run](https://aka.ms/PowerToysOverview_PowerToysRun) | [Shortcut Guide](https://aka.ms/PowerToysOverview_ShortcutGuide) | [Video Conference Mute (Experimental)](https://aka.ms/PowerToysOverview_VideoConference) | - +| espresso | | | ## Installing and running Microsoft PowerToys ### Requirements diff --git a/doc/images/icons/Espresso.png b/doc/images/icons/Espresso.png new file mode 100644 index 000000000000..2ff48cb39e49 Binary files /dev/null and b/doc/images/icons/Espresso.png differ diff --git a/doc/images/overview/Espresso_large.png b/doc/images/overview/Espresso_large.png new file mode 100644 index 000000000000..d573613513fb Binary files /dev/null and b/doc/images/overview/Espresso_large.png differ diff --git a/doc/images/overview/Espresso_small.png b/doc/images/overview/Espresso_small.png new file mode 100644 index 000000000000..4964ab73300c Binary files /dev/null and b/doc/images/overview/Espresso_small.png differ diff --git a/doc/images/overview/Original/Espresso.png b/doc/images/overview/Original/Espresso.png new file mode 100644 index 000000000000..fe30ac7f40ef Binary files /dev/null and b/doc/images/overview/Original/Espresso.png differ diff --git a/installer/PowerToysSetup/Product.wxs b/installer/PowerToysSetup/Product.wxs index 9c45963e4f3b..bc8881766379 100644 --- a/installer/PowerToysSetup/Product.wxs +++ b/installer/PowerToysSetup/Product.wxs @@ -2,11 +2,13 @@ + + @@ -218,16 +220,37 @@ + + + + + + + + + + + + + + + + + + + + + @@ -272,9 +295,10 @@ - + + @@ -361,6 +385,7 @@ + @@ -378,6 +403,8 @@ + + @@ -483,6 +510,7 @@ + @@ -512,6 +540,7 @@ + + @@ -588,6 +618,7 @@ + @@ -600,24 +631,28 @@ + + + + @@ -626,6 +661,7 @@ + @@ -633,7 +669,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -711,22 +779,22 @@ - - + + - - + + - - + + @@ -813,6 +881,10 @@ + + + + diff --git a/src/common/SettingsAPI/settings_helpers.h b/src/common/SettingsAPI/settings_helpers.h index 1174032fe28e..cd8f39432b1c 100644 --- a/src/common/SettingsAPI/settings_helpers.h +++ b/src/common/SettingsAPI/settings_helpers.h @@ -8,6 +8,7 @@ namespace PTSettingsHelper { constexpr inline const wchar_t* log_settings_filename = L"log_settings.json"; + std::wstring get_module_save_file_location(std::wstring_view powertoy_key); std::wstring get_module_save_folder_location(std::wstring_view powertoy_name); std::wstring get_root_save_folder_location(); diff --git a/src/common/logger/logger_settings.h b/src/common/logger/logger_settings.h index 5398e2e39b65..c7268fb088ee 100644 --- a/src/common/logger/logger_settings.h +++ b/src/common/logger/logger_settings.h @@ -13,6 +13,7 @@ struct LogSettings inline const static std::wstring actionRunnerLogPath = L"RunnerLogs\\action-runner-log.txt"; inline const static std::string launcherLoggerName = "launcher"; inline const static std::wstring launcherLogPath = L"LogsModuleInterface\\launcher-log.txt"; + inline const static std::wstring espressoLogPath = L"Logs\\espresso-log.txt"; inline const static std::string fancyZonesLoggerName = "fancyzones"; inline const static std::wstring fancyZonesLogPath = L"fancyzones-log.txt"; inline const static std::wstring fancyZonesOldLogPath = L"FancyZonesLogs\\"; // needed to clean up old logs diff --git a/src/modules/espresso/Espresso/Core/APIHelper.cs b/src/modules/espresso/Espresso/Core/APIHelper.cs new file mode 100644 index 000000000000..0c0fb85fe51f --- /dev/null +++ b/src/modules/espresso/Espresso/Core/APIHelper.cs @@ -0,0 +1,280 @@ +// 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.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Win32; +using NLog; + +namespace Espresso.Shell.Core +{ + [Flags] + public enum EXECUTION_STATE : uint + { + ES_AWAYMODE_REQUIRED = 0x00000040, + ES_CONTINUOUS = 0x80000000, + ES_DISPLAY_REQUIRED = 0x00000002, + ES_SYSTEM_REQUIRED = 0x00000001, + } + + /// + /// Helper class that allows talking to Win32 APIs without having to rely on PInvoke in other parts + /// of the codebase. + /// + public class APIHelper + { + private const string BuildRegistryLocation = @"SOFTWARE\Microsoft\Windows NT\CurrentVersion"; + private const int StdOutputHandle = -11; + private const uint GenericWrite = 0x40000000; + private const uint GenericRead = 0x80000000; + + private static readonly Logger _log; + private static CancellationTokenSource _tokenSource; + private static CancellationToken _threadToken; + + private static Task? _runnerThread; + + [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] + private static extern EXECUTION_STATE SetThreadExecutionState(EXECUTION_STATE esFlags); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool AllocConsole(); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool SetStdHandle(int nStdHandle, IntPtr hHandle); + + [DllImport("kernel32.dll")] + private static extern uint GetCurrentThreadId(); + + [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] + private static extern IntPtr CreateFile( + [MarshalAs(UnmanagedType.LPTStr)] string filename, + [MarshalAs(UnmanagedType.U4)] uint access, + [MarshalAs(UnmanagedType.U4)] FileShare share, + IntPtr securityAttributes, + [MarshalAs(UnmanagedType.U4)] FileMode creationDisposition, + [MarshalAs(UnmanagedType.U4)] FileAttributes flagsAndAttributes, + IntPtr templateFile); + + static APIHelper() + { + _log = LogManager.GetCurrentClassLogger(); + _tokenSource = new CancellationTokenSource(); + } + + public static void AllocateConsole() + { + _log.Debug("Bootstrapping the console allocation routine."); + AllocConsole(); + _log.Debug($"Console allocation result: {Marshal.GetLastWin32Error()}"); + + var outputFilePointer = CreateFile("CONOUT$", GenericRead | GenericWrite, FileShare.Write, IntPtr.Zero, FileMode.OpenOrCreate, 0, IntPtr.Zero); + _log.Debug($"CONOUT creation result: {Marshal.GetLastWin32Error()}"); + + SetStdHandle(StdOutputHandle, outputFilePointer); + _log.Debug($"SetStdHandle result: {Marshal.GetLastWin32Error()}"); + + Console.SetOut(new StreamWriter(Console.OpenStandardOutput(), Console.OutputEncoding) { AutoFlush = true }); + } + + /// + /// Sets the computer awake state using the native Win32 SetThreadExecutionState API. This + /// function is just a nice-to-have wrapper that helps avoid tracking the success or failure of + /// the call. + /// + /// Single or multiple EXECUTION_STATE entries. + /// true if successful, false if failed + private static bool SetAwakeState(EXECUTION_STATE state) + { + try + { + var stateResult = SetThreadExecutionState(state); + return stateResult != 0; + } + catch + { + return false; + } + } + + public static void SetIndefiniteKeepAwake(Action callback, Action failureCallback, bool keepDisplayOn = false) + { + _tokenSource.Cancel(); + + try + { + if (_runnerThread != null && !_runnerThread.IsCanceled) + { + _runnerThread.Wait(_threadToken); + } + } + catch (OperationCanceledException) + { + _log.Info("Confirmed background thread cancellation when setting indefinite keep awake."); + } + + _tokenSource = new CancellationTokenSource(); + _threadToken = _tokenSource.Token; + + _runnerThread = Task.Run(() => RunIndefiniteLoop(keepDisplayOn), _threadToken) + .ContinueWith((result) => callback(result.Result), TaskContinuationOptions.OnlyOnRanToCompletion) + .ContinueWith((result) => failureCallback, TaskContinuationOptions.NotOnRanToCompletion); + } + + public static void SetNoKeepAwake() + { + _tokenSource.Cancel(); + + try + { + if (_runnerThread != null && !_runnerThread.IsCanceled) + { + _runnerThread.Wait(_threadToken); + } + } + catch (OperationCanceledException) + { + _log.Info("Confirmed background thread cancellation when setting passive keep awake."); + } + } + + public static void SetTimedKeepAwake(uint seconds, Action callback, Action failureCallback, bool keepDisplayOn = true) + { + _tokenSource.Cancel(); + + try + { + if (_runnerThread != null && !_runnerThread.IsCanceled) + { + _runnerThread.Wait(_threadToken); + } + } + catch (OperationCanceledException) + { + _log.Info("Confirmed background thread cancellation when setting indefinite keep awake."); + } + + _tokenSource = new CancellationTokenSource(); + _threadToken = _tokenSource.Token; + + _runnerThread = Task.Run(() => RunTimedLoop(seconds, keepDisplayOn), _threadToken) + .ContinueWith((result) => callback(result.Result), TaskContinuationOptions.OnlyOnRanToCompletion) + .ContinueWith((result) => failureCallback, TaskContinuationOptions.NotOnRanToCompletion); + } + + private static bool RunIndefiniteLoop(bool keepDisplayOn = false) + { + bool success; + if (keepDisplayOn) + { + success = SetAwakeState(EXECUTION_STATE.ES_SYSTEM_REQUIRED | EXECUTION_STATE.ES_DISPLAY_REQUIRED | EXECUTION_STATE.ES_CONTINUOUS); + } + else + { + success = SetAwakeState(EXECUTION_STATE.ES_SYSTEM_REQUIRED | EXECUTION_STATE.ES_CONTINUOUS); + } + + try + { + if (success) + { + _log.Info($"Initiated indefinite keep awake in background thread: {GetCurrentThreadId()}. Screen on: {keepDisplayOn}"); + while (true) + { + if (_threadToken.IsCancellationRequested) + { + _threadToken.ThrowIfCancellationRequested(); + } + } + } + else + { + _log.Info("Could not successfully set up indefinite keep awake."); + return success; + } + } + catch (OperationCanceledException ex) + { + // Task was clearly cancelled. + _log.Info($"Background thread termination: {GetCurrentThreadId()}. Message: {ex.Message}"); + return success; + } + } + + private static bool RunTimedLoop(uint seconds, bool keepDisplayOn = true) + { + bool success = false; + + // In case cancellation was already requested. + _threadToken.ThrowIfCancellationRequested(); + try + { + if (keepDisplayOn) + { + success = SetAwakeState(EXECUTION_STATE.ES_SYSTEM_REQUIRED | EXECUTION_STATE.ES_DISPLAY_REQUIRED | EXECUTION_STATE.ES_CONTINUOUS); + } + else + { + success = SetAwakeState(EXECUTION_STATE.ES_SYSTEM_REQUIRED | EXECUTION_STATE.ES_CONTINUOUS); + } + + if (success) + { + _log.Info($"Initiated temporary keep awake in background thread: {GetCurrentThreadId()}. Screen on: {keepDisplayOn}"); + var startTime = DateTime.UtcNow; + while (DateTime.UtcNow - startTime < TimeSpan.FromSeconds(Math.Abs(seconds))) + { + if (_threadToken.IsCancellationRequested) + { + _threadToken.ThrowIfCancellationRequested(); + } + } + + return success; + } + else + { + _log.Info("Could not set up timed keep-awake with display on."); + return success; + } + } + catch (OperationCanceledException ex) + { + // Task was clearly cancelled. + _log.Info($"Background thread termination: {GetCurrentThreadId()}. Message: {ex.Message}"); + return success; + } + } + + public static string GetOperatingSystemBuild() + { + try + { +#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. + RegistryKey registryKey = Registry.LocalMachine.OpenSubKey(BuildRegistryLocation); +#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type. + + if (registryKey != null) + { + var versionString = $"{registryKey.GetValue("ProductName")} {registryKey.GetValue("DisplayVersion")} {registryKey.GetValue("BuildLabEx")}"; + return versionString; + } + else + { + _log.Info("Registry key acquisition for OS failed."); + return string.Empty; + } + } + catch (Exception ex) + { + _log.Info($"Could not get registry key for the build number. Error: {ex.Message}"); + return string.Empty; + } + } + } +} diff --git a/src/modules/espresso/Espresso/Core/TrayHelper.cs b/src/modules/espresso/Espresso/Core/TrayHelper.cs new file mode 100644 index 000000000000..f0ff727137dc --- /dev/null +++ b/src/modules/espresso/Espresso/Core/TrayHelper.cs @@ -0,0 +1,297 @@ +// 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.Drawing; +using System.IO; +using System.Text.Json; +using System.Windows.Forms; +using Microsoft.PowerToys.Settings.UI.Library; + +#pragma warning disable CS8602 // Dereference of a possibly null reference. +#pragma warning disable CS8603 // Possible null reference return. + +namespace Espresso.Shell.Core +{ + internal static class TrayHelper + { + private static NotifyIcon? trayIcon; + + private static NotifyIcon TrayIcon { get => trayIcon; set => trayIcon = value; } + + private static SettingsUtils? moduleSettings; + + private static SettingsUtils ModuleSettings { get => moduleSettings; set => moduleSettings = value; } + + static TrayHelper() + { + TrayIcon = new NotifyIcon(); + ModuleSettings = new SettingsUtils(); + } + + public static void InitializeTray(string text, Icon icon, ContextMenuStrip? contextMenu = null) + { + System.Threading.Tasks.Task.Factory.StartNew( + (tray) => + { + ((NotifyIcon?)tray).Text = text; + ((NotifyIcon?)tray).Icon = icon; + ((NotifyIcon?)tray).ContextMenuStrip = contextMenu; + ((NotifyIcon?)tray).Visible = true; + + Application.Run(); + }, TrayIcon); + } + + internal static void SetTray(string text, EspressoSettings settings) + { + SetTray( + text, + settings.Properties.KeepDisplayOn, + settings.Properties.Mode, + PassiveKeepAwakeCallback(text), + IndefiniteKeepAwakeCallback(text), + TimedKeepAwakeCallback(text), + KeepDisplayOnCallback(text), + ExitCallback()); + } + + private static Action ExitCallback() + { + return () => + { + Environment.Exit(0); + }; + } + + private static Action KeepDisplayOnCallback(string moduleName) + { + return () => + { + EspressoSettings currentSettings; + + try + { + currentSettings = ModuleSettings.GetSettings(moduleName); + } + catch (FileNotFoundException) + { + currentSettings = new EspressoSettings(); + } + + currentSettings.Properties.KeepDisplayOn = !currentSettings.Properties.KeepDisplayOn; + + ModuleSettings.SaveSettings(JsonSerializer.Serialize(currentSettings), moduleName); + }; + } + + private static Action TimedKeepAwakeCallback(string moduleName) + { + return (hours, minutes) => + { + EspressoSettings currentSettings; + + try + { + currentSettings = ModuleSettings.GetSettings(moduleName); + } + catch (FileNotFoundException) + { + currentSettings = new EspressoSettings(); + } + + currentSettings.Properties.Mode = EspressoMode.TIMED; + currentSettings.Properties.Hours = hours; + currentSettings.Properties.Minutes = minutes; + + ModuleSettings.SaveSettings(JsonSerializer.Serialize(currentSettings), moduleName); + }; + } + + private static Action PassiveKeepAwakeCallback(string moduleName) + { + return () => + { + EspressoSettings currentSettings; + + try + { + currentSettings = ModuleSettings.GetSettings(moduleName); + } + catch (FileNotFoundException) + { + currentSettings = new EspressoSettings(); + } + + currentSettings.Properties.Mode = EspressoMode.PASSIVE; + + ModuleSettings.SaveSettings(JsonSerializer.Serialize(currentSettings), moduleName); + }; + } + + private static Action IndefiniteKeepAwakeCallback(string moduleName) + { + return () => + { + EspressoSettings currentSettings; + + try + { + currentSettings = ModuleSettings.GetSettings(moduleName); + } + catch (FileNotFoundException) + { + currentSettings = new EspressoSettings(); + } + + currentSettings.Properties.Mode = EspressoMode.INDEFINITE; + + ModuleSettings.SaveSettings(JsonSerializer.Serialize(currentSettings), moduleName); + }; + } + + public static void SetTray(string text, bool keepDisplayOn, EspressoMode mode, Action passiveKeepAwakeCallback, Action indefiniteKeepAwakeCallback, Action timedKeepAwakeCallback, Action keepDisplayOnCallback, Action exitCallback) + { + var contextMenuStrip = new ContextMenuStrip(); + + // Main toolstrip. + var operationContextMenu = new ToolStripMenuItem + { + Text = "Mode", + }; + + // No keep-awake menu item. + var passiveMenuItem = new ToolStripMenuItem + { + Text = "Off (Passive)", + }; + + if (mode == EspressoMode.PASSIVE) + { + passiveMenuItem.Checked = true; + } + else + { + passiveMenuItem.Checked = false; + } + + passiveMenuItem.Click += (e, s) => + { + // User opted to set the mode to indefinite, so we need to write new settings. + passiveKeepAwakeCallback(); + }; + + // Indefinite keep-awake menu item. + var indefiniteMenuItem = new ToolStripMenuItem + { + Text = "Keep awake indefinitely", + }; + + if (mode == EspressoMode.INDEFINITE) + { + indefiniteMenuItem.Checked = true; + } + else + { + indefiniteMenuItem.Checked = false; + } + + indefiniteMenuItem.Click += (e, s) => + { + // User opted to set the mode to indefinite, so we need to write new settings. + indefiniteKeepAwakeCallback(); + }; + + var displayOnMenuItem = new ToolStripMenuItem + { + Text = "Keep screen on", + }; + if (keepDisplayOn) + { + displayOnMenuItem.Checked = true; + } + else + { + displayOnMenuItem.Checked = false; + } + + displayOnMenuItem.Click += (e, s) => + { + // User opted to set the display mode directly. + keepDisplayOnCallback(); + }; + + // Timed keep-awake menu item + var timedMenuItem = new ToolStripMenuItem + { + Text = "Keep awake temporarily", + }; + if (mode == EspressoMode.TIMED) + { + timedMenuItem.Checked = true; + } + else + { + timedMenuItem.Checked = false; + } + + var halfHourMenuItem = new ToolStripMenuItem + { + Text = "30 minutes", + }; + halfHourMenuItem.Click += (e, s) => + { + // User is setting the keep-awake to 30 minutes. + timedKeepAwakeCallback(0, 30); + }; + + var oneHourMenuItem = new ToolStripMenuItem + { + Text = "1 hour", + }; + oneHourMenuItem.Click += (e, s) => + { + // User is setting the keep-awake to 1 hour. + timedKeepAwakeCallback(1, 0); + }; + + var twoHoursMenuItem = new ToolStripMenuItem + { + Text = "2 hours", + }; + twoHoursMenuItem.Click += (e, s) => + { + // User is setting the keep-awake to 2 hours. + timedKeepAwakeCallback(2, 0); + }; + + // Exit menu item. + var exitContextMenu = new ToolStripMenuItem + { + Text = "Exit", + }; + exitContextMenu.Click += (e, s) => + { + // User is setting the keep-awake to 2 hours. + exitCallback(); + }; + + timedMenuItem.DropDownItems.Add(halfHourMenuItem); + timedMenuItem.DropDownItems.Add(oneHourMenuItem); + timedMenuItem.DropDownItems.Add(twoHoursMenuItem); + + operationContextMenu.DropDownItems.Add(passiveMenuItem); + operationContextMenu.DropDownItems.Add(indefiniteMenuItem); + operationContextMenu.DropDownItems.Add(timedMenuItem); + + contextMenuStrip.Items.Add(operationContextMenu); + contextMenuStrip.Items.Add(displayOnMenuItem); + contextMenuStrip.Items.Add(new ToolStripSeparator()); + contextMenuStrip.Items.Add(exitContextMenu); + + TrayIcon.Text = text; + TrayIcon.ContextMenuStrip = contextMenuStrip; + } + } +} diff --git a/src/modules/espresso/Espresso/Espresso.csproj b/src/modules/espresso/Espresso/Espresso.csproj new file mode 100644 index 000000000000..eeb421c4d536 --- /dev/null +++ b/src/modules/espresso/Espresso/Espresso.csproj @@ -0,0 +1,83 @@ + + + + WinExe + netcoreapp3.1 + $(SolutionDir)$(Platform)\$(Configuration)\modules\Espresso + enable + x64 + false + false + true + + true + PowerToys.Espresso + $(Version).0 + Images\Espresso.ico + + + + DEBUG;TRACE + full + x64 + prompt + MinimumRecommendedRules.ruleset + 4 + false + true + + + + TRACE;RELEASE + true + pdbonly + x64 + prompt + MinimumRecommendedRules.ruleset + 4 + true + + + + + + + + + + + + + + + + + + Never + + + + + + PreserveNewest + + + + + GlobalSuppressions.cs + + + StyleCop.json + + + + + 1.1.118 + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + diff --git a/src/modules/espresso/Espresso/Images/Espresso.ico b/src/modules/espresso/Espresso/Images/Espresso.ico new file mode 100644 index 000000000000..875ec0a63be4 Binary files /dev/null and b/src/modules/espresso/Espresso/Images/Espresso.ico differ diff --git a/src/modules/espresso/Espresso/NLog.config b/src/modules/espresso/Espresso/NLog.config new file mode 100644 index 000000000000..6448e4cf0cb0 --- /dev/null +++ b/src/modules/espresso/Espresso/NLog.config @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/modules/espresso/Espresso/Program.cs b/src/modules/espresso/Espresso/Program.cs new file mode 100644 index 000000000000..0e6e4bfeb8be --- /dev/null +++ b/src/modules/espresso/Espresso/Program.cs @@ -0,0 +1,320 @@ +// 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.CommandLine; +using System.CommandLine.Invocation; +using System.Diagnostics; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Reactive.Concurrency; +using System.Reactive.Linq; +using System.Reflection; +using System.Threading; +using System.Windows; +using Espresso.Shell.Core; +using ManagedCommon; +using Microsoft.PowerToys.Settings.UI.Library; +using NLog; + +#pragma warning disable CS8602 // Dereference of a possibly null reference. +#pragma warning disable CS8603 // Possible null reference return. + +namespace Espresso.Shell +{ + internal class Program + { + private static Mutex? _mutex = null; + private const string AppName = "Espresso"; + private static FileSystemWatcher? _watcher = null; + private static SettingsUtils? _settingsUtils = null; + + public static Mutex LockMutex { get => _mutex; set => _mutex = value; } + + private static Logger? _log; + + private static int Main(string[] args) + { + bool instantiated; + LockMutex = new Mutex(true, AppName, out instantiated); + + if (!instantiated) + { + ForceExit(AppName + " is already running! Exiting the application.", 1); + } + + _log = LogManager.GetCurrentClassLogger(); + _settingsUtils = new SettingsUtils(); + + _log.Info("Launching Espresso..."); + _log.Info(FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).FileVersion); + _log.Info($"OS: {Environment.OSVersion}"); + _log.Info($"OS Build: {APIHelper.GetOperatingSystemBuild()}"); + + _log.Info("Parsing parameters..."); + + var configOption = new Option( + aliases: new[] { "--use-pt-config", "-c" }, + getDefaultValue: () => false, + description: "Specifies whether Espresso will be using the PowerToys configuration file for managing the state.") + { + Argument = new Argument(() => false) + { + Arity = ArgumentArity.ZeroOrOne, + }, + }; + + configOption.Required = false; + + var displayOption = new Option( + aliases: new[] { "--display-on", "-d" }, + getDefaultValue: () => true, + description: "Determines whether the display should be kept awake.") + { + Argument = new Argument(() => false) + { + Arity = ArgumentArity.ZeroOrOne, + }, + }; + + displayOption.Required = false; + + var timeOption = new Option( + aliases: new[] { "--time-limit", "-t" }, + getDefaultValue: () => 0, + description: "Determines the interval, in seconds, during which the computer is kept awake.") + { + Argument = new Argument(() => 0) + { + Arity = ArgumentArity.ExactlyOne, + }, + }; + + timeOption.Required = false; + + var pidOption = new Option( + aliases: new[] { "--pid", "-p" }, + getDefaultValue: () => 0, + description: "Bind the execution of Espresso to another process.") + { + Argument = new Argument(() => 0) + { + Arity = ArgumentArity.ZeroOrOne, + }, + }; + + pidOption.Required = false; + + var rootCommand = new RootCommand + { + configOption, + displayOption, + timeOption, + pidOption, + }; + + rootCommand.Description = AppName; + + rootCommand.Handler = CommandHandler.Create(HandleCommandLineArguments); + + _log.Info("Parameter setup complete. Proceeding to the rest of the app initiation..."); + + return rootCommand.InvokeAsync(args).Result; + } + + private static void ForceExit(string message, int exitCode) + { + _log.Info(message); + Console.ReadKey(); + Environment.Exit(exitCode); + } + + private static void HandleCommandLineArguments(bool usePtConfig, bool displayOn, uint timeLimit, int pid) + { + if (pid == 0) + { + _log.Info("No PID specified. Allocating console..."); + APIHelper.AllocateConsole(); + } + + _log.Info($"The value for --use-pt-config is: {usePtConfig}"); + _log.Info($"The value for --display-on is: {displayOn}"); + _log.Info($"The value for --time-limit is: {timeLimit}"); + _log.Info($"The value for --pid is: {pid}"); + + if (usePtConfig) + { + // Configuration file is used, therefore we disregard any other command-line parameter + // and instead watch for changes in the file. + try + { +#pragma warning disable CS8604 // Possible null reference argument. + TrayHelper.InitializeTray(AppName, new Icon(Application.GetResourceStream(new Uri("/Images/Espresso.ico", UriKind.Relative)).Stream)); +#pragma warning restore CS8604 // Possible null reference argument. + + var settingsPath = _settingsUtils.GetSettingsFilePath(AppName); + _log.Info($"Reading configuration file: {settingsPath}"); + + _watcher = new FileSystemWatcher + { + Path = Path.GetDirectoryName(settingsPath), + EnableRaisingEvents = true, + NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.CreationTime, + Filter = Path.GetFileName(settingsPath), + }; + + var changedObservable = Observable.FromEventPattern( + h => _watcher.Changed += h, + h => _watcher.Changed -= h); + + var createdObservable = Observable.FromEventPattern( + cre => _watcher.Created += cre, + cre => _watcher.Created -= cre); + + var mergedObservable = Observable.Merge(changedObservable, createdObservable); + + mergedObservable.Throttle(TimeSpan.FromMilliseconds(25)) + .SubscribeOn(TaskPoolScheduler.Default) + .Select(e => e.EventArgs) + .Subscribe(HandleEspressoConfigChange); + + TrayHelper.SetTray(AppName, new EspressoSettings()); + + // Initially the file might not be updated, so we need to start processing + // settings right away. + ProcessSettings(); + } + catch (Exception ex) + { + var errorString = $"There was a problem with the configuration file. Make sure it exists.\n{ex.Message}"; + _log.Info(errorString); + _log.Debug(errorString); + } + } + else + { + var mode = timeLimit <= 0 ? EspressoMode.INDEFINITE : EspressoMode.TIMED; + + if (mode == EspressoMode.INDEFINITE) + { + SetupIndefiniteKeepAwake(displayOn); + } + else + { + SetupTimedKeepAwake(timeLimit, displayOn); + } + } + + var exitSignal = new ManualResetEvent(false); + if (pid != 0) + { + RunnerHelper.WaitForPowerToysRunner(pid, () => + { + exitSignal.Set(); + Environment.Exit(0); + }); + } + + exitSignal.WaitOne(); + } + + private static void SetupIndefiniteKeepAwake(bool displayOn) + { + // Indefinite keep awake. + APIHelper.SetIndefiniteKeepAwake(LogCompletedKeepAwakeThread, LogUnexpectedOrCancelledKeepAwakeThreadCompletion, displayOn); + } + + private static void HandleEspressoConfigChange(FileSystemEventArgs fileEvent) + { + _log.Info("Detected a settings file change. Updating configuration..."); + _log.Info("Resetting keep-awake to normal state due to settings change."); + ProcessSettings(); + } + + private static void ProcessSettings() + { + try + { + EspressoSettings settings = _settingsUtils.GetSettings(AppName); + + if (settings != null) + { + switch (settings.Properties.Mode) + { + case EspressoMode.PASSIVE: + { + SetupNoKeepAwake(); + break; + } + + case EspressoMode.INDEFINITE: + { + // Indefinite keep awake. + SetupIndefiniteKeepAwake(settings.Properties.KeepDisplayOn); + break; + } + + case EspressoMode.TIMED: + { + // Timed keep-awake. + uint computedTime = (settings.Properties.Hours * 60 * 60) + (settings.Properties.Minutes * 60); + SetupTimedKeepAwake(computedTime, settings.Properties.KeepDisplayOn); + + break; + } + + default: + { + var errorMessage = "Unknown mode of operation. Check config file."; + _log.Info(errorMessage); + _log.Debug(errorMessage); + break; + } + } + + TrayHelper.SetTray(AppName, settings); + } + else + { + var errorMessage = "Settings are null."; + _log.Info(errorMessage); + _log.Debug(errorMessage); + } + } + catch (Exception ex) + { + var errorMessage = $"There was a problem reading the configuration file. Error: {ex.GetType()} {ex.Message}"; + _log.Info(errorMessage); + _log.Debug(errorMessage); + } + } + + private static void SetupNoKeepAwake() + { + _log.Info($"Operating in passive mode (computer's standard power plan). No custom keep awake settings enabled."); + + APIHelper.SetNoKeepAwake(); + } + + private static void SetupTimedKeepAwake(uint time, bool displayOn) + { + _log.Info($"Timed keep-awake. Expected runtime: {time} seconds with display on setting set to {displayOn}."); + + APIHelper.SetTimedKeepAwake(time, LogCompletedKeepAwakeThread, LogUnexpectedOrCancelledKeepAwakeThreadCompletion, displayOn); + } + + private static void LogUnexpectedOrCancelledKeepAwakeThreadCompletion() + { + var errorMessage = "The keep-awake thread was terminated early."; + _log.Info(errorMessage); + _log.Debug(errorMessage); + } + + private static void LogCompletedKeepAwakeThread(bool result) + { + _log.Info($"Exited keep-awake thread successfully: {result}"); + } + } +} diff --git a/src/modules/espresso/EspressoModuleInterface/EspressoConstants.h b/src/modules/espresso/EspressoModuleInterface/EspressoConstants.h new file mode 100644 index 000000000000..6b97fe7021a7 --- /dev/null +++ b/src/modules/espresso/EspressoModuleInterface/EspressoConstants.h @@ -0,0 +1,7 @@ +#include + +namespace EspressoConstants +{ + // Name of the powertoy module. + inline const std::wstring ModuleKey = L"Espresso"; +} \ No newline at end of file diff --git a/src/modules/espresso/EspressoModuleInterface/EspressoModuleInterface.rc b/src/modules/espresso/EspressoModuleInterface/EspressoModuleInterface.rc new file mode 100644 index 000000000000..91d08ea09eb3 --- /dev/null +++ b/src/modules/espresso/EspressoModuleInterface/EspressoModuleInterface.rc @@ -0,0 +1,72 @@ +// Microsoft Visual C++ generated resource script. +// +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US +#pragma code_page(1252) + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// String Table +// + +STRINGTABLE +BEGIN + IDS_ESPRESSO_NAME "Espresso" +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED + diff --git a/src/modules/espresso/EspressoModuleInterface/EspressoModuleInterface.vcxproj b/src/modules/espresso/EspressoModuleInterface/EspressoModuleInterface.vcxproj new file mode 100644 index 000000000000..5f880d835a4e --- /dev/null +++ b/src/modules/espresso/EspressoModuleInterface/EspressoModuleInterface.vcxproj @@ -0,0 +1,92 @@ + + + + + + Debug + x64 + + + Release + x64 + + + + 15.0 + {5e7360a8-d048-4ed3-8f09-0bfd64c5529a} + Win32Proj + Espresso + EspressoModuleInterface + 10.0.17134.0 + + + + DynamicLibrary + + + + + + + + + + + + + + + $(SolutionDir)$(Platform)\$(Configuration)\modules\Espresso\ + + + + EXAMPLEPOWERTOY_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + ..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories) + + + $(OutDir)$(TargetName)$(TargetExt) + + + + + + + + + + + + Create + Create + pch.h + pch.h + + + + + + {d9b8fc84-322a-4f9f-bbb9-20915c47ddfd} + + + {6955446d-23f7-4023-9bb3-8657f904af99} + + + + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + \ No newline at end of file diff --git a/src/modules/espresso/EspressoModuleInterface/EspressoModuleInterface.vcxproj.filters b/src/modules/espresso/EspressoModuleInterface/EspressoModuleInterface.vcxproj.filters new file mode 100644 index 000000000000..2dd5624a5345 --- /dev/null +++ b/src/modules/espresso/EspressoModuleInterface/EspressoModuleInterface.vcxproj.filters @@ -0,0 +1,50 @@ + + + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + Header Files + + + Generated Files + + + + + {e8ef1c4e-cc50-4ce5-b00d-4e3ac5c1a7db} + + + {fbd9cdd2-e7d5-4417-9b52-25e345ae9562} + + + {c2a23a2b-5846-440f-b29e-eea748dba12d} + + + {77f1702b-da7f-4ff6-90a3-19db515cf963} + + + + + + + + Resource Files + + + \ No newline at end of file diff --git a/src/modules/espresso/EspressoModuleInterface/dllmain.cpp b/src/modules/espresso/EspressoModuleInterface/dllmain.cpp new file mode 100644 index 000000000000..e86c7c5b86ef --- /dev/null +++ b/src/modules/espresso/EspressoModuleInterface/dllmain.cpp @@ -0,0 +1,181 @@ +#include "pch.h" +#include +#include +#include +#include "trace.h" +#include "resource.h" +#include "EspressoConstants.h" +#include +#include + +#include +#include +#include +#include +#include + +#include + +BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) +{ + switch (ul_reason_for_call) + { + case DLL_PROCESS_ATTACH: + Trace::RegisterProvider(); + break; + case DLL_THREAD_ATTACH: + case DLL_THREAD_DETACH: + break; + case DLL_PROCESS_DETACH: + Trace::UnregisterProvider(); + break; + } + return TRUE; +} + +// The PowerToy name that will be shown in the settings. +const static wchar_t* MODULE_NAME = L"Espresso"; + +// Add a description that will we shown in the module settings page. +const static wchar_t* MODULE_DESC = L""; + +// Implement the PowerToy Module Interface and all the required methods. +class Espresso : public PowertoyModuleIface +{ + std::wstring app_name; + + // Contains the non localized key of the powertoy + std::wstring app_key; + +private: + // The PowerToy state. + bool m_enabled = false; + + HANDLE m_hProcess; + + HANDLE send_telemetry_event; + + // Handle to event used to invoke Espresso + HANDLE m_hInvokeEvent; + + bool is_process_running() + { + return WaitForSingleObject(m_hProcess, 0) == WAIT_TIMEOUT; + } + + void launch_process() + { + Logger::trace(L"Launching Espresso process"); + unsigned long powertoys_pid = GetCurrentProcessId(); + + std::wstring executable_args = L"--use-pt-config --pid " + std::to_wstring(powertoys_pid); + Logger::trace(L"Espresso launching with parameters: " + executable_args); + + SHELLEXECUTEINFOW sei{ sizeof(sei) }; + sei.fMask = { SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI }; + sei.lpFile = L"modules\\Espresso\\PowerToys.Espresso.exe"; + sei.nShow = SW_SHOWNORMAL; + sei.lpParameters = executable_args.data(); + if (!ShellExecuteExW(&sei)) + { + DWORD error = GetLastError(); + std::wstring message = L"Espresso failed to start with error = "; + message += std::to_wstring(error); + Logger::error(message); + } + + m_hProcess = sei.hProcess; + } + +public: + // Constructor + Espresso() + { + app_name = GET_RESOURCE_STRING(IDS_ESPRESSO_NAME); + app_key = EspressoConstants::ModuleKey; + std::filesystem::path logFilePath(PTSettingsHelper::get_module_save_folder_location(this->app_key)); + logFilePath.append(LogSettings::espressoLogPath); + Logger::init(LogSettings::launcherLoggerName, logFilePath.wstring(), PTSettingsHelper::get_log_settings_file_location()); + Logger::info("Launcher object is constructing"); + }; + + // Destroy the powertoy and free memory + virtual void destroy() override + { + delete this; + } + + // Return the display name of the powertoy, this will be cached by the runner + virtual const wchar_t* get_name() override + { + return MODULE_NAME; + } + + // Return JSON with the configuration options. + virtual bool get_config(wchar_t* buffer, int* buffer_size) override + { + HINSTANCE hinstance = reinterpret_cast(&__ImageBase); + + // Create a Settings object. + PowerToysSettings::Settings settings(hinstance, get_name()); + settings.set_description(MODULE_DESC); + + return settings.serialize_to_buffer(buffer, buffer_size); + } + + // Return the non localized key of the powertoy, this will be cached by the runner + virtual const wchar_t* get_key() override + { + return app_key.c_str(); + } + + // Called by the runner to pass the updated settings values as a serialized JSON. + virtual void set_config(const wchar_t* config) override + { + try + { + // Parse the input JSON string. + PowerToysSettings::PowerToyValues values = + PowerToysSettings::PowerToyValues::from_json_string(config, get_key()); + + // If you don't need to do any custom processing of the settings, proceed + // to persists the values calling: + values.save_to_settings_file(); + } + catch (std::exception&) + { + // Improper JSON. + } + } + + virtual void enable() + { + ResetEvent(send_telemetry_event); + ResetEvent(m_hInvokeEvent); + launch_process(); + m_enabled = true; + }; + + virtual void disable() + { + if (m_enabled) + { + ResetEvent(send_telemetry_event); + ResetEvent(m_hInvokeEvent); + TerminateProcess(m_hProcess, 1); + } + + m_enabled = false; + } + + // Returns if the powertoys is enabled + virtual bool is_enabled() override + { + return m_enabled; + } +}; + +extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create() +{ + return new Espresso(); +} \ No newline at end of file diff --git a/src/modules/espresso/EspressoModuleInterface/packages.config b/src/modules/espresso/EspressoModuleInterface/packages.config new file mode 100644 index 000000000000..6a0a5a1e09d4 --- /dev/null +++ b/src/modules/espresso/EspressoModuleInterface/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/modules/espresso/EspressoModuleInterface/pch.cpp b/src/modules/espresso/EspressoModuleInterface/pch.cpp new file mode 100644 index 000000000000..17305716aacd --- /dev/null +++ b/src/modules/espresso/EspressoModuleInterface/pch.cpp @@ -0,0 +1 @@ +#include "pch.h" \ No newline at end of file diff --git a/src/modules/espresso/EspressoModuleInterface/pch.h b/src/modules/espresso/EspressoModuleInterface/pch.h new file mode 100644 index 000000000000..eddac0fdc1f3 --- /dev/null +++ b/src/modules/espresso/EspressoModuleInterface/pch.h @@ -0,0 +1,7 @@ +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include +#include +#include \ No newline at end of file diff --git a/src/modules/espresso/EspressoModuleInterface/resource.h b/src/modules/espresso/EspressoModuleInterface/resource.h new file mode 100644 index 000000000000..53fc0ecf48a9 --- /dev/null +++ b/src/modules/espresso/EspressoModuleInterface/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Espresso.rc +// +#define IDS_ESPRESSO_NAME 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/src/modules/espresso/EspressoModuleInterface/trace.cpp b/src/modules/espresso/EspressoModuleInterface/trace.cpp new file mode 100644 index 000000000000..e2508396ca63 --- /dev/null +++ b/src/modules/espresso/EspressoModuleInterface/trace.cpp @@ -0,0 +1,19 @@ +#include "pch.h" +#include "trace.h" + +TRACELOGGING_DEFINE_PROVIDER( + g_hProvider, + "Microsoft.PowerToys", + // {38e8889b-9731-53f5-e901-e8a7c1753074} + (0x38e8889b, 0x9731, 0x53f5, 0xe9, 0x01, 0xe8, 0xa7, 0xc1, 0x75, 0x30, 0x74), + TraceLoggingOptionProjectTelemetry()); + +void Trace::RegisterProvider() +{ + TraceLoggingRegister(g_hProvider); +} + +void Trace::UnregisterProvider() +{ + TraceLoggingUnregister(g_hProvider); +} diff --git a/src/modules/espresso/EspressoModuleInterface/trace.h b/src/modules/espresso/EspressoModuleInterface/trace.h new file mode 100644 index 000000000000..e5c7680cf033 --- /dev/null +++ b/src/modules/espresso/EspressoModuleInterface/trace.h @@ -0,0 +1,8 @@ +#pragma once + +class Trace +{ +public: + static void RegisterProvider(); + static void UnregisterProvider(); +}; diff --git a/src/runner/main.cpp b/src/runner/main.cpp index bcf37d72c59a..8aaa51dc7b05 100644 --- a/src/runner/main.cpp +++ b/src/runner/main.cpp @@ -149,7 +149,7 @@ int runner(bool isProcessElevated, bool openSettings, bool openOobe) chdir_current_executable(); // Load Powertoys DLLs - const std::array knownModules = { + const std::array knownModules = { L"modules/FancyZones/fancyzones.dll", L"modules/FileExplorerPreview/powerpreview.dll", L"modules/ImageResizer/ImageResizerExt.dll", @@ -158,6 +158,7 @@ int runner(bool isProcessElevated, bool openSettings, bool openOobe) L"modules/PowerRename/PowerRenameExt.dll", L"modules/ShortcutGuide/ShortcutGuideModuleInterface/ShortcutGuideModuleInterface.dll", L"modules/ColorPicker/ColorPicker.dll", + L"modules/Espresso/EspressoModuleInterface.dll", }; for (const auto& moduleSubdir : knownModules) diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/EnabledModules.cs b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/EnabledModules.cs index 1fadcb961bdc..d1689bee8715 100644 --- a/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/EnabledModules.cs +++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/EnabledModules.cs @@ -143,6 +143,22 @@ public bool ColorPicker } } + private bool espresso; + + [JsonPropertyName("Espresso")] + public bool Espresso + { + get => espresso; + set + { + if (espresso != value) + { + LogTelemetryEvent(value); + espresso = value; + } + } + } + public string ToJsonString() { return JsonSerializer.Serialize(this); diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/EspressoProperties.cs b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/EspressoProperties.cs new file mode 100644 index 000000000000..8b81deb574f5 --- /dev/null +++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/EspressoProperties.cs @@ -0,0 +1,38 @@ +// 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.Text.Json.Serialization; + +namespace Microsoft.PowerToys.Settings.UI.Library +{ + public class EspressoProperties + { + public EspressoProperties() + { + KeepDisplayOn = false; + Mode = EspressoMode.PASSIVE; + Hours = 0; + Minutes = 0; + } + + [JsonPropertyName("espresso_keep_display_on")] + public bool KeepDisplayOn { get; set; } + + [JsonPropertyName("espresso_mode")] + public EspressoMode Mode { get; set; } + + [JsonPropertyName("espresso_hours")] + public uint Hours { get; set; } + + [JsonPropertyName("espresso_minutes")] + public uint Minutes { get; set; } + } + + public enum EspressoMode + { + PASSIVE = 0, + INDEFINITE = 1, + TIMED = 2, + } +} diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/EspressoSettings.cs b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/EspressoSettings.cs new file mode 100644 index 000000000000..a79a433ec577 --- /dev/null +++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/EspressoSettings.cs @@ -0,0 +1,35 @@ +// 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.Text.Json.Serialization; +using Microsoft.PowerToys.Settings.UI.Library.Interfaces; + +namespace Microsoft.PowerToys.Settings.UI.Library +{ + public class EspressoSettings : BasePTModuleSettings, ISettingsConfig + { + public const string ModuleName = "Espresso"; + public const string ModuleVersion = "0.0.1"; + + public EspressoSettings() + { + Name = ModuleName; + Version = ModuleVersion; + Properties = new EspressoProperties(); + } + + [JsonPropertyName("properties")] + public EspressoProperties Properties { get; set; } + + public string GetModuleName() + { + return Name; + } + + public bool UpgradeSettingsConfiguration() + { + return false; + } + } +} diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/ISettingsUtils.cs b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/ISettingsUtils.cs index ea7b615fcf9c..7cfa33be626c 100644 --- a/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/ISettingsUtils.cs +++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/ISettingsUtils.cs @@ -19,5 +19,7 @@ T GetSettingsOrDefault(string powertoy = "", string fileName = "settings.json bool SettingsExists(string powertoy = "", string fileName = "settings.json"); void DeleteSettings(string powertoy = ""); + + string GetSettingsFilePath(string powertoy = "", string fileName = "settings.json"); } } diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/Microsoft.PowerToys.Settings.UI.Library.csproj b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/Microsoft.PowerToys.Settings.UI.Library.csproj index 683e7f0d7eaf..e039aa704e30 100644 --- a/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/Microsoft.PowerToys.Settings.UI.Library.csproj +++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/Microsoft.PowerToys.Settings.UI.Library.csproj @@ -20,6 +20,9 @@ x64 + DEBUG;TRACE + full + true diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/SettingsUtils.cs b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/SettingsUtils.cs index eb40f04cf6e3..926674ab03fb 100644 --- a/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/SettingsUtils.cs +++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/SettingsUtils.cs @@ -135,5 +135,11 @@ public void SaveSettings(string jsonSettings, string powertoy = DefaultModuleNam #endif } } + + // Returns the file path to the settings file, that is exposed from the local ISettingsPath instance. + public string GetSettingsFilePath(string powertoy = "", string fileName = "settings.json") + { + return _settingsPath.GetSettingsPath(powertoy, fileName); + } } } diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/SndEspressoSettings.cs b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/SndEspressoSettings.cs new file mode 100644 index 000000000000..e9ccea662ce3 --- /dev/null +++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/SndEspressoSettings.cs @@ -0,0 +1,32 @@ +// 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.Text; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Microsoft.PowerToys.Settings.UI.Library +{ + public class SndEspressoSettings + { + [JsonPropertyName("Espresso")] + public EspressoSettings Settings { get; set; } + + public SndEspressoSettings() + { + } + + public SndEspressoSettings(EspressoSettings settings) + { + Settings = settings; + } + + public string ToJsonString() + { + return JsonSerializer.Serialize(this); + } + } +} diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/ViewModels/EspressoViewModel.cs b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/ViewModels/EspressoViewModel.cs new file mode 100644 index 000000000000..94dd1db8a85c --- /dev/null +++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/ViewModels/EspressoViewModel.cs @@ -0,0 +1,157 @@ +// 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.CompilerServices; +using Microsoft.PowerToys.Settings.UI.Library.Helpers; +using Microsoft.PowerToys.Settings.UI.Library.Interfaces; + +namespace Microsoft.PowerToys.Settings.UI.Library.ViewModels +{ + public class EspressoViewModel : Observable + { + private GeneralSettings GeneralSettingsConfig { get; set; } + + private EspressoSettings Settings { get; set; } + + private Func SendConfigMSG { get; } + + public EspressoViewModel(ISettingsRepository settingsRepository, ISettingsRepository moduleSettingsRepository, Func ipcMSGCallBackFunc) + { + // To obtain the general settings configurations of PowerToys Settings. + if (settingsRepository == null) + { + throw new ArgumentNullException(nameof(settingsRepository)); + } + + GeneralSettingsConfig = settingsRepository.SettingsConfig; + + // To obtain the settings configurations of Fancy zones. + if (moduleSettingsRepository == null) + { + throw new ArgumentNullException(nameof(moduleSettingsRepository)); + } + + Settings = moduleSettingsRepository.SettingsConfig; + + _isEnabled = GeneralSettingsConfig.Enabled.Espresso; + _keepDisplayOn = Settings.Properties.KeepDisplayOn; + _mode = Settings.Properties.Mode; + _hours = Settings.Properties.Hours; + _minutes = Settings.Properties.Minutes; + + // set the callback functions value to hangle outgoing IPC message. + SendConfigMSG = ipcMSGCallBackFunc; + } + + public bool IsEnabled + { + get => _isEnabled; + set + { + if (_isEnabled != value) + { + _isEnabled = value; + + GeneralSettingsConfig.Enabled.Espresso = value; + OnPropertyChanged(nameof(IsEnabled)); + OnPropertyChanged(nameof(IsTimeConfigurationEnabled)); + + var outgoing = new OutGoingGeneralSettings(GeneralSettingsConfig); + SendConfigMSG(outgoing.ToString()); + NotifyPropertyChanged(); + } + } + } + + public bool IsTimeConfigurationEnabled + { + get => _mode == EspressoMode.TIMED && _isEnabled; + } + + public EspressoMode Mode + { + get => _mode; + set + { + if (_mode != value) + { + _mode = value; + OnPropertyChanged(nameof(Mode)); + OnPropertyChanged(nameof(IsTimeConfigurationEnabled)); + + Settings.Properties.Mode = value; + NotifyPropertyChanged(); + } + } + } + + public bool KeepDisplayOn + { + get => _keepDisplayOn; + set + { + if (_keepDisplayOn != value) + { + _keepDisplayOn = value; + OnPropertyChanged(nameof(KeepDisplayOn)); + + Settings.Properties.KeepDisplayOn = value; + NotifyPropertyChanged(); + } + } + } + + public uint Hours + { + get => _hours; + set + { + if (_hours != value) + { + _hours = value; + OnPropertyChanged(nameof(Hours)); + + Settings.Properties.Hours = value; + NotifyPropertyChanged(); + } + } + } + + public uint Minutes + { + get => _minutes; + set + { + if (_minutes != value) + { + _minutes = value; + OnPropertyChanged(nameof(Minutes)); + + Settings.Properties.Minutes = value; + NotifyPropertyChanged(); + } + } + } + + public void NotifyPropertyChanged([CallerMemberName] string propertyName = null) + { + OnPropertyChanged(propertyName); + if (SendConfigMSG != null) + { + SndEspressoSettings outsettings = new SndEspressoSettings(Settings); + SndModuleSettings ipcMessage = new SndModuleSettings(outsettings); + + var targetMessage = ipcMessage.ToJsonString(); + SendConfigMSG(targetMessage); + } + } + + private bool _isEnabled; + private uint _hours; + private uint _minutes; + private bool _keepDisplayOn; + private EspressoMode _mode; + } +} diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI.UnitTests/Microsoft.PowerToys.Settings.UI.UnitTests.csproj b/src/settings-ui/Microsoft.PowerToys.Settings.UI.UnitTests/Microsoft.PowerToys.Settings.UI.UnitTests.csproj index 51e345c4d739..6e895111b540 100644 --- a/src/settings-ui/Microsoft.PowerToys.Settings.UI.UnitTests/Microsoft.PowerToys.Settings.UI.UnitTests.csproj +++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI.UnitTests/Microsoft.PowerToys.Settings.UI.UnitTests.csproj @@ -1,4 +1,4 @@ - + netcoreapp3.1 @@ -9,6 +9,9 @@ ..\..\..\x64\Debug\SettingsTests\ + full + true + DEBUG;TRACE diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI/Assets/FluentIcons/FluentIconsEspresso.png b/src/settings-ui/Microsoft.PowerToys.Settings.UI/Assets/FluentIcons/FluentIconsEspresso.png new file mode 100644 index 000000000000..22790a3de26e Binary files /dev/null and b/src/settings-ui/Microsoft.PowerToys.Settings.UI/Assets/FluentIcons/FluentIconsEspresso.png differ diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI/Assets/Modules/Espresso.png b/src/settings-ui/Microsoft.PowerToys.Settings.UI/Assets/Modules/Espresso.png new file mode 100644 index 000000000000..d573613513fb Binary files /dev/null and b/src/settings-ui/Microsoft.PowerToys.Settings.UI/Assets/Modules/Espresso.png differ diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI/Assets/Modules/OOBE/Espresso.png b/src/settings-ui/Microsoft.PowerToys.Settings.UI/Assets/Modules/OOBE/Espresso.png new file mode 100644 index 000000000000..00542f14ab55 Binary files /dev/null and b/src/settings-ui/Microsoft.PowerToys.Settings.UI/Assets/Modules/OOBE/Espresso.png differ diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI/Converters/EspressoModeToBoolConverter.cs b/src/settings-ui/Microsoft.PowerToys.Settings.UI/Converters/EspressoModeToBoolConverter.cs new file mode 100644 index 000000000000..09f237c26ff6 --- /dev/null +++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI/Converters/EspressoModeToBoolConverter.cs @@ -0,0 +1,41 @@ +// 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 Microsoft.PowerToys.Settings.UI.Library; +using Windows.UI.Xaml.Data; + +namespace Microsoft.PowerToys.Settings.UI.Converters +{ + public sealed class EspressoModeToBoolConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, string language) + { + if (targetType != typeof(bool?)) + { + throw new InvalidOperationException("The target type needs to be a boolean."); + } + + if (parameter == null) + { + throw new NullReferenceException("Parameter cannot be null for the Espresso mode to bool converter."); + } + + var expectedMode = (EspressoMode)Enum.Parse(typeof(EspressoMode), parameter.ToString()); + var currentMode = (EspressoMode)value; + + return currentMode.Equals(expectedMode); + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + if (parameter == null) + { + throw new NullReferenceException("Parameter cannot be null for the Espresso mode to bool converter."); + } + + return (EspressoMode)Enum.Parse(typeof(EspressoMode), parameter.ToString()); + } + } +} diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI/Microsoft.PowerToys.Settings.UI.csproj b/src/settings-ui/Microsoft.PowerToys.Settings.UI/Microsoft.PowerToys.Settings.UI.csproj index f2d65009adf1..ed5163ff5fd3 100644 --- a/src/settings-ui/Microsoft.PowerToys.Settings.UI/Microsoft.PowerToys.Settings.UI.csproj +++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI/Microsoft.PowerToys.Settings.UI.csproj @@ -105,6 +105,7 @@ ShortcutVisualControl.xaml + @@ -125,6 +126,9 @@ + + OobeEspresso.xaml + OobeColorPicker.xaml @@ -162,6 +166,9 @@ + + EspressoPage.xaml + ColorPickerPage.xaml @@ -201,6 +208,7 @@ + @@ -212,10 +220,12 @@ + + @@ -298,6 +308,10 @@ Designer MSBuild:Compile + + MSBuild:Compile + Designer + Designer MSBuild:Compile @@ -370,6 +384,10 @@ MSBuild:Compile Designer + + MSBuild:Compile + Designer + Designer MSBuild:Compile @@ -448,4 +466,4 @@ --> - \ No newline at end of file + diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI/OOBE/Enums/PowerToysModulesEnum.cs b/src/settings-ui/Microsoft.PowerToys.Settings.UI/OOBE/Enums/PowerToysModulesEnum.cs index 83ca42c99f6e..3a5c7aa1b580 100644 --- a/src/settings-ui/Microsoft.PowerToys.Settings.UI/OOBE/Enums/PowerToysModulesEnum.cs +++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI/OOBE/Enums/PowerToysModulesEnum.cs @@ -8,6 +8,7 @@ public enum PowerToysModulesEnum { Overview = 0, ColorPicker, + Espresso, FancyZones, FileExplorer, ImageResizer, diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI/OOBE/Views/OobeEspresso.xaml b/src/settings-ui/Microsoft.PowerToys.Settings.UI/OOBE/Views/OobeEspresso.xaml new file mode 100644 index 000000000000..c6581b629b27 --- /dev/null +++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI/OOBE/Views/OobeEspresso.xaml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI/OOBE/Views/OobeEspresso.xaml.cs b/src/settings-ui/Microsoft.PowerToys.Settings.UI/OOBE/Views/OobeEspresso.xaml.cs new file mode 100644 index 000000000000..6bfe06037876 --- /dev/null +++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI/OOBE/Views/OobeEspresso.xaml.cs @@ -0,0 +1,48 @@ +// 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.Threading; +using Microsoft.PowerToys.Settings.UI.OOBE.Enums; +using Microsoft.PowerToys.Settings.UI.OOBE.ViewModel; +using Microsoft.PowerToys.Settings.UI.Views; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Navigation; + +namespace Microsoft.PowerToys.Settings.UI.OOBE.Views +{ + /// + /// An empty page that can be used on its own or navigated to within a Frame. + /// + public sealed partial class OobeEspresso : Page + { + public OobePowerToysModule ViewModel { get; set; } + + public OobeEspresso() + { + this.InitializeComponent(); + ViewModel = new OobePowerToysModule(OobeShellPage.OobeShellHandler.Modules[(int)PowerToysModulesEnum.Espresso]); + DataContext = ViewModel; + } + + private void SettingsLaunchButton_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e) + { + if (OobeShellPage.OpenMainWindowCallback != null) + { + OobeShellPage.OpenMainWindowCallback(typeof(EspressoPage)); + } + + ViewModel.LogOpeningSettingsEvent(); + } + + protected override void OnNavigatedTo(NavigationEventArgs e) + { + ViewModel.LogOpeningModuleEvent(); + } + + protected override void OnNavigatedFrom(NavigationEventArgs e) + { + ViewModel.LogClosingModuleEvent(); + } + } +} diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI/OOBE/Views/OobeShellPage.xaml.cs b/src/settings-ui/Microsoft.PowerToys.Settings.UI/OOBE/Views/OobeShellPage.xaml.cs index 4887e19d0643..170005ac2c76 100644 --- a/src/settings-ui/Microsoft.PowerToys.Settings.UI/OOBE/Views/OobeShellPage.xaml.cs +++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI/OOBE/Views/OobeShellPage.xaml.cs @@ -81,6 +81,18 @@ public OobeShellPage() Description = loader.GetString("Oobe_ColorPicker_Description"), Link = "https://aka.ms/PowerToysOverview_ColorPicker", }); + Modules.Insert((int)PowerToysModulesEnum.Espresso, new OobePowerToysModule() + { + ModuleName = loader.GetString("Oobe_Espresso"), + Tag = "Espresso", + IsNew = false, + Icon = "\uEC32", + Image = "ms-appx:///Assets/Modules/Espresso.png", + FluentIcon = "ms-appx:///Assets/FluentIcons/FluentIconsEspresso.png", + PreviewImageSource = "ms-appx:///Assets/Modules/OOBE/Espresso.png", + Description = loader.GetString("Oobe_Espresso_Description"), + Link = "https://aka.ms/PowerToysOverview_Espresso", + }); Modules.Insert((int)PowerToysModulesEnum.FancyZones, new OobePowerToysModule() { ModuleName = loader.GetString("Oobe_FancyZones"), @@ -204,6 +216,7 @@ private void NavigationView_SelectionChanged(Microsoft.UI.Xaml.Controls.Navigati { case "Overview": NavigationFrame.Navigate(typeof(OobeOverview)); break; case "ColorPicker": NavigationFrame.Navigate(typeof(OobeColorPicker)); break; + case "Espresso": NavigationFrame.Navigate(typeof(OobeEspresso)); break; case "FancyZones": NavigationFrame.Navigate(typeof(OobeFancyZones)); break; case "Run": NavigationFrame.Navigate(typeof(OobeRun)); break; case "ImageResizer": NavigationFrame.Navigate(typeof(OobeImageResizer)); break; diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI/Strings/en-us/Resources.resw b/src/settings-ui/Microsoft.PowerToys.Settings.UI/Strings/en-us/Resources.resw index e70452007d49..a7b48f8a3792 100644 --- a/src/settings-ui/Microsoft.PowerToys.Settings.UI/Strings/en-us/Resources.resw +++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI/Strings/en-us/Resources.resw @@ -121,6 +121,10 @@ General Navigation view item name for General + + Espresso + Product name: Navigation view item name for Espresso + PowerToys Run Product name: Navigation view item name for PowerToys Run @@ -480,6 +484,10 @@ Shows a help overlay with Windows shortcuts when the Windows key is pressed. + + Press duration before showing (ms) + pressing a key in milliseconds + Appearance & behavior @@ -904,6 +912,10 @@ https://aka.ms/PowerToysOverview_ColorPicker URL. Do not loc + + https://aka.ms/PowerToysOverview_Espresso + URL. Do not loc + https://aka.ms/PowerToysOverview_FancyZones URL. Do not loc @@ -988,6 +1000,43 @@ You can include or remove each plugin from the global results, change the direct activation phrase and configure additional options. + + Position & appearance + + + Show PowerToys Run on + as in Show PowerToys Run on primary monitor + + + Monitor with mouse cursor + + + Monitor with focused window + + + Primary monitor + + + Plugins are loading... + + + Move the color down + + + Move the color up + + + Flash zones when switching layout + + + Enable quick layout switch + + + Quick layout switch + + + Open Shortcut Guide + Let's get started! @@ -1096,7 +1145,7 @@ From there, simply click on a Markdown file or SVG icon in the File Explorer and PowerToys Run supports various action keys to funnel search queries for a specific subset of results. Typing {<} searches for running processes only, {?} will search only for file, or {.} for installed applications! See PowerToys documentation for the complete set of 'Action Keys' available. - {Shift} + {Win} + {/} to open Shortcut Guide, press it again to close or press {Esc}. + {Win} + {?} to open Shortcut Guide, press it again to close or press {Esc}. You can also launch it by holding the {Win} key for one second! Tips & tricks @@ -1160,42 +1209,67 @@ From there, simply click on a Markdown file or SVG icon in the File Explorer and PowerToys Settings - - Position & appearance + + About Espresso - - Show PowerToys Run on - as in Show PowerToys Run on primary monitor + + A convenient way to keep your PC awake on-demand. - - Monitor with mouse cursor + + Enable Espresso - - Monitor with focused window + + Off (Passive) - - Primary monitor + + Keep awake indefinitely - - Plugins are loading... + + Keep awake temporarily - - Move the color down + + Your PC operates according to its current power plan - - Move the color up + + Keeps your PC awake until the setting is disabled - - Flash zones when switching layout + + Keeps your PC awake until the set time elapses - - Enable quick layout switch + + Keep screen on - - Quick layout switch + + Mode - - Open Shortcut Guide + + Behavior + + + Hours + + + Minutes + + + Den Delimarsky's Espresso + + + https://espresso.den.dev + URL. Do not loc + + + Espresso + Module name, do not loc + + + Espresso is a Windows tool designed to keep your PC awake on-demand without having to manage its power settings. This behavior can be helpful when running time-consuming tasks while ensuring that your PC does not go to sleep or turn off its screens. + + + Open {PowerToys Settings} and enable Espresso + + + You can always change modes quickly by {right-clicking the Espresso icon} in the system tray. An error occurred trying to update to diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI/Views/EspressoPage.xaml b/src/settings-ui/Microsoft.PowerToys.Settings.UI/Views/EspressoPage.xaml new file mode 100644 index 000000000000..a1cdcb327c90 --- /dev/null +++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI/Views/EspressoPage.xaml @@ -0,0 +1,188 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI/Views/EspressoPage.xaml.cs b/src/settings-ui/Microsoft.PowerToys.Settings.UI/Views/EspressoPage.xaml.cs new file mode 100644 index 000000000000..5a47c6165b4b --- /dev/null +++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI/Views/EspressoPage.xaml.cs @@ -0,0 +1,23 @@ +// 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 Microsoft.PowerToys.Settings.UI.Library; +using Microsoft.PowerToys.Settings.UI.Library.ViewModels; +using Windows.UI.Xaml.Controls; + +namespace Microsoft.PowerToys.Settings.UI.Views +{ + public sealed partial class EspressoPage : Page + { + private EspressoViewModel ViewModel { get; set; } + + public EspressoPage() + { + var settingsUtils = new SettingsUtils(); + ViewModel = new EspressoViewModel(SettingsRepository.GetInstance(settingsUtils), SettingsRepository.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage); + DataContext = ViewModel; + InitializeComponent(); + } + } +} diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI/Views/ShellPage.xaml b/src/settings-ui/Microsoft.PowerToys.Settings.UI/Views/ShellPage.xaml index 96a250ed32db..46cd11f4dbb9 100644 --- a/src/settings-ui/Microsoft.PowerToys.Settings.UI/Views/ShellPage.xaml +++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI/Views/ShellPage.xaml @@ -41,7 +41,13 @@ - + + + + + + + diff --git a/src/settings-ui/PowerToys.Settings/PowerToys.Settings.csproj b/src/settings-ui/PowerToys.Settings/PowerToys.Settings.csproj index 92daaa3d46ac..1e2cec7e34e9 100644 --- a/src/settings-ui/PowerToys.Settings/PowerToys.Settings.csproj +++ b/src/settings-ui/PowerToys.Settings/PowerToys.Settings.csproj @@ -34,6 +34,9 @@ ..\..\..\$(Platform)\$(Configuration)\Settings false + full + true + DEBUG;TRACE