Compare commits

..

4 Commits

Author SHA1 Message Date
Jessica Dene Earley-Cha
3f3e04086e Add developer documentation for implementing telemetry events in PowerToys modules. (#44912)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

Adds a new doc (`doc/devdocs/Events.md`) that walks developers through
how to add telemetry events to PowerToys with next steps of reaching out
to Carlos or Jessica.

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #xxx
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2026-02-10 11:22:51 -08:00
Mike Griese
3b874a9567 CmdPal: Port the devhome perf widgets to cmdpal (#45217)
Pretty direct port of the code, to prove it works.

There's definitely some improvement to be made here, esp WRT to the
network and GPU listing - networks should all just be listed. Or at
least automatically track the active one. And GPU should aggregate a
bunch of stats.

And we can probably add the details to these list items.

But most importantly, _it works_.

re: #45201
2026-02-10 06:00:27 -06:00
Kai Tao
7a86543c8d v0.97.2 hotfix release note (#45485)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #xxx
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed

---------

Co-authored-by: Jiří Polášek <me@jiripolasek.com>
2026-02-09 23:37:48 -08:00
Kai Tao
67a013f729 Advanced Paste: Handle Foundry local Port change on the fly (#45362)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
Foundry Local returns 400 Bad Request if a manual port change made for
foundry local.

Fix #45340

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [X] Closes: #45340
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
Follow steps described in the issue, and the advanced paste can work
without having to restart powertoys itself
2026-02-10 15:31:23 +08:00
36 changed files with 3575 additions and 203 deletions

View File

@@ -38,6 +38,7 @@ Gbps
gcode
Heatshrink
Mbits
Kbits
MBs
mkv
msix
@@ -97,6 +98,7 @@ Yubico
Perplexity
Groq
svgl
devhome
# KEYS
@@ -322,6 +324,7 @@ REGSTR
# Misc Win32 APIs and PInvokes
INVOKEIDLIST
MEMORYSTATUSEX
# PowerRename metadata pattern abbreviations (used in tests and regex patterns)
DDDD
@@ -342,3 +345,7 @@ reportbug
#ffmpeg
crf
nostdin
# Performance counter keys
engtype
Nonpaged

View File

@@ -192,6 +192,7 @@ ycv
yeelam
Yuniardi
yuyoyuppe
zadjii
Zeol
Zhao
Zhaopeng
@@ -228,6 +229,7 @@ regedit
roslyn
Skia
Spotify
taskmgr
tldr
Vanara
wangyi
@@ -243,4 +245,3 @@ xamlstyler
Xavalon
Xbox
Youdao
zadjii

View File

@@ -197,6 +197,7 @@ Canvascustomlayout
CAPTUREBLT
CAPTURECHANGED
CARETBLINKING
carlos
Carlseibert
CAtl
caub
@@ -217,6 +218,7 @@ certmgr
cfp
CHANGECBCHAIN
changecursor
chatasweetie
checkmarks
CHILDACTIVATE
CHILDWINDOW
@@ -2242,6 +2244,7 @@ YSpeed
YStr
YTimer
YVIRTUALSCREEN
zamora
ZEROINIT
zonability
zonable

View File

@@ -219,6 +219,10 @@
<Platform Solution="*|x64" Project="x64" />
<Deploy />
</Project>
<Project Path="src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/Microsoft.CmdPal.Ext.PerformanceMonitor.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Microsoft.CmdPal.Ext.PowerToys.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />

View File

@@ -103,10 +103,38 @@ There are <a href="https://learn.microsoft.com/windows/powertoys/install#communi
</details>
## ✨ What's new
**Version 0.97.1 (January 2026)**
**Version 0.97.2 (Feb 2026)**
This patch release fixes several important stability issues identified in v0.97.0 based on incoming reports. Check out the [v0.97.0](https://github.com/microsoft/PowerToys/releases/tag/v0.97.0) notes for the full list of changes.
## Advanced Paste
- #45207 Fixed a crash in the Advanced Paste settings page caused by null values during JSON deserialization.
## Color Picker
- #45367 Fixed contrast issue in Color picker UI.
## Command Palette
- #45194 Fixed an issue where some Command Palette PowerToys Extension strings were not localised.
## Cursor Wrap
- #45210 Fixed "Automatically activate on utility startup" setting not persisting when disabled. Thanks [@ThanhNguyxn](https://github.com/ThanhNguyxn)!
- #45303 Added option to disable Cursor Wrapping when only a single monitor is connected. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
## Image Resizer
- #45184 Fixed Image Resizer not working after upgrading PowerToys on Windows 10 by properly cleaning up legacy sparse app packages.
## LightSwitch
- #45304 Fixed Light Switch startup logic to correctly apply the appropriate theme on launch.
## Workspaces
- #45183 Fixed overlay positioning issue in workspace snapshot draw caused by DPI-aware coordinate mismatch.
## Quick Access and Measure Tool
- #45443 Fixed crash related to `IsShownInSwitchers` property when Explorer is not running.
**Version 0.97.1 (January 2026)**
**Highlights**
### Advanced Paste

197
doc/devdocs/events.md Normal file
View File

@@ -0,0 +1,197 @@
# Telemetry Events
PowerToys collects limited telemetry to understand feature usage, reliability, and product quality. When adding a new telemetry event, follow the steps below to ensure the event is properly declared, documented, and available after release.
**⚠️ Important**: Telemetry must never include personal information, file paths, or usergenerated content.
## Developer Effort Overview (What to Expect)
Adding a telemetry event is a **multi-step process** that typically spans several areas of the codebase and documentation.
At a high level, developers should expect to:
1. Within one PR:
1. Add a new telemetry event(s) to module
1. Add the new event(s) DATA_AND_PRIVACY.md
1. Reach out to @carlos-zamora or @chatasweetie so internal scripts can process new event(s)
### Privacy Guidelines
**NEVER** log:
- User data (text, files, emails, etc.)
- File paths or filenames
- Personal information
- Sensitive system information
- Anything that could identify a specific user
DO log:
- Feature usage (which features, how often)
- Success/failure status
- Timing/performance metrics
- Error types (not error messages with user data)
- Aggregate counts
### Event Naming Convention
Follow this pattern: `UtilityName_EventDescription`
Examples:
- `ColorPicker_Session`
- `FancyZones_LayoutApplied`
- `PowerRename_Rename`
- `AdvancedPaste_FormatClicked`
- `CmdPal_ExtensionInvoked`
## Adding Telemetry Events to PowerToys
PowerToys uses ETW (Event Tracing for Windows) for telemetry in both C++ and C# modules. The telemetry system is:
- Opt-in by default (disabled since v0.86)
- Privacy-focused - never logs personal info, file paths, or user-generated content
- Controlled by registry - HKEY_CURRENT_USER\Software\Classes\PowerToys\AllowDataDiagnostics
### C++ Telemetry Implementation
**Core Components**
| File | Purpose |
| ------------- |:-------------:|
| [ProjectTelemetry.h](../../src/common/Telemetry/ProjectTelemetry.h) | Declares the global ETW provider g_hProvider |
| [TraceBase.h](../../src/common/Telemetry/TraceBase.h) | Base class with RegisterProvider(), UnregisterProvider(), and IsDataDiagnosticsEnabled() check |
| [TraceLoggingDefines.h](../../src/common/Telemetry/TraceLoggingDefines.h) | Privacy tags and telemetry option group macros
#### Pattern for C++ Modules
1. Create a `Trace` class inheriting from `telemetry::TraceBase` (src/common/Telemetry/TraceBase.h):
```c
// trace.h
#pragma once
#include <common/Telemetry/TraceBase.h>
class Trace : public telemetry::TraceBase
{
public:
static void MyEvent(/* parameters */);
};
```
2. Implement events using `TraceLoggingWriteWrapper`:
```cpp
// trace.cpp
#include "trace.h"
#include <common/Telemetry/TraceBase.h>
TRACELOGGING_DEFINE_PROVIDER(
g_hProvider,
"Microsoft.PowerToys",
(0x38e8889b, 0x9731, 0x53f5, 0xe9, 0x01, 0xe8, 0xa7, 0xc1, 0x75, 0x30, 0x74),
TraceLoggingOptionProjectTelemetry());
void Trace::MyEvent(bool enabled)
{
TraceLoggingWriteWrapper(
g_hProvider,
"ModuleName_EventName", // Event name
TraceLoggingBoolean(enabled, "Enabled"), // Event data
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingBoolean(TRUE, "UTCReplace_AppSessionGuid"),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE));
}
```
**Key C++ Telemetry Macros**
| Macro | Purpose |
| ------------- |:-------------:|
| `TraceLoggingWriteWrapper` [CustomAction.cpp](../../installer/PowerToysSetupCustomActionsVNext/CustomAction.cpp) | Wraps `TraceLoggingWrite` with `IsDataDiagnosticsEnabled()` check |
| `ProjectTelemetryPrivacyDataTag(tag)` [TraceLoggingDefines.h](../../src/common/Telemetry/TraceLoggingDefines.h) | Sets privacy classification |
### C# Telemetry Implementation
**Core Components**
| File | Purpose |
| ------------- |:-------------:|
| [PowerToysTelemetry.cs](../../src/common/ManagedTelemetry/Telemetry/PowerToysTelemetry.cs) | Singleton `Log` instance with `WriteEvent<T>()` method |
| [EventBase.cs](../../src/common/ManagedTelemetry/Telemetry/Events/EventBase.cs) | Base class for all events (provides `EventName`, `Version`) |
| [IEvent.cs](../../src/common/ManagedTelemetry/Telemetry/Events/IEvent.cs) | Interface requiring `PartA_PrivTags` property |
| [TelemetryBase.cs](../../src/common/Telemetry/TelemetryBase.cs) | Inherits from `EventSource`, defines ETW constants |
| [DataDiagnosticsSettings.cs](../../src/common/ManagedTelemetry/Telemetry/DataDiagnosticsSettings.cs) | Registry-based enable/disable check
#### Pattern for C# Modules
1. Create an event class inheriting from `EventBase` and implementing `IEvent`:
```csharp
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Tracing;
using Microsoft.PowerToys.Telemetry;
using Microsoft.PowerToys.Telemetry.Events;
namespace MyModule.Telemetry
{
[EventData]
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
public class MyModuleEvent : EventBase, IEvent
{
// Event properties (logged as telemetry data)
public string SomeProperty { get; set; }
public int SomeValue { get; set; }
// Required: Privacy tag
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
// Optional: Set EventName in constructor (defaults to class name)
public MyModuleEvent(string prop, int val)
{
EventName = "MyModule_EventName";
SomeProperty = prop;
SomeValue = val;
}
}
}
```
2. Log the event:
```csharp
PowerToysTelemetry.Log.WriteEvent(new MyModuleEvent("value", 42));
```
**Privacy Tags (C#)**
| Tag | Use Case |
| ------------- |:-------------:|
| `PartA_PrivTags.ProductAndServiceUsage` [TelemetryBase.cs](../../src/common/Telemetry/TelemetryBase.cs) | Feature usage events
| `PartA_PrivTags.ProductAndServicePerformance` [TelemetryBase.cs](../../src/common/Telemetry/TelemetryBase.cs) | Performance/timing events
### Update DATA_AND_PRIVACY.md file
Add your new event(s) to [DATA_AND_PRIVACY.md](../../DATA_AND_PRIVACY.md).
## Launch Product Version Containing the new events
Events do not become active until they ship in a released PowerToys version. After your PRs are merged:
- The event will begin firing once users install the version that includes it
- In order for PowerToys to process these events, you must complete the next section
## Next Steps
Reach out to @carlos-zamora or @chatasweetie so internal scripts can process new event(s).
## Summary
Required steps:
1. In one PR:
- Add the event(s) in code
- Document event(s) in DATA_AND_PRIVACY.md
1. Ship the change in a PowerToys release
1. Reach out for next steps

View File

@@ -5,8 +5,6 @@
#include <spdlog/sinks/base_sink.h>
#include <filesystem>
#include <string_view>
#include <tuple>
#include <vector>
#include "../../src/common/logger/logger.h"
#include "../../src/common/utils/gpo.h"
@@ -59,135 +57,6 @@ constexpr inline const wchar_t *DataDiagnosticsRegValueName = L"AllowDataDiagnos
static Shared::Trace::ETWTrace trace{L"PowerToys_Installer"};
namespace
{
struct VersionQuad
{
uint16_t major = 0;
uint16_t minor = 0;
uint16_t patch = 0;
uint16_t revision = 0;
bool operator>(const VersionQuad& other) const
{
return std::tie(major, minor, patch, revision) > std::tie(other.major, other.minor, other.patch, other.revision);
}
};
std::wstring VersionToWString(const VersionQuad& v)
{
return std::to_wstring(v.major) + L"." + std::to_wstring(v.minor) + L"." + std::to_wstring(v.patch) + L"." + std::to_wstring(v.revision);
}
bool TryGetFileVersion(const std::wstring& filePath, VersionQuad& version)
{
DWORD dummyHandle = 0;
DWORD verSize = GetFileVersionInfoSizeW(filePath.c_str(), &dummyHandle);
if (verSize == 0)
{
return false;
}
std::vector<BYTE> verData(verSize);
if (!GetFileVersionInfoW(filePath.c_str(), 0, verSize, verData.data()))
{
return false;
}
VS_FIXEDFILEINFO* verInfo = nullptr;
UINT verInfoSize = 0;
if (!VerQueryValueW(verData.data(), L"\\", reinterpret_cast<LPVOID*>(&verInfo), &verInfoSize) || verInfo == nullptr)
{
return false;
}
version.major = HIWORD(verInfo->dwFileVersionMS);
version.minor = LOWORD(verInfo->dwFileVersionMS);
version.patch = HIWORD(verInfo->dwFileVersionLS);
version.revision = LOWORD(verInfo->dwFileVersionLS);
return true;
}
bool IsPowerToysPerUserProduct(const wchar_t* productCode, const wchar_t* userSid, MSIINSTALLCONTEXT context)
{
if ((context != MSIINSTALLCONTEXT_USERMANAGED) && (context != MSIINSTALLCONTEXT_USERUNMANAGED))
{
return false;
}
wchar_t componentPath[MAX_PATH]{};
DWORD pathLength = MAX_PATH;
INSTALLSTATE state = MsiGetComponentPathExW(productCode, POWERTOYS_EXE_COMPONENT, userSid, context, componentPath, &pathLength);
return state == INSTALLSTATE_LOCAL || state == INSTALLSTATE_SOURCE || state == INSTALLSTATE_DEFAULT;
}
bool IsAnyPowerToysPerUserInstallPresent()
{
static constexpr wchar_t sidAllUsers[] = L"S-1-1-0";
const DWORD contexts = MSIINSTALLCONTEXT_USERMANAGED | MSIINSTALLCONTEXT_USERUNMANAGED;
for (DWORD index = 0;; ++index)
{
WCHAR productCode[39]{};
WCHAR sidBuffer[256]{};
DWORD sidLength = static_cast<DWORD>(std::size(sidBuffer));
MSIINSTALLCONTEXT installedContext = MSIINSTALLCONTEXT_NONE;
UINT enumResult = MsiEnumProductsExW(
nullptr,
sidAllUsers,
contexts,
index,
productCode,
&installedContext,
sidBuffer,
&sidLength);
if (enumResult == ERROR_NO_MORE_ITEMS)
{
break;
}
if (enumResult != ERROR_SUCCESS && enumResult != ERROR_MORE_DATA)
{
continue;
}
std::wstring dynamicSid;
const wchar_t* sidPtr = sidBuffer[0] ? sidBuffer : nullptr;
if (enumResult == ERROR_MORE_DATA)
{
dynamicSid.resize(sidLength + 1);
DWORD retrySidLength = static_cast<DWORD>(dynamicSid.size());
enumResult = MsiEnumProductsExW(
nullptr,
sidAllUsers,
contexts,
index,
productCode,
&installedContext,
dynamicSid.data(),
&retrySidLength);
if (enumResult != ERROR_SUCCESS)
{
continue;
}
dynamicSid.resize(retrySidLength);
sidPtr = dynamicSid.empty() || dynamicSid[0] == L'\0' ? nullptr : dynamicSid.c_str();
}
if (IsPowerToysPerUserProduct(productCode, sidPtr, installedContext))
{
return true;
}
}
return false;
}
}
inline bool isDataDiagnosticEnabled()
{
HKEY key{};
@@ -468,69 +337,6 @@ LExit:
return WcaFinalize(er);
}
UINT __stdcall CheckInstallGuardsCA(MSIHANDLE hInstall)
{
HRESULT hr = S_OK;
UINT er = ERROR_SUCCESS;
LPWSTR currentScope = nullptr;
LPWSTR installFolder = nullptr;
hr = WcaInitialize(hInstall, "CheckInstallGuardsCA");
ExitOnFailure(hr, "Failed to initialize");
hr = WcaGetProperty(L"InstallScope", &currentScope);
ExitOnFailure(hr, "Failed to get InstallScope property");
if (currentScope != nullptr && std::wstring{ currentScope } == L"perMachine" && IsAnyPowerToysPerUserInstallPresent())
{
PMSIHANDLE hRecord = MsiCreateRecord(0);
MsiRecordSetStringW(hRecord, 0, L"PowerToys is already installed per-user for at least one account. Please uninstall all per-user PowerToys installations before installing machine-wide.");
MsiProcessMessage(hInstall, static_cast<INSTALLMESSAGE>(INSTALLMESSAGE_ERROR + MB_OK), hRecord);
hr = E_ABORT;
ExitOnFailure(hr, "Per-user installation detected while attempting machine-wide install");
}
hr = WcaGetProperty(L"INSTALLFOLDER", &installFolder);
ExitOnFailure(hr, "Failed to get INSTALLFOLDER property");
if (installFolder && *installFolder != L'\0')
{
const auto installedExePath = std::filesystem::path(installFolder) / L"PowerToys.exe";
if (std::filesystem::exists(installedExePath))
{
VersionQuad existingVersion;
if (TryGetFileVersion(installedExePath.wstring(), existingVersion))
{
const VersionQuad targetVersion{
static_cast<uint16_t>(VERSION_MAJOR),
static_cast<uint16_t>(VERSION_MINOR),
static_cast<uint16_t>(VERSION_REVISION),
0
};
if (existingVersion > targetVersion)
{
const auto existingVersionText = VersionToWString(existingVersion);
const auto targetVersionText = VersionToWString(targetVersion);
const auto message = L"A newer PowerToys version (" + existingVersionText + L") already exists in the installation folder. The requested installer version (" + targetVersionText + L") is older. Uninstall the newer version first.";
PMSIHANDLE hRecord = MsiCreateRecord(0);
MsiRecordSetStringW(hRecord, 0, message.c_str());
MsiProcessMessage(hInstall, static_cast<INSTALLMESSAGE>(INSTALLMESSAGE_ERROR + MB_OK), hRecord);
hr = E_ABORT;
ExitOnFailure(hr, "A higher PowerToys.exe version already exists");
}
}
}
}
LExit:
ReleaseStr(currentScope);
ReleaseStr(installFolder);
er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE;
return WcaFinalize(er);
}
// We've deprecated Video Conference Mute. This Custom Action cleans up any stray registry entry for the driver dll.
UINT __stdcall CleanVideoConferenceRegistryCA(MSIHANDLE hInstall)
{

View File

@@ -3,7 +3,6 @@ LIBRARY "PowerToysSetupCustomActionsVNext"
EXPORTS
LaunchPowerToysCA
CheckGPOCA
CheckInstallGuardsCA
CleanVideoConferenceRegistryCA
ApplyModulesRegistryChangeSetsCA
DetectPrevInstallPathCA

View File

@@ -121,7 +121,6 @@
<Custom Action="SetUnApplyModulesRegistryChangeSetsParam" Before="UnApplyModulesRegistryChangeSets" />
<Custom Action="CheckGPO" After="InstallInitialize" Condition="NOT Installed" />
<Custom Action="CheckInstallGuards" After="CheckGPO" Condition="NOT Installed" />
<Custom Action="SetBundleInstallLocationData" Before="SetBundleInstallLocation" Condition="NOT Installed OR WIX_UPGRADE_DETECTED" />
<Custom Action="SetBundleInstallLocation" After="InstallFiles" Condition="NOT Installed OR WIX_UPGRADE_DETECTED" />
<Custom Action="ApplyModulesRegistryChangeSets" After="InstallFiles" Condition="NOT Installed" />
@@ -259,7 +258,6 @@
<CustomAction Id="UnRegisterCmdPalPackage" Return="ignore" Impersonate="yes" Execute="deferred" DllEntry="UnRegisterCmdPalPackageCA" BinaryRef="PTCustomActions" />
<CustomAction Id="CheckGPO" Return="check" Impersonate="yes" DllEntry="CheckGPOCA" BinaryRef="PTCustomActions" />
<CustomAction Id="CheckInstallGuards" Return="check" Impersonate="yes" DllEntry="CheckInstallGuardsCA" BinaryRef="PTCustomActions" />
<CustomAction Id="InstallCmdPalPackage" Return="ignore" Impersonate="yes" Execute="deferred" DllEntry="InstallCmdPalPackageCA" BinaryRef="PTCustomActions" />

View File

@@ -34,8 +34,7 @@ public sealed class FoundryLocalModelProvider : ILanguageModelProvider
}
// Check if model is in catalog
var isInCatalog = _catalogModels?.Any(m => m.Name == modelId) ?? false;
if (!isInCatalog)
if (!EnsureModelInCatalog(modelId))
{
var errorMessage = $"{modelId} is not supported in Foundry Local. Please configure supported models in Settings.";
Logger.LogError($"[FoundryLocal] {errorMessage}");
@@ -43,15 +42,28 @@ public sealed class FoundryLocalModelProvider : ILanguageModelProvider
}
// Ensure the model is loaded before returning chat client
var isLoaded = _foundryClient!.EnsureModelLoaded(modelId).GetAwaiter().GetResult();
var isLoaded = EnsureModelLoadedWithRefresh(modelId);
if (!isLoaded)
{
Logger.LogError($"[FoundryLocal] Failed to load model: {modelId}");
throw new InvalidOperationException($"Failed to load the model '{modelId}'.");
}
var client = _foundryClient;
if (client == null)
{
const string message = "Foundry Local client could not be created. Please make sure Foundry Local is installed and running.";
Logger.LogError($"[FoundryLocal] {message}");
throw new InvalidOperationException(message);
}
// Use ServiceUri instead of Endpoint since Endpoint already includes /v1
var baseUri = _foundryClient.GetServiceUri();
var baseUri = client.GetServiceUri();
if (baseUri == null && TryRefreshClient("Service URI was not available"))
{
baseUri = _foundryClient?.GetServiceUri();
}
if (baseUri == null)
{
const string message = "Foundry Local service URL is not available. Please make sure Foundry Local is installed and running.";
@@ -124,6 +136,7 @@ public sealed class FoundryLocalModelProvider : ILanguageModelProvider
if (_foundryClient != null && _catalogModels != null && _catalogModels.Any())
{
await _foundryClient.EnsureRunning().ConfigureAwait(false);
_serviceUrl = await _foundryClient.GetServiceUrl().ConfigureAwait(false);
return;
}
@@ -153,4 +166,75 @@ public sealed class FoundryLocalModelProvider : ILanguageModelProvider
Logger.LogInfo($"[FoundryLocal] Available: {available}");
return available;
}
private bool EnsureModelInCatalog(string modelId)
{
var isInCatalog = _catalogModels?.Any(m => m.Name == modelId) ?? false;
if (isInCatalog)
{
return true;
}
Logger.LogWarning($"[FoundryLocal] Model not found in catalog. Refreshing client for model: {modelId}");
if (!TryRefreshClient("Model not in catalog"))
{
return false;
}
return _catalogModels?.Any(m => m.Name == modelId) ?? false;
}
private bool EnsureModelLoadedWithRefresh(string modelId)
{
var isLoaded = false;
try
{
isLoaded = _foundryClient!.EnsureModelLoaded(modelId).GetAwaiter().GetResult();
}
catch (Exception ex)
{
Logger.LogWarning($"[FoundryLocal] EnsureModelLoaded failed: {ex.Message}");
}
if (isLoaded)
{
return true;
}
if (!TryRefreshClient("EnsureModelLoaded failed"))
{
return false;
}
try
{
return _foundryClient!.EnsureModelLoaded(modelId).GetAwaiter().GetResult();
}
catch (Exception ex)
{
Logger.LogError($"[FoundryLocal] EnsureModelLoaded failed after refresh: {ex.Message}", ex);
return false;
}
}
private bool TryRefreshClient(string reason)
{
Logger.LogInfo($"[FoundryLocal] Refreshing Foundry Local client: {reason}");
try
{
_foundryClient = null;
_catalogModels = null;
_serviceUrl = null;
InitializeAsync().GetAwaiter().GetResult();
return _foundryClient != null;
}
catch (Exception ex)
{
Logger.LogError($"[FoundryLocal] Failed to refresh Foundry Local client: {ex.Message}", ex);
return false;
}
}
}

View File

@@ -13,6 +13,7 @@ using Microsoft.CmdPal.Ext.Bookmarks;
using Microsoft.CmdPal.Ext.Calc;
using Microsoft.CmdPal.Ext.ClipboardHistory;
using Microsoft.CmdPal.Ext.Indexer;
using Microsoft.CmdPal.Ext.PerformanceMonitor;
using Microsoft.CmdPal.Ext.Registry;
using Microsoft.CmdPal.Ext.RemoteDesktop;
using Microsoft.CmdPal.Ext.Shell;
@@ -177,6 +178,7 @@ public partial class App : Application, IDisposable
services.AddSingleton<ICommandProvider, TimeDateCommandsProvider>();
services.AddSingleton<ICommandProvider, SystemCommandExtensionProvider>();
services.AddSingleton<ICommandProvider, RemoteDesktopCommandProvider>();
services.AddSingleton<ICommandProvider, PerformanceMonitorCommandsProvider>();
}
private static void AddUIServices(ServiceCollection services, DispatcherQueue dispatcherQueue)

View File

@@ -141,6 +141,7 @@
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.Apps\Microsoft.CmdPal.Ext.Apps.csproj" />
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.Bookmark\Microsoft.CmdPal.Ext.Bookmarks.csproj" />
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.Calc\Microsoft.CmdPal.Ext.Calc.csproj" />
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.PerformanceMonitor\Microsoft.CmdPal.Ext.PerformanceMonitor.csproj" />
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.Registry\Microsoft.CmdPal.Ext.Registry.csproj" />
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.Shell\Microsoft.CmdPal.Ext.Shell.csproj" />
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.TimeDate\Microsoft.CmdPal.Ext.TimeDate.csproj" />

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 CoreWidgetProvider.Widgets.Enums;
public enum WidgetDataState
{
Unknown,
Requested, // Request is out, waiting on a response. Current data is stale.
Okay, // Received and updated data, stable state.
Failed, // Failed retrieving data.
}

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 CoreWidgetProvider.Widgets.Enums;
public enum WidgetPageState
{
Unknown,
Configure,
Loading,
Content,
}

View File

@@ -0,0 +1,146 @@
// 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.Diagnostics;
using System.Linq;
namespace CoreWidgetProvider.Helpers;
internal sealed partial class CPUStats : IDisposable
{
// CPU counters
private readonly PerformanceCounter _procPerf = new("Processor Information", "% Processor Utility", "_Total");
private readonly PerformanceCounter _procPerformance = new("Processor Information", "% Processor Performance", "_Total");
private readonly PerformanceCounter _procFrequency = new("Processor Information", "Processor Frequency", "_Total");
private readonly Dictionary<Process, PerformanceCounter> _cpuCounters = new();
internal sealed class ProcessStats
{
public Process? Process { get; set; }
public float CpuUsage { get; set; }
}
public float CpuUsage { get; set; }
public float CpuSpeed { get; set; }
public ProcessStats[] ProcessCPUStats { get; set; }
public List<float> CpuChartValues { get; set; } = new();
public CPUStats()
{
CpuUsage = 0;
ProcessCPUStats =
[
new ProcessStats(),
new ProcessStats(),
new ProcessStats()
];
InitCPUPerfCounters();
}
private void InitCPUPerfCounters()
{
var allProcesses = Process.GetProcesses().Where(p => (long)p.MainWindowHandle != 0);
foreach (var process in allProcesses)
{
_cpuCounters.Add(process, new PerformanceCounter("Process", "% Processor Time", process.ProcessName, true));
}
}
public void GetData(bool includeTopProcesses)
{
var timer = Stopwatch.StartNew();
CpuUsage = _procPerf.NextValue() / 100;
var usageMs = timer.ElapsedMilliseconds;
CpuSpeed = _procFrequency.NextValue() * (_procPerformance.NextValue() / 100);
var speedMs = timer.ElapsedMilliseconds - usageMs;
lock (CpuChartValues)
{
ChartHelper.AddNextChartValue(CpuUsage * 100, CpuChartValues);
}
var chartMs = timer.ElapsedMilliseconds - speedMs;
var processCPUUsages = new Dictionary<Process, float>();
if (includeTopProcesses)
{
foreach (var processCounter in _cpuCounters)
{
try
{
// process might be terminated
processCPUUsages.Add(processCounter.Key, processCounter.Value.NextValue() / Environment.ProcessorCount);
}
catch (InvalidOperationException)
{
// _log.Information($"ProcessCounter Key {processCounter.Key} no longer exists, removing from _cpuCounters.");
_cpuCounters.Remove(processCounter.Key);
}
catch (Exception)
{
// _log.Error(ex, "Error going through process counters.");
}
}
var cpuIndex = 0;
foreach (var processCPUValue in processCPUUsages.OrderByDescending(x => x.Value).Take(3))
{
ProcessCPUStats[cpuIndex].Process = processCPUValue.Key;
ProcessCPUStats[cpuIndex].CpuUsage = processCPUValue.Value;
cpuIndex++;
}
}
timer.Stop();
var total = timer.ElapsedMilliseconds;
var processesMs = total - chartMs;
// CoreLogger.LogDebug($"[{usageMs}]+[{speedMs}]+[{chartMs}]+[{processesMs}]=[{total}]");
}
internal string CreateCPUImageUrl()
{
return ChartHelper.CreateImageUrl(CpuChartValues, ChartHelper.ChartType.CPU);
}
internal string GetCpuProcessText(int cpuProcessIndex)
{
if (cpuProcessIndex >= ProcessCPUStats.Length)
{
return "no data";
}
return $"{ProcessCPUStats[cpuProcessIndex].Process?.ProcessName} ({ProcessCPUStats[cpuProcessIndex].CpuUsage / 100:p})";
}
internal void KillTopProcess(int cpuProcessIndex)
{
if (cpuProcessIndex >= ProcessCPUStats.Length)
{
return;
}
ProcessCPUStats[cpuProcessIndex].Process?.Kill();
}
public void Dispose()
{
_procPerf.Dispose();
_procPerformance.Dispose();
_procFrequency.Dispose();
foreach (var counter in _cpuCounters.Values)
{
counter.Dispose();
}
}
}

View File

@@ -0,0 +1,289 @@
// 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.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Xml.Linq;
namespace CoreWidgetProvider.Helpers;
internal sealed class ChartHelper
{
public enum ChartType
{
CPU,
GPU,
Mem,
Net,
}
public const int ChartHeight = 86;
public const int ChartWidth = 268;
private const string LightGrayBoxStyle = "fill:none;stroke:lightgrey;stroke-width:1";
private const string CPULineStyle = "fill:none;stroke:rgb(57,184,227);stroke-width:1";
private const string GPULineStyle = "fill:none;stroke:rgb(222,104,242);stroke-width:1";
private const string MemLineStyle = "fill:none;stroke:rgb(92,158,250);stroke-width:1";
private const string NetLineStyle = "fill:none;stroke:rgb(245,98,142);stroke-width:1";
private const string FillStyle = "fill:url(#gradientId);stroke:transparent";
private const string CPUBrushStop1Style = "stop-color:rgb(57,184,227);stop-opacity:0.4";
private const string CPUBrushStop2Style = "stop-color:rgb(0,86,110);stop-opacity:0.25";
private const string GPUBrushStop1Style = "stop-color:rgb(222,104,242);stop-opacity:0.4";
private const string GPUBrushStop2Style = "stop-color:rgb(125,0,138);stop-opacity:0.25";
private const string MemBrushStop1Style = "stop-color:rgb(92,158,250);stop-opacity:0.4";
private const string MemBrushStop2Style = "stop-color:rgb(0,34,92);stop-opacity:0.25";
private const string NetBrushStop1Style = "stop-color:rgb(245,98,142);stop-opacity:0.4";
private const string NetBrushStop2Style = "stop-color:rgb(130,0,47);stop-opacity:0.25";
private const string SvgElement = "svg";
private const string RectElement = "rect";
private const string PolylineElement = "polyline";
private const string DefsElement = "defs";
private const string LinearGradientElement = "linearGradient";
private const string StopElement = "stop";
private const string HeightAttr = "height";
private const string WidthAttr = "width";
private const string StyleAttr = "style";
private const string PointsAttr = "points";
private const string OffsetAttr = "offset";
private const string X1Attr = "x1";
private const string X2Attr = "x2";
private const string Y1Attr = "y1";
private const string Y2Attr = "y2";
private const string IdAttr = "id";
private const int MaxChartValues = 34;
public static string CreateImageUrl(List<float> chartValues, ChartType type)
{
var chartStr = CreateChart(chartValues, type);
return "data:image/svg+xml;utf8," + chartStr;
}
/// <summary>
/// Creates an SVG image for the chart.
/// </summary>
/// <param name="chartValues">The values to plot on the chart</param>
/// <param name="type">The type of chart. Each chart type uses different colors.</param>
/// <remarks>
/// The SVG is made of three shapes: <br/>
/// 1. A colored line, plotting the points on the graph <br/>
/// 2. A transparent line, outlining the gradient under the graph <br/>
/// 3. A grey box, outlining the entire image <br/>
/// The SVG also contains a definition for the fill gradient.
/// </remarks>
/// <returns>A string representing the chart as an SVG image.</returns>
public static string CreateChart(List<float> chartValues, ChartType type)
{
// The SVG created by this method will look similar to this:
/*
<svg height="102" width="264">
<defs>
<linearGradient x1="0%" x2="0%" y1="0%" y2="100%" id="gradientId">
<stop offset="0%" style="stop-color:rgb(222,104,242);stop-opacity:0.4" />
<stop offset="95%" style="stop-color:rgb(125,0,138);stop-opacity:0.25" />
</linearGradient>
</defs>
<polyline points="1,91 10,71 253,51 262,31 262,101 1,101" style="fill:url(#gradientId);stroke:transparent" />
<polyline points="1,91 10,71 253,51 262,31" style="fill:none;stroke:rgb(222,104,242);stroke-width:1" />
<rect height="102" width="264" style="fill:none;stroke:lightgrey;stroke-width:1" />
</svg>
*/
// The following code can be uncommented for testing when a static image is desired.
/* chartValues.Clear();
chartValues = new List<float>
{
10, 30, 20, 40, 30, 50, 40, 60, 50, 100,
10, 30, 20, 40, 30, 50, 40, 60, 50, 70,
0, 30, 20, 40, 30, 50, 40, 60, 50, 70,
};*/
var chartDoc = new XDocument();
lock (chartValues)
{
var svgElement = CreateBlankSvg(ChartHeight, ChartWidth);
// Create the line that will show the points on the graph.
var lineElement = new XElement(PolylineElement);
var points = TransformPointsToLine(chartValues, out var startX, out var finalX);
lineElement.SetAttributeValue(PointsAttr, points.ToString());
lineElement.SetAttributeValue(StyleAttr, GetLineStyle(type));
// Create the line that will contain the gradient fill.
TransformPointsToLoop(points, startX, finalX);
var fillElement = new XElement(PolylineElement);
fillElement.SetAttributeValue(PointsAttr, points.ToString());
fillElement.SetAttributeValue(StyleAttr, FillStyle);
// Add the gradient definition and the three shapes to the svg.
svgElement.Add(CreateGradientDefinition(type));
svgElement.Add(fillElement);
svgElement.Add(lineElement);
svgElement.Add(CreateBorderBox(ChartHeight, ChartWidth));
chartDoc.Add(svgElement);
}
return chartDoc.ToString();
}
private static XElement CreateBlankSvg(int height, int width)
{
var svgElement = new XElement(SvgElement);
svgElement.SetAttributeValue(HeightAttr, height);
svgElement.SetAttributeValue(WidthAttr, width);
return svgElement;
}
private static XElement CreateGradientDefinition(ChartType type)
{
var defsElement = new XElement(DefsElement);
var gradientElement = new XElement(LinearGradientElement);
// Vertical gradients are created when x1 and x2 are equal and y1 and y2 differ.
gradientElement.SetAttributeValue(X1Attr, "0%");
gradientElement.SetAttributeValue(X2Attr, "0%");
gradientElement.SetAttributeValue(Y1Attr, "0%");
gradientElement.SetAttributeValue(Y2Attr, "100%");
gradientElement.SetAttributeValue(IdAttr, "gradientId");
string stop1Style;
string stop2Style;
switch (type)
{
case ChartType.GPU:
stop1Style = GPUBrushStop1Style;
stop2Style = GPUBrushStop2Style;
break;
case ChartType.Mem:
stop1Style = MemBrushStop1Style;
stop2Style = MemBrushStop2Style;
break;
case ChartType.Net:
stop1Style = NetBrushStop1Style;
stop2Style = NetBrushStop2Style;
break;
case ChartType.CPU:
default:
stop1Style = CPUBrushStop1Style;
stop2Style = CPUBrushStop2Style;
break;
}
var stop1 = new XElement(StopElement);
stop1.SetAttributeValue(OffsetAttr, "0%");
stop1.SetAttributeValue(StyleAttr, stop1Style);
var stop2 = new XElement(StopElement);
stop2.SetAttributeValue(OffsetAttr, "95%");
stop2.SetAttributeValue(StyleAttr, stop2Style);
gradientElement.Add(stop1);
gradientElement.Add(stop2);
defsElement.Add(gradientElement);
return defsElement;
}
private static XElement CreateBorderBox(int height, int width)
{
var boxElement = new XElement(RectElement);
boxElement.SetAttributeValue(HeightAttr, height);
boxElement.SetAttributeValue(WidthAttr, width);
boxElement.SetAttributeValue(StyleAttr, LightGrayBoxStyle);
return boxElement;
}
private static string GetLineStyle(ChartType type)
{
var lineStyle = type switch
{
ChartType.CPU => CPULineStyle,
ChartType.GPU => GPULineStyle,
ChartType.Mem => MemLineStyle,
ChartType.Net => NetLineStyle,
_ => CPULineStyle,
};
return lineStyle;
}
private static StringBuilder TransformPointsToLine(List<float> chartValues, out int startX, out int finalX)
{
var points = new StringBuilder();
// The X value where the graph starts must be adjusted so that the graph is right-aligned.
// The max available width of the widget is 268. Since there is a 1 px border around the chart, the width of the chart's line must be <=266.
// To create a chart of exactly the right size, we'll have 34 points with 8 pixels in between:
// 1 px left border + 1 px for first point + 33 segments * 8 px per segment + 1 px right border = 267 pixels total in width.
const int pxBetweenPoints = 8;
// When the chart doesn't have all points yet, move the chart over to the right by increasing the starting X coordinate.
// For a chart with only 1 point, the svg will not render a polyline.
// For a chart with 2 points, starting X coordinate == 2 + (34 - 2) * 8 == 1 + 32 * 8 == 1 + 256 == 257
// For a chart with 30 points, starting X coordinate == 2 + (34 - 34) * 8 == 1 + 0 * 8 == 1 + 0 == 2
startX = 2 + ((MaxChartValues - chartValues.Count) * pxBetweenPoints);
finalX = startX;
// Extend graph by one pixel to cover gap on the left when the chart is otherwise full.
if (startX == 2)
{
var invertedHeight = 100 - chartValues[0];
var finalY = (invertedHeight * (ChartHeight / 100.0)) - 1;
points.Append(CultureInfo.InvariantCulture, $"1,{finalY} ");
}
foreach (var origY in chartValues)
{
// We receive the height as a number up from the X axis (bottom of the chart), but we have to invert it
// since the Y coordinate is relative to the top of the chart.
var invertedHeight = 100 - origY;
// Scale the final Y to whatever the chart height is.
var finalY = (invertedHeight * (ChartHeight / 100.0)) - 1;
points.Append(CultureInfo.InvariantCulture, $"{finalX},{finalY} ");
finalX += pxBetweenPoints;
}
// Remove the trailing space.
if (points.Length > 0)
{
points.Remove(points.Length - 1, 1);
finalX -= pxBetweenPoints;
}
return points;
}
private static void TransformPointsToLoop(StringBuilder points, int startX, int finalX)
{
// Close the loop.
// Add a point at the most recent X value that corresponds with y = 0
points.Append(CultureInfo.InvariantCulture, $" {finalX},{ChartHeight - 1}");
// Add a point at the start of the chart that corresponds with y = 0
points.Append(CultureInfo.InvariantCulture, $" {startX},{ChartHeight - 1}");
}
public static void AddNextChartValue(float value, List<float> chartValues)
{
if (chartValues.Count >= MaxChartValues)
{
chartValues.RemoveAt(0);
}
chartValues.Add(value);
}
}

View File

@@ -0,0 +1,147 @@
// 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 Timer = System.Timers.Timer;
namespace CoreWidgetProvider.Helpers;
internal sealed partial class DataManager : IDisposable
{
private readonly SystemData _systemData;
private readonly DataType _dataType;
private readonly Timer _updateTimer;
private readonly Action _updateAction;
private const int OneSecondInMilliseconds = 1000;
public DataManager(DataType type, Action updateWidget)
{
_systemData = new SystemData();
_updateAction = updateWidget;
_dataType = type;
_updateTimer = new Timer(OneSecondInMilliseconds);
_updateTimer.Elapsed += UpdateTimer_Elapsed;
_updateTimer.AutoReset = true;
_updateTimer.Enabled = false;
}
private void GetMemoryData()
{
lock (SystemData.MemStats)
{
SystemData.MemStats.GetData();
}
}
private void GetNetworkData()
{
lock (SystemData.NetStats)
{
SystemData.NetStats.GetData();
}
}
private void GetGPUData()
{
lock (SystemData.GPUStats)
{
SystemData.GPUStats.GetData();
}
}
private void GetCPUData(bool includeTopProcesses)
{
lock (SystemData.CpuStats)
{
SystemData.CpuStats.GetData(includeTopProcesses);
}
}
private void UpdateTimer_Elapsed(object? sender, System.Timers.ElapsedEventArgs e)
{
switch (_dataType)
{
case DataType.CPU:
case DataType.CpuWithTopProcesses:
{
// CPU
GetCPUData(_dataType == DataType.CpuWithTopProcesses);
break;
}
case DataType.GPU:
{
// gpu
GetGPUData();
break;
}
case DataType.Memory:
{
// memory
GetMemoryData();
break;
}
case DataType.Network:
{
// network
GetNetworkData();
break;
}
}
_updateAction?.Invoke();
}
internal MemoryStats GetMemoryStats()
{
lock (SystemData.MemStats)
{
return SystemData.MemStats;
}
}
internal NetworkStats GetNetworkStats()
{
lock (SystemData.NetStats)
{
return SystemData.NetStats;
}
}
internal GPUStats GetGPUStats()
{
lock (SystemData.GPUStats)
{
return SystemData.GPUStats;
}
}
internal CPUStats GetCPUStats()
{
lock (SystemData.CpuStats)
{
return SystemData.CpuStats;
}
}
public void Start()
{
_updateTimer.Start();
}
public void Stop()
{
_updateTimer.Stop();
}
public void Dispose()
{
_systemData.Dispose();
_updateTimer.Dispose();
}
}

View File

@@ -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.
namespace CoreWidgetProvider.Helpers;
public enum DataType
{
/// <summary>
/// CPU related data.
/// </summary>
CPU,
/// <summary>
/// CPU related data, including the top processes.
/// Calculating the top processes takes a lot longer,
/// so by default we don't.
/// </summary>
CpuWithTopProcesses,
/// <summary>
/// Memory related data.
/// </summary>
Memory,
/// <summary>
/// GPU related data.
/// </summary>
GPU,
/// <summary>
/// Network related data.
/// </summary>
Network,
}

View File

@@ -0,0 +1,283 @@
// 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.Diagnostics;
using System.Globalization;
using System.Linq;
namespace CoreWidgetProvider.Helpers;
internal sealed partial class GPUStats : IDisposable
{
// GPU counters
private readonly Dictionary<int, List<PerformanceCounter>> _gpuCounters = new();
private readonly List<Data> _stats = new();
public sealed class Data
{
public string? Name { get; set; }
public int PhysId { get; set; }
public float Usage { get; set; }
public float Temperature { get; set; }
public List<float> GpuChartValues { get; set; } = new();
}
public GPUStats()
{
GetGPUPerfCounters();
LoadGPUsFromCounters();
}
public void GetGPUPerfCounters()
{
// There are really 4 different things we should be tracking the usage
// of. Similar to how the instance name ends with `3D`, the following
// suffixes are important.
//
// * `3D`
// * `VideoEncode`
// * `VideoDecode`
// * `VideoProcessing`
//
// We could totally put each of those sets of counters into their own
// set. That's what we should do, so that we can report the sum of those
// numbers as the total utilization, and then have them broken out in
// the card template and in the details metadata.
_gpuCounters.Clear();
var perfCounterCategory = new PerformanceCounterCategory("GPU Engine");
var instanceNames = perfCounterCategory.GetInstanceNames();
foreach (var instanceName in instanceNames)
{
if (!instanceName.EndsWith("3D", StringComparison.InvariantCulture))
{
continue;
}
var utilizationCounters = perfCounterCategory.GetCounters(instanceName)
.Where(x => x.CounterName.StartsWith("Utilization Percentage", StringComparison.InvariantCulture));
foreach (var counter in utilizationCounters)
{
var counterKey = counter.InstanceName;
// skip these values
GetKeyValueFromCounterKey("pid", ref counterKey);
GetKeyValueFromCounterKey("luid", ref counterKey);
int phys;
var success = int.TryParse(GetKeyValueFromCounterKey("phys", ref counterKey), out phys);
if (success)
{
GetKeyValueFromCounterKey("eng", ref counterKey);
var engtype = GetKeyValueFromCounterKey("engtype", ref counterKey);
if (engtype != "3D")
{
continue;
}
if (!_gpuCounters.TryGetValue(phys, out var value))
{
value = new();
_gpuCounters.Add(phys, value);
}
value.Add(counter);
}
}
}
}
public void LoadGPUsFromCounters()
{
// The old dev home code tracked GPU stats by querying WMI for the list
// of GPUs, and then matching them up with the performance counter IDs.
//
// We can't use WMI here, because it drags in a dependency on
// Microsoft.Management.Infrastructure, which is not compatible with
// AOT.
//
// For now, we'll just use the indices as the GPU names.
_stats.Clear();
foreach (var (k, v) in _gpuCounters)
{
var id = k;
var counters = v;
_stats.Add(new Data() { PhysId = id, Name = "GPU " + id });
}
}
public void GetData()
{
foreach (var gpu in _stats)
{
List<PerformanceCounter>? counters;
var success = _gpuCounters.TryGetValue(gpu.PhysId, out counters);
if (success && counters != null)
{
// TODO: This outer try/catch should be replaced with more secure locking around shared resources.
try
{
var sum = 0.0f;
var countersToRemove = new List<PerformanceCounter>();
foreach (var counter in counters)
{
try
{
// NextValue() can throw an InvalidOperationException if the counter is no longer there.
sum += counter.NextValue();
}
catch (InvalidOperationException)
{
// We can't modify the list during the loop, so save it to remove at the end.
// _log.Information(ex, "Failed to get next value, remove");
countersToRemove.Add(counter);
}
catch (Exception)
{
// _log.Error(ex, "Error going through process counters.");
}
}
foreach (var counter in countersToRemove)
{
counters.Remove(counter);
counter.Dispose();
}
gpu.Usage = sum / 100;
lock (gpu.GpuChartValues)
{
ChartHelper.AddNextChartValue(sum, gpu.GpuChartValues);
}
}
catch (Exception)
{
// _log.Error(ex, "Error summing process counters.");
}
}
}
}
internal string CreateGPUImageUrl(int gpuChartIndex)
{
return ChartHelper.CreateImageUrl(_stats.ElementAt(gpuChartIndex).GpuChartValues, ChartHelper.ChartType.GPU);
}
internal string GetGPUName(int gpuActiveIndex)
{
if (_stats.Count <= gpuActiveIndex)
{
return string.Empty;
}
return _stats[gpuActiveIndex].Name ?? string.Empty;
}
internal int GetPrevGPUIndex(int gpuActiveIndex)
{
if (_stats.Count == 0)
{
return 0;
}
if (gpuActiveIndex == 0)
{
return _stats.Count - 1;
}
return gpuActiveIndex - 1;
}
internal int GetNextGPUIndex(int gpuActiveIndex)
{
if (_stats.Count == 0)
{
return 0;
}
if (gpuActiveIndex == _stats.Count - 1)
{
return 0;
}
return gpuActiveIndex + 1;
}
internal float GetGPUUsage(int gpuActiveIndex, string gpuActiveEngType)
{
if (_stats.Count <= gpuActiveIndex)
{
return 0;
}
return _stats[gpuActiveIndex].Usage;
}
internal string GetGPUTemperature(int gpuActiveIndex)
{
// MG Jan 2026: This code was lifted from the old Dev Home codebase.
// However, the performance counters for GPU temperature are not being
// collected. So this function always returns "--" for now.
//
// I have not done the code archeology to figure out why they were
// removed.
if (_stats.Count <= gpuActiveIndex)
{
return "--";
}
var temperature = _stats[gpuActiveIndex].Temperature;
if (temperature == 0)
{
return "--";
}
return temperature.ToString("0.", CultureInfo.InvariantCulture) + " \x00B0C";
}
private string GetKeyValueFromCounterKey(string key, ref string counterKey)
{
if (!counterKey.StartsWith(key, StringComparison.InvariantCulture))
{
return "error";
}
counterKey = counterKey.Substring(key.Length + 1);
if (key.Equals("engtype", StringComparison.Ordinal))
{
return counterKey;
}
var pos = counterKey.IndexOf('_');
if (key.Equals("luid", StringComparison.Ordinal))
{
pos = counterKey.IndexOf('_', pos + 1);
}
var retValue = counterKey.Substring(0, pos);
counterKey = counterKey.Substring(pos + 1);
return retValue;
}
public void Dispose()
{
foreach (var counterPair in _gpuCounters)
{
foreach (var counter in counterPair.Value)
{
counter.Dispose();
}
}
}
}

View File

@@ -0,0 +1,100 @@
// 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.Diagnostics;
using System.Runtime.InteropServices;
using Windows.Win32;
namespace CoreWidgetProvider.Helpers;
internal sealed partial class MemoryStats : IDisposable
{
private readonly PerformanceCounter _memCommitted = new("Memory", "Committed Bytes", string.Empty);
private readonly PerformanceCounter _memCached = new("Memory", "Cache Bytes", string.Empty);
private readonly PerformanceCounter _memCommittedLimit = new("Memory", "Commit Limit", string.Empty);
private readonly PerformanceCounter _memPoolPaged = new("Memory", "Pool Paged Bytes", string.Empty);
private readonly PerformanceCounter _memPoolNonPaged = new("Memory", "Pool Nonpaged Bytes", string.Empty);
public float MemUsage
{
get; set;
}
public ulong AllMem
{
get; set;
}
public ulong UsedMem
{
get; set;
}
public ulong MemCommitted
{
get; set;
}
public ulong MemCommitLimit
{
get; set;
}
public ulong MemCached
{
get; set;
}
public ulong MemPagedPool
{
get; set;
}
public ulong MemNonPagedPool
{
get; set;
}
public List<float> MemChartValues { get; set; } = new();
public void GetData()
{
Windows.Win32.System.SystemInformation.MEMORYSTATUSEX memStatus = default;
memStatus.dwLength = (uint)Marshal.SizeOf<Windows.Win32.System.SystemInformation.MEMORYSTATUSEX>();
if (PInvoke.GlobalMemoryStatusEx(ref memStatus))
{
AllMem = memStatus.ullTotalPhys;
var availableMem = memStatus.ullAvailPhys;
UsedMem = AllMem - availableMem;
MemUsage = (float)UsedMem / AllMem;
lock (MemChartValues)
{
ChartHelper.AddNextChartValue(MemUsage * 100, MemChartValues);
}
}
MemCached = (ulong)_memCached.NextValue();
MemCommitted = (ulong)_memCommitted.NextValue();
MemCommitLimit = (ulong)_memCommittedLimit.NextValue();
MemPagedPool = (ulong)_memPoolPaged.NextValue();
MemNonPagedPool = (ulong)_memPoolNonPaged.NextValue();
}
public string CreateMemImageUrl()
{
return ChartHelper.CreateImageUrl(MemChartValues, ChartHelper.ChartType.Mem);
}
public void Dispose()
{
_memCommitted.Dispose();
_memCached.Dispose();
_memCommittedLimit.Dispose();
_memPoolPaged.Dispose();
_memPoolNonPaged.Dispose();
}
}

View File

@@ -0,0 +1,169 @@
// 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.Diagnostics;
using System.Linq;
namespace CoreWidgetProvider.Helpers;
internal sealed partial class NetworkStats : IDisposable
{
private readonly Dictionary<string, List<PerformanceCounter>> _networkCounters = new();
private Dictionary<string, Data> NetworkUsages { get; set; } = new();
private Dictionary<string, List<float>> NetChartValues { get; set; } = new();
public sealed class Data
{
public float Usage
{
get; set;
}
public float Sent
{
get; set;
}
public float Received
{
get; set;
}
}
public NetworkStats()
{
InitNetworkPerfCounters();
}
private void InitNetworkPerfCounters()
{
var perfCounterCategory = new PerformanceCounterCategory("Network Interface");
var instanceNames = perfCounterCategory.GetInstanceNames();
foreach (var instanceName in instanceNames)
{
var instanceCounters = new List<PerformanceCounter>();
instanceCounters.Add(new PerformanceCounter("Network Interface", "Bytes Sent/sec", instanceName));
instanceCounters.Add(new PerformanceCounter("Network Interface", "Bytes Received/sec", instanceName));
instanceCounters.Add(new PerformanceCounter("Network Interface", "Current Bandwidth", instanceName));
_networkCounters.Add(instanceName, instanceCounters);
NetChartValues.Add(instanceName, new List<float>());
NetworkUsages.Add(instanceName, new Data());
}
}
public void GetData()
{
float maxUsage = 0;
foreach (var networkCounterWithName in _networkCounters)
{
try
{
var sent = networkCounterWithName.Value[0].NextValue();
var received = networkCounterWithName.Value[1].NextValue();
var bandWidth = networkCounterWithName.Value[2].NextValue();
if (bandWidth == 0)
{
continue;
}
var usage = 8 * (sent + received) / bandWidth;
var name = networkCounterWithName.Key;
NetworkUsages[name].Sent = sent;
NetworkUsages[name].Received = received;
NetworkUsages[name].Usage = usage;
var chartValues = NetChartValues[name];
lock (chartValues)
{
ChartHelper.AddNextChartValue(usage * 100, chartValues);
}
if (usage > maxUsage)
{
maxUsage = usage;
}
}
catch (Exception)
{
// Log.Error(ex, "Error getting network data.");
}
}
}
public string CreateNetImageUrl(int netChartIndex)
{
return ChartHelper.CreateImageUrl(NetChartValues.ElementAt(netChartIndex).Value, ChartHelper.ChartType.Net);
}
public string GetNetworkName(int networkIndex)
{
if (NetChartValues.Count <= networkIndex)
{
return string.Empty;
}
return NetChartValues.ElementAt(networkIndex).Key;
}
public Data GetNetworkUsage(int networkIndex)
{
if (NetChartValues.Count <= networkIndex)
{
return new Data();
}
var currNetworkName = NetChartValues.ElementAt(networkIndex).Key;
if (!NetworkUsages.TryGetValue(currNetworkName, out var value))
{
return new Data();
}
return value;
}
public int GetPrevNetworkIndex(int networkIndex)
{
if (NetChartValues.Count == 0)
{
return 0;
}
if (networkIndex == 0)
{
return NetChartValues.Count - 1;
}
return networkIndex - 1;
}
public int GetNextNetworkIndex(int networkIndex)
{
if (NetChartValues.Count == 0)
{
return 0;
}
if (networkIndex == NetChartValues.Count - 1)
{
return 0;
}
return networkIndex + 1;
}
public void Dispose()
{
foreach (var counterPair in _networkCounters)
{
foreach (var counter in counterPair.Value)
{
counter.Dispose();
}
}
}
}

View File

@@ -0,0 +1,104 @@
// 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.Text;
using Microsoft.CmdPal.Core.Common;
namespace CoreWidgetProvider.Helpers;
// This class was pilfered from devhome, but changed much more substantially to
// get the resources out of our resources.pri the way we need.
public static class Resources
{
private static readonly Windows.ApplicationModel.Resources.Core.ResourceMap? _map;
private static readonly string ResourcesPath = "Microsoft.CmdPal.Ext.PerformanceMonitor/Resources";
static Resources()
{
try
{
var currentResourceManager = Windows.ApplicationModel.Resources.Core.ResourceManager.Current;
if (currentResourceManager.MainResourceMap is not null)
{
_map = currentResourceManager.MainResourceMap;
}
}
catch (Exception)
{
// Resource map not available (e.g., during unit tests)
_map = null;
}
}
public static string GetResource(string identifier, ILogger? log = null)
{
if (_map is null)
{
return identifier;
}
var fullKey = $"{ResourcesPath}/{identifier}";
var val = _map.GetValue(fullKey);
#if DEBUG
if (val == null)
{
log?.LogError($"Failed loading resource: {identifier}");
DebugResources(log);
}
#endif
return val!.ValueAsString;
}
public static string ReplaceIdentifersFast(
string original)
{
// walk the string, looking for a pair of '%' characters
StringBuilder sb = new();
var length = original.Length;
for (var i = 0; i < length; i++)
{
if (original[i] == '%')
{
var end = original.IndexOf('%', i + 1);
if (end > i)
{
var identifier = original.Substring(i + 1, end - i - 1);
var resourceString = GetResource(identifier);
sb.Append(resourceString);
i = end; // move index to the end '%'
continue;
}
}
sb.Append(original[i]);
}
return sb.ToString();
}
private static void DebugResources(ILogger? log)
{
var currentResourceManager = Windows.ApplicationModel.Resources.Core.ResourceManager.Current;
StringBuilder sb = new();
foreach (var (k, v) in currentResourceManager.AllResourceMaps)
{
sb.AppendLine(k);
foreach (var (k2, v2) in v)
{
sb.Append('\t');
sb.AppendLine(k2);
}
sb.AppendLine();
}
log?.LogDebug($"Resource maps:");
log?.LogDebug(sb.ToString());
}
}

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;
namespace CoreWidgetProvider.Helpers;
internal sealed partial class SystemData : IDisposable
{
public static MemoryStats MemStats { get; set; } = new MemoryStats();
public static NetworkStats NetStats { get; set; } = new NetworkStats();
public static GPUStats GPUStats { get; set; } = new GPUStats();
public static CPUStats CpuStats { get; set; } = new CPUStats();
public SystemData()
{
}
public void Dispose()
{
}
}

View File

@@ -0,0 +1,14 @@
The code in this directory was largely lifted from the [DevHome repo].
The specific directory we're using is
https://github.com/microsoft/devhome/tree/main/extensions/CoreWidgetProvider
This has code for all the DevHome performance widgets.
Minimal changes have been made to match our style guidelines.
Additionally, a much larger change was made to Resources.cs, to match our own
resource loading needs.
The code was lifted as of commit d52734ce0e33a82af3313d24c3c2979c37b68bab
[DevHome repo]: https://github.com/microsoft/devhome/

View File

@@ -0,0 +1,20 @@
{
"type": "AdaptiveCard",
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"version": "1.5",
"body": [
{
"type": "Container",
"items": [
{
"type": "TextBlock",
"text": "%Widget_Template/Loading%",
"wrap": true,
"horizontalAlignment": "center"
}
],
"verticalContentAlignment": "center",
"height": "stretch"
}
]
}

View File

@@ -0,0 +1,99 @@
{
"type": "AdaptiveCard",
"body": [
{
"type": "Container",
"$when": "${errorMessage != null}",
"items": [
{
"type": "TextBlock",
"text": "${errorMessage}",
"wrap": true,
"size": "small"
}
],
"style": "warning"
},
{
"type": "Container",
"$when": "${errorMessage == null}",
"items": [
{
"type": "Image",
"url": "${cpuGraphUrl}",
"height": "${chartHeight}",
"width": "${chartWidth}",
"$when": "${$host.widgetSize != \"small\"}",
"horizontalAlignment": "center"
},
{
"type": "ColumnSet",
"columns": [
{
"type": "Column",
"items": [
{
"type": "TextBlock",
"isSubtle": true,
"text": "%CPUUsage_Widget_Template/CPU_Usage%"
},
{
"type": "TextBlock",
"size": "large",
"weight": "bolder",
"text": "${cpuUsage}"
}
]
},
{
"type": "Column",
"items": [
{
"type": "TextBlock",
"isSubtle": true,
"horizontalAlignment": "right",
"text": "%CPUUsage_Widget_Template/CPU_Speed%"
},
{
"type": "TextBlock",
"size": "large",
"horizontalAlignment": "right",
"text": "${cpuSpeed}"
}
]
}
]
},
{
"type": "Container",
"$when": false,
"items": [
{
"type": "TextBlock",
"isSubtle": true,
"text": "%CPUUsage_Widget_Template/Processes%",
"wrap": true
},
{
"type": "TextBlock",
"size": "medium",
"text": "${cpuProc1}"
},
{
"type": "TextBlock",
"size": "medium",
"text": "${cpuProc2}"
},
{
"type": "TextBlock",
"size": "medium",
"text": "${cpuProc3}"
}
]
}
]
}
],
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"version": "1.5"
}

View File

@@ -0,0 +1,86 @@
{
"type": "AdaptiveCard",
"body": [
{
"type": "Container",
"$when": "${errorMessage != null}",
"items": [
{
"type": "TextBlock",
"text": "${errorMessage}",
"wrap": true,
"size": "small"
}
],
"style": "warning"
},
{
"type": "Container",
"$when": "${errorMessage == null}",
"items": [
{
"type": "Image",
"url": "${gpuGraphUrl}",
"height": "${chartHeight}",
"width": "${chartWidth}",
"$when": "${$host.widgetSize != \"small\"}",
"horizontalAlignment": "center"
},
{
"type": "ColumnSet",
"columns": [
{
"type": "Column",
"items": [
{
"text": "%GPUUsage_Widget_Template/GPU_Usage%",
"type": "TextBlock",
"size": "small",
"isSubtle": true
},
{
"text": "${gpuUsage}",
"type": "TextBlock",
"size": "large",
"weight": "bolder"
}
]
},
{
"type": "Column",
"items": [
{
"text": "%GPUUsage_Widget_Template/GPU_Temperature%",
"type": "TextBlock",
"size": "small",
"isSubtle": true,
"horizontalAlignment": "right"
},
{
"text": "${gpuTemp}",
"type": "TextBlock",
"size": "large",
"weight": "bolder",
"horizontalAlignment": "right"
}
]
}
]
},
{
"text": "%GPUUsage_Widget_Template/GPU_Name%",
"type": "TextBlock",
"size": "small",
"isSubtle": true
},
{
"text": "${gpuName}",
"type": "TextBlock",
"size": "medium"
}
]
}
],
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"version": "1.5"
}

View File

@@ -0,0 +1,178 @@
{
"type": "AdaptiveCard",
"body": [
{
"type": "Container",
"$when": "${errorMessage != null}",
"items": [
{
"type": "TextBlock",
"text": "${errorMessage}",
"wrap": true,
"size": "small"
}
],
"style": "warning"
},
{
"type": "Container",
"$when": "${errorMessage == null}",
"items": [
{
"type": "Image",
"url": "${memGraphUrl}",
"height": "${chartHeight}",
"width": "${chartWidth}",
"$when": "${$host.widgetSize != \"small\"}",
"horizontalAlignment": "center"
},
{
"type": "ColumnSet",
"columns": [
{
"type": "Column",
"items": [
{
"text": "%Memory_Widget_Template/UsedMemory%",
"type": "TextBlock",
"size": "small",
"isSubtle": true
},
{
"text": "${usedMem}",
"type": "TextBlock",
"size": "${if($host.widgetSize == \"small\", \"medium\", \"large\")}",
"weight": "bolder"
}
]
},
{
"type": "Column",
"items": [
{
"text": "%Memory_Widget_Template/AllMemory%",
"type": "TextBlock",
"size": "small",
"isSubtle": true,
"horizontalAlignment": "right"
},
{
"text": "${allMem}",
"type": "TextBlock",
"size": "${if($host.widgetSize == \"small\", \"medium\", \"large\")}",
"weight": "bolder",
"horizontalAlignment": "right"
}
]
}
]
},
{
"type": "ColumnSet",
"columns": [
{
"type": "Column",
"items": [
{
"text": "%Memory_Widget_Template/Committed%",
"type": "TextBlock",
"size": "small",
"isSubtle": true
},
{
"text": "${committedMem}/${committedLimitMem}",
"type": "TextBlock",
"size": "medium"
}
]
},
{
"type": "Column",
"items": [
{
"text": "%Memory_Widget_Template/Cached%",
"type": "TextBlock",
"size": "small",
"isSubtle": true,
"horizontalAlignment": "right"
},
{
"text": "${cachedMem}",
"type": "TextBlock",
"size": "medium",
"horizontalAlignment": "right"
}
]
}
]
},
{
"type": "ColumnSet",
"$when": "${$host.widgetSize == \"large\"}",
"columns": [
{
"type": "Column",
"items": [
{
"text": "%Memory_Widget_Template/PagedPool%",
"type": "TextBlock",
"size": "small",
"isSubtle": true
},
{
"text": "${pagedPoolMem}",
"type": "TextBlock",
"size": "medium"
}
]
},
{
"type": "Column",
"items": [
{
"text": "%Memory_Widget_Template/NonPagedPool%",
"type": "TextBlock",
"size": "small",
"isSubtle": true,
"horizontalAlignment": "right"
},
{
"text": "${nonPagedPoolMem}",
"type": "TextBlock",
"size": "medium",
"horizontalAlignment": "right"
}
]
}
]
},
{
"type": "ColumnSet",
"$when": "${$host.widgetSize != \"small\"}",
"columns": [
{
"type": "Column",
"items": [
{
"text": "%Memory_Widget_Template/MemoryUsage%",
"type": "TextBlock",
"size": "small",
"isSubtle": true,
"horizontalAlignment": "right"
},
{
"text": "${memUsage}",
"type": "TextBlock",
"size": "medium",
"horizontalAlignment": "right"
}
]
}
]
}
]
}
],
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"version": "1.5"
}

View File

@@ -0,0 +1,88 @@
{
"type": "AdaptiveCard",
"body": [
{
"type": "Container",
"$when": "${errorMessage != null}",
"items": [
{
"type": "TextBlock",
"text": "${errorMessage}",
"wrap": true,
"size": "small"
}
],
"style": "warning"
},
{
"type": "Container",
"$when": "${errorMessage == null}",
"items": [
{
"type": "Image",
"url": "${netGraphUrl}",
"height": "${chartHeight}",
"width": "${chartWidth}",
"$when": "${$host.widgetSize != \"small\"}",
"horizontalAlignment": "center"
},
{
"type": "ColumnSet",
"columns": [
{
"type": "Column",
"items": [
{
"text": "%NetworkUsage_Widget_Template/Sent%",
"type": "TextBlock",
"spacing": "none",
"size": "small",
"isSubtle": true
},
{
"text": "${netSent}",
"type": "TextBlock",
"size": "large",
"weight": "bolder"
}
]
},
{
"type": "Column",
"items": [
{
"text": "%NetworkUsage_Widget_Template/Received%",
"type": "TextBlock",
"spacing": "none",
"size": "small",
"isSubtle": true,
"horizontalAlignment": "right"
},
{
"text": "${netReceived}",
"type": "TextBlock",
"size": "large",
"weight": "bolder",
"horizontalAlignment": "right"
}
]
}
]
},
{
"text": "%NetworkUsage_Widget_Template/Network_Name%",
"type": "TextBlock",
"size": "small",
"isSubtle": true
},
{
"text": "${networkName}",
"type": "TextBlock",
"size": "medium"
}
]
}
],
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"version": "1.5"
}

View File

@@ -0,0 +1,31 @@
// 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.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.PerformanceMonitor;
internal sealed class Icons
{
internal static IconInfo CpuIcon => new("\uE9D9"); // CPU icon
internal static IconInfo MemoryIcon => new("\uE964"); // Memory icon
internal static IconInfo DiskIcon => new("\uE977"); // PC1 icon
internal static IconInfo HardDriveIcon => new("\uEDA2"); // HardDrive icon
internal static IconInfo NetworkIcon => new("\uEC05"); // Network icon
internal static IconInfo StackedAreaIcon => new("\uE9D2"); // StackedArea icon
internal static IconInfo GpuIcon => new("\uE950"); // Component icon
internal static IconInfo NavigateBackwardIcon => new("\uE72B"); // Previous icon
internal static IconInfo NavigateForwardIcon => new("\uE72A"); // Next icon
}
#pragma warning restore SA1402 // File may only contain a single type

View File

@@ -0,0 +1,58 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\..\Common.Dotnet.AotCompatibility.props" />
<Import Project="..\Common.ExtDependencies.props" />
<PropertyGroup>
<RootNamespace>Microsoft.CmdPal.Ext.PerformanceMonitor</RootNamespace>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<!-- MRT from windows app sdk will search for a pri file with the same name of the module before defaulting to resources.pri -->
<ProjectPriFileName>Microsoft.CmdPal.Ext.PerformanceMonitor.pri</ProjectPriFileName>
<nullable>enable</nullable>
<LangVersion>preview</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Diagnostics.PerformanceCounter" />
<PackageReference Include="Microsoft.Windows.CsWin32">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\Core\Microsoft.CmdPal.Core.Common\Microsoft.CmdPal.Core.Common.csproj" />
<!-- CmdPal Toolkit reference now included via Common.ExtDependencies.props -->
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DependentUpon>Resources.resx</DependentUpon>
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
<Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<None Update="DevHome\Templates\SystemCPUUsageTemplate.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="DevHome\Templates\SystemGPUUsageTemplate.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="DevHome\Templates\SystemMemoryTemplate.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="DevHome\Templates\SystemNetworkUsageTemplate.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@@ -0,0 +1 @@
GlobalMemoryStatusEx

View File

@@ -0,0 +1,123 @@
// 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.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Foundation;
namespace Microsoft.CmdPal.Ext.PerformanceMonitor;
#pragma warning disable SA1402 // File may only contain a single type
/// <summary>
/// Helper class for creating ListPage's which can listen for when they're
/// loaded and unloaded. This works because CmdPal will attach an event handler
/// to the ItemsChanged event when the page is added to the UI, and remove it
/// when the page is removed from the UI.
///
/// Subclasses should override the Loaded and Unloaded methods to start/stop
/// any background work needed to populate the page.
/// </summary>
internal abstract partial class OnLoadStaticListPage : OnLoadBasePage, IListPage
{
private string _searchText = string.Empty;
public virtual string PlaceholderText { get; set => SetProperty(ref field, value); } = string.Empty;
public virtual string SearchText { get => _searchText; set => SetProperty(ref _searchText, value); }
public virtual bool ShowDetails { get; set => SetProperty(ref field, value); }
public virtual bool HasMoreItems { get; set => SetProperty(ref field, value); }
public virtual IFilters? Filters { get; set => SetProperty(ref field, value); }
public virtual IGridProperties? GridProperties { get; set => SetProperty(ref field, value); }
public virtual ICommandItem? EmptyContent { get; set => SetProperty(ref field, value); }
public void LoadMore()
{
}
protected void SetSearchNoUpdate(string newSearchText)
{
_searchText = newSearchText;
}
public abstract IListItem[] GetItems();
}
/// <summary>
/// Helper class for creating ContentPage's which can listen for when they're
/// loaded and unloaded. This works because CmdPal will attach an event handler
/// to the ItemsChanged event when the page is added to the UI, and remove it
/// when the page is removed from the UI.
///
/// Subclasses should override the Loaded and Unloaded methods to start/stop
/// any background work needed to populate the page.
/// </summary>
internal abstract partial class OnLoadContentPage : OnLoadBasePage, IContentPage
{
public virtual IDetails? Details { get; set => SetProperty(ref field, value); }
public virtual IContextItem[] Commands { get; set => SetProperty(ref field, value); } = [];
public abstract IContent[] GetContent();
}
internal abstract partial class OnLoadBasePage : Page
{
private int _loadCount;
#pragma warning disable CS0067 // The event is never used
private event TypedEventHandler<object, IItemsChangedEventArgs>? InternalItemsChanged;
#pragma warning restore CS0067 // The event is never used
public event TypedEventHandler<object, IItemsChangedEventArgs> ItemsChanged
{
add
{
InternalItemsChanged += value;
if (_loadCount == 0)
{
Loaded();
}
_loadCount++;
}
remove
{
InternalItemsChanged -= value;
_loadCount--;
_loadCount = Math.Max(0, _loadCount);
if (_loadCount == 0)
{
Unloaded();
}
}
}
protected abstract void Loaded();
protected abstract void Unloaded();
protected void RaiseItemsChanged(int totalItems = -1)
{
try
{
// TODO #181 - This is the same thing that BaseObservable has to deal with.
InternalItemsChanged?.Invoke(this, new ItemsChangedEventArgs(totalItems));
}
catch
{
}
}
}
#pragma warning restore SA1402 // File may only contain a single type

View File

@@ -0,0 +1,40 @@
// 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 CoreWidgetProvider.Helpers;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.PerformanceMonitor;
public partial class PerformanceMonitorCommandsProvider : CommandProvider
{
private readonly ICommandItem[] _commands;
private readonly ICommandItem _band;
public PerformanceMonitorCommandsProvider()
{
DisplayName = Resources.GetResource("Performance_Monitor_Title");
Id = "PerformanceMonitor";
Icon = Icons.StackedAreaIcon;
var page = new PerformanceWidgetsPage(false);
var band = new PerformanceWidgetsPage(true);
_band = new CommandItem(band) { Title = DisplayName };
_commands = [
new CommandItem(page) { Title = DisplayName },
];
}
public override ICommandItem[] TopLevelCommands()
{
return _commands;
}
// Soon...
// public override ICommandItem[]? GetDockBands()
// {
// return new ICommandItem[] { _band };
// }
}

View File

@@ -0,0 +1,926 @@
// 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.Diagnostics;
using System.Globalization;
using System.IO;
using System.Text;
using System.Text.Json.Nodes;
using CoreWidgetProvider.Helpers;
using CoreWidgetProvider.Widgets.Enums;
using Microsoft.CmdPal.Core.Common;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.ApplicationModel;
namespace Microsoft.CmdPal.Ext.PerformanceMonitor;
#pragma warning disable SA1402 // File may only contain a single type
/// <summary>
/// Page for displaying performance monitor widgets. Can be used as both a list
/// in the main window, or as a band in the dock.
/// By using OnLoadStaticListPage, we can get onload/onunload events to start/stop
/// the data gathering.
/// </summary>
internal sealed partial class PerformanceWidgetsPage : OnLoadStaticListPage, IDisposable
{
public override string Id => "com.microsoft.cmdpal.performanceWidget";
public override string Title => Resources.GetResource("Performance_Monitor_Title");
public override IconInfo Icon => Icons.StackedAreaIcon;
private readonly bool _isBandPage;
private readonly SystemCPUUsageWidgetPage _cpuPage = new();
private readonly ListItem _cpuItem;
private readonly SystemMemoryUsageWidgetPage _memoryPage = new();
private readonly ListItem _memoryItem;
private readonly SystemNetworkUsageWidgetPage _networkPage = new();
private readonly ListItem _networkItem;
private readonly SystemGPUUsageWidgetPage _gpuPage = new();
private readonly ListItem _gpuItem;
// For bands, we want two bands, one for up and one for down
private ListItem? _networkUpItem;
private ListItem? _networkDownItem;
private string _networkUpSpeed = string.Empty;
private string _networkDownSpeed = string.Empty;
public PerformanceWidgetsPage(bool isBandPage = false)
{
_isBandPage = isBandPage;
_cpuItem = new ListItem(_cpuPage)
{
Title = _cpuPage.GetItemTitle(isBandPage),
MoreCommands = _cpuPage.Commands,
};
_cpuPage.Updated += (s, e) =>
{
_cpuItem.Title = _cpuPage.GetItemTitle(isBandPage);
};
_memoryItem = new ListItem(_memoryPage)
{
Title = _memoryPage.GetItemTitle(isBandPage),
MoreCommands = _memoryPage.Commands,
};
_memoryPage.Updated += (s, e) =>
{
_memoryItem.Title = _memoryPage.GetItemTitle(isBandPage);
};
_networkItem = new ListItem(_networkPage)
{
Title = _networkPage.GetItemTitle(isBandPage),
MoreCommands = _networkPage.Commands,
};
_networkPage.Updated += (s, e) =>
{
_networkItem.Title = _networkPage.GetItemTitle(isBandPage);
_networkUpSpeed = _networkPage.GetUpSpeed();
_networkDownSpeed = _networkPage.GetDownSpeed();
_networkDownItem?.Title = $"{_networkDownSpeed}";
_networkUpItem?.Title = $"{_networkUpSpeed}";
};
_gpuItem = new ListItem(_gpuPage)
{
Title = _gpuPage.GetItemTitle(isBandPage),
MoreCommands = _gpuPage.Commands,
};
_gpuPage.Updated += (s, e) =>
{
_gpuItem.Title = _gpuPage.GetItemTitle(isBandPage);
};
if (_isBandPage)
{
// add subtitles to them all
_cpuItem.Subtitle = Resources.GetResource("CPU_Usage_Subtitle");
_memoryItem.Subtitle = Resources.GetResource("Memory_Usage_Subtitle");
_networkItem.Subtitle = Resources.GetResource("Network_Usage_Subtitle");
_gpuItem.Subtitle = Resources.GetResource("GPU_Usage_Subtitle");
}
}
protected override void Loaded()
{
_cpuPage.PushActivate();
_memoryPage.PushActivate();
_networkPage.PushActivate();
_gpuPage.PushActivate();
}
protected override void Unloaded()
{
_cpuPage.PopActivate();
_memoryPage.PopActivate();
_networkPage.PopActivate();
_gpuPage.PopActivate();
}
public override IListItem[] GetItems()
{
if (!_isBandPage)
{
// TODO add details
return new[] { _cpuItem, _memoryItem, _networkItem, _gpuItem };
}
else
{
_networkUpItem = new ListItem(_networkPage)
{
Title = $"{_networkUpSpeed}",
Subtitle = Resources.GetResource("Network_Send_Subtitle"),
MoreCommands = _networkPage.Commands,
};
_networkDownItem = new ListItem(_networkPage)
{
Title = $"{_networkDownSpeed}",
Subtitle = Resources.GetResource("Network_Receive_Subtitle"),
MoreCommands = _networkPage.Commands,
};
return new[] { _cpuItem, _memoryItem, _networkDownItem, _networkUpItem, _gpuItem };
}
}
public void Dispose()
{
_cpuPage.Dispose();
_memoryPage.Dispose();
_networkPage.Dispose();
_gpuPage.Dispose();
}
}
/// <summary>
/// Base class for all the performance monitor widget pages.
/// This handles common stuff like loading their widget JSON
/// and updating it when needed.
/// </summary>
internal abstract partial class WidgetPage : OnLoadContentPage
{
internal event EventHandler? Updated;
protected Dictionary<string, string> ContentData { get; } = new();
protected WidgetPageState Page { get; set; } = WidgetPageState.Unknown;
protected Dictionary<WidgetPageState, string> Template { get; set; } = new();
protected JsonObject ContentDataJson
{
get
{
var json = new JsonObject();
lock (ContentData)
{
foreach (var kvp in ContentData)
{
if (kvp.Value is not null)
{
json[kvp.Key] = kvp.Value;
}
}
}
return json;
}
}
private readonly FormContent _formContent = new();
public void UpdateWidget()
{
lock (ContentData)
{
LoadContentData();
}
_formContent.DataJson = ContentDataJson.ToJsonString();
Updated?.Invoke(this, EventArgs.Empty);
}
protected abstract void LoadContentData();
protected abstract string GetTemplatePath(WidgetPageState page);
protected string GetTemplateForPage(WidgetPageState page)
{
if (Template.TryGetValue(page, out var value))
{
CoreLogger.LogDebug($"Using cached template for {page}");
return value;
}
try
{
var path = Path.Combine(Package.Current.EffectivePath, GetTemplatePath(page));
var template = File.ReadAllText(path, Encoding.Default) ?? throw new FileNotFoundException(path);
template = Resources.ReplaceIdentifersFast(template);
CoreLogger.LogDebug($"Caching template for {page}");
Template[page] = template;
return template;
}
catch (Exception e)
{
CoreLogger.LogError("Error getting template.", e);
return string.Empty;
}
}
public override IContent[] GetContent()
{
_formContent.TemplateJson = GetTemplateForPage(WidgetPageState.Content);
return [_formContent];
}
/// <summary>
/// Increment our tracker of how many pages have needed us active. This is a
/// little wackier than just OnLoad/Unload. Both the ListPage for
/// PerformanceWidgetsPage itself, AND the widget itself need the stats to
/// be updating. So we use a counter to track how many "clients" need us
/// active. When either is activated, we'll start updating. When both are
/// removed, we'll stop updating.
/// </summary>
internal virtual void PushActivate()
{
_loadCount++;
}
internal virtual void PopActivate()
{
_loadCount--;
}
private int _loadCount;
protected bool IsActive => _loadCount > 0;
protected override void Loaded()
{
PushActivate();
}
protected override void Unloaded()
{
PopActivate();
}
internal static string FloatToPercentString(float value)
{
return ((int)(value * 100)).ToString(CultureInfo.InvariantCulture) + "%";
}
}
internal sealed partial class SystemCPUUsageWidgetPage : WidgetPage, IDisposable
{
public override string Title => Resources.GetResource("CPU_Usage_Title");
public override string Id => "com.microsoft.cmdpal.cpu_widget";
public override IconInfo Icon => Icons.CpuIcon;
private readonly DataManager _dataManager;
public SystemCPUUsageWidgetPage()
{
_dataManager = new(DataType.CPU, () => UpdateWidget());
Commands = [
new CommandContextItem(OpenTaskManagerCommand.Instance),
];
}
protected override void LoadContentData()
{
// CoreLogger.LogDebug("Getting CPU stats");
try
{
ContentData.Clear();
var timer = Stopwatch.StartNew();
var currentData = _dataManager.GetCPUStats();
var dataDuration = timer.ElapsedMilliseconds;
ContentData["cpuUsage"] = FloatToPercentString(currentData.CpuUsage);
ContentData["cpuSpeed"] = SpeedToString(currentData.CpuSpeed);
ContentData["cpuGraphUrl"] = currentData.CreateCPUImageUrl();
ContentData["chartHeight"] = ChartHelper.ChartHeight + "px";
ContentData["chartWidth"] = ChartHelper.ChartWidth + "px";
// ContentData["cpuProc1"] = currentData.GetCpuProcessText(0);
// ContentData["cpuProc2"] = currentData.GetCpuProcessText(1);
// ContentData["cpuProc3"] = currentData.GetCpuProcessText(2);
var contentDuration = timer.ElapsedMilliseconds - dataDuration;
// CoreLogger.LogDebug($"CPU stats retrieved in {dataDuration} ms, content prepared in {contentDuration} ms. (Total {timer.ElapsedMilliseconds} ms)");
// DataState = WidgetDataState.Okay;
}
catch (Exception e)
{
// Log.Error(e, "Error retrieving stats.");
ContentData.Clear();
ContentData["errorMessage"] = e.Message;
// ContentData = content.ToJsonString();
// DataState = WidgetDataState.Failed;
return;
}
}
protected override string GetTemplatePath(WidgetPageState page)
{
return page switch
{
WidgetPageState.Content => @"DevHome\Templates\SystemCPUUsageTemplate.json",
WidgetPageState.Loading => @"DevHome\Templates\SystemCPUUsageTemplate.json",
_ => throw new NotImplementedException(),
};
}
public string GetItemTitle(bool isBandPage)
{
if (ContentData.TryGetValue("cpuUsage", out var usage))
{
return isBandPage ? usage : string.Format(CultureInfo.CurrentCulture, Resources.GetResource("CPU_Usage_Label"), usage);
}
else
{
return isBandPage ? Resources.GetResource("CPU_Usage_Unknown") : Resources.GetResource("CPU_Usage_Unknown_Label");
}
}
private string SpeedToString(float cpuSpeed)
{
return string.Format(CultureInfo.InvariantCulture, "{0:0.00} GHz", cpuSpeed / 1000);
}
internal override void PushActivate()
{
base.PushActivate();
if (IsActive)
{
_dataManager.Start();
}
}
internal override void PopActivate()
{
base.PopActivate();
if (!IsActive)
{
_dataManager.Stop();
}
}
public void Dispose()
{
_dataManager.Dispose();
}
}
internal sealed partial class SystemMemoryUsageWidgetPage : WidgetPage, IDisposable
{
public override string Id => "com.microsoft.cmdpal.memory_widget";
public override string Title => Resources.GetResource("Memory_Usage_Title");
public override IconInfo Icon => Icons.MemoryIcon;
private readonly DataManager _dataManager;
public SystemMemoryUsageWidgetPage()
{
_dataManager = new(DataType.Memory, () => UpdateWidget());
Commands = [
new CommandContextItem(OpenTaskManagerCommand.Instance),
];
}
protected override void LoadContentData()
{
// CoreLogger.LogDebug("Getting Memory stats");
try
{
ContentData.Clear();
var timer = Stopwatch.StartNew();
var currentData = _dataManager.GetMemoryStats();
var dataDuration = timer.ElapsedMilliseconds;
ContentData["allMem"] = MemUlongToString(currentData.AllMem);
ContentData["usedMem"] = MemUlongToString(currentData.UsedMem);
ContentData["memUsage"] = FloatToPercentString(currentData.MemUsage);
ContentData["committedMem"] = MemUlongToString(currentData.MemCommitted);
ContentData["committedLimitMem"] = MemUlongToString(currentData.MemCommitLimit);
ContentData["cachedMem"] = MemUlongToString(currentData.MemCached);
ContentData["pagedPoolMem"] = MemUlongToString(currentData.MemPagedPool);
ContentData["nonPagedPoolMem"] = MemUlongToString(currentData.MemNonPagedPool);
ContentData["memGraphUrl"] = currentData.CreateMemImageUrl();
ContentData["chartHeight"] = ChartHelper.ChartHeight + "px";
ContentData["chartWidth"] = ChartHelper.ChartWidth + "px";
var contentDuration = timer.ElapsedMilliseconds - dataDuration;
// CoreLogger.LogDebug($"Memory stats retrieved in {dataDuration} ms, content prepared in {contentDuration} ms. (Total {timer.ElapsedMilliseconds} ms)");
}
catch (Exception e)
{
ContentData.Clear();
ContentData["errorMessage"] = e.Message;
return;
}
}
protected override string GetTemplatePath(WidgetPageState page)
{
return page switch
{
WidgetPageState.Content => @"DevHome\Templates\SystemMemoryTemplate.json",
WidgetPageState.Loading => @"DevHome\Templates\SystemMemoryTemplate.json",
_ => throw new NotImplementedException(),
};
}
public string GetItemTitle(bool isBandPage)
{
if (ContentData.TryGetValue("memUsage", out var usage))
{
return isBandPage ? usage : string.Format(CultureInfo.CurrentCulture, Resources.GetResource("Memory_Usage_Label"), usage);
}
else
{
return isBandPage ? Resources.GetResource("Memory_Usage_Unknown") : Resources.GetResource("Memory_Usage_Unknown_Label");
}
}
private string MemUlongToString(ulong memBytes)
{
if (memBytes < 1024)
{
return memBytes.ToString(CultureInfo.InvariantCulture) + " B";
}
var memSize = memBytes / 1024.0;
if (memSize < 1024)
{
return memSize.ToString("0.00", CultureInfo.InvariantCulture) + " kB";
}
memSize /= 1024;
if (memSize < 1024)
{
return memSize.ToString("0.00", CultureInfo.InvariantCulture) + " MB";
}
memSize /= 1024;
return memSize.ToString("0.00", CultureInfo.InvariantCulture) + " GB";
}
internal override void PushActivate()
{
base.PushActivate();
if (IsActive)
{
_dataManager.Start();
}
}
internal override void PopActivate()
{
base.PopActivate();
if (!IsActive)
{
_dataManager.Stop();
}
}
public void Dispose()
{
_dataManager.Dispose();
}
}
internal sealed partial class SystemNetworkUsageWidgetPage : WidgetPage, IDisposable
{
public override string Id => "com.microsoft.cmdpal.network_widget";
public override string Title => Resources.GetResource("Network_Usage_Title");
public override IconInfo Icon => Icons.NetworkIcon;
private readonly DataManager _dataManager;
private int _networkIndex;
public SystemNetworkUsageWidgetPage()
{
_dataManager = new(DataType.Network, () => UpdateWidget());
Commands = [
new CommandContextItem(new PrevNetworkCommand(this) { Name = Resources.GetResource("Previous_Network_Title") }),
new CommandContextItem(new NextNetworkCommand(this) { Name = Resources.GetResource("Next_Network_Title") }),
new CommandContextItem(OpenTaskManagerCommand.Instance),
];
}
protected override void LoadContentData()
{
// CoreLogger.LogDebug("Getting Network stats");
try
{
ContentData.Clear();
var timer = Stopwatch.StartNew();
var currentData = _dataManager.GetNetworkStats();
var dataDuration = timer.ElapsedMilliseconds;
var netName = currentData.GetNetworkName(_networkIndex);
var networkStats = currentData.GetNetworkUsage(_networkIndex);
ContentData["networkUsage"] = FloatToPercentString(networkStats.Usage);
ContentData["netSent"] = BytesToBitsPerSecString(networkStats.Sent);
ContentData["netReceived"] = BytesToBitsPerSecString(networkStats.Received);
ContentData["networkName"] = netName;
ContentData["netGraphUrl"] = currentData.CreateNetImageUrl(_networkIndex);
ContentData["chartHeight"] = ChartHelper.ChartHeight + "px";
ContentData["chartWidth"] = ChartHelper.ChartWidth + "px";
var contentDuration = timer.ElapsedMilliseconds - dataDuration;
// CoreLogger.LogDebug($"Network stats retrieved in {dataDuration} ms, content prepared in {contentDuration} ms. (Total {timer.ElapsedMilliseconds} ms)");
}
catch (Exception e)
{
ContentData.Clear();
ContentData["errorMessage"] = e.Message;
return;
}
}
protected override string GetTemplatePath(WidgetPageState page)
{
return page switch
{
WidgetPageState.Content => @"DevHome\Templates\SystemNetworkUsageTemplate.json",
WidgetPageState.Loading => @"DevHome\Templates\SystemNetworkUsageTemplate.json",
_ => throw new NotImplementedException(),
};
}
public string GetItemTitle(bool isBandPage)
{
if (ContentData.TryGetValue("networkName", out var name) && ContentData.TryGetValue("networkUsage", out var usage))
{
return isBandPage ? usage : string.Format(CultureInfo.CurrentCulture, Resources.GetResource("Network_Usage_Label"), name, usage);
}
else
{
return isBandPage ? Resources.GetResource("Network_Usage_Unknown") : Resources.GetResource("Network_Usage_Unknown_Label");
}
}
// up/down speed is always used for bands
public string GetUpSpeed()
{
if (ContentData.TryGetValue("netSent", out var upSpeed))
{
return upSpeed;
}
else
{
return "???";
}
}
public string GetDownSpeed()
{
if (ContentData.TryGetValue("netReceived", out var downSpeed))
{
return downSpeed;
}
else
{
return "???";
}
}
private string BytesToBitsPerSecString(float value)
{
// Bytes to bits
value *= 8;
// bits to Kbits
value /= 1024;
if (value < 1024)
{
if (value < 100)
{
return string.Format(CultureInfo.InvariantCulture, "{0:0.0} Kbps", value);
}
return string.Format(CultureInfo.InvariantCulture, "{0:0} Kbps", value);
}
// Kbits to Mbits
value /= 1024;
if (value < 1024)
{
if (value < 100)
{
return string.Format(CultureInfo.InvariantCulture, "{0:0.0} Mbps", value);
}
return string.Format(CultureInfo.InvariantCulture, "{0:0} Mbps", value);
}
// Mbits to Gbits
value /= 1024;
if (value < 100)
{
return string.Format(CultureInfo.InvariantCulture, "{0:0.0} Gbps", value);
}
return string.Format(CultureInfo.InvariantCulture, "{0:0} Gbps", value);
}
internal override void PushActivate()
{
base.PushActivate();
if (IsActive)
{
_dataManager.Start();
}
}
internal override void PopActivate()
{
base.PopActivate();
if (!IsActive)
{
_dataManager.Stop();
}
}
private void HandlePrevNetwork()
{
_networkIndex = _dataManager.GetNetworkStats().GetPrevNetworkIndex(_networkIndex);
UpdateWidget();
}
private void HandleNextNetwork()
{
_networkIndex = _dataManager.GetNetworkStats().GetNextNetworkIndex(_networkIndex);
UpdateWidget();
}
public void Dispose()
{
_dataManager.Dispose();
}
private sealed partial class PrevNetworkCommand : InvokableCommand
{
private readonly SystemNetworkUsageWidgetPage _page;
public PrevNetworkCommand(SystemNetworkUsageWidgetPage page)
{
_page = page;
}
public override string Id => "com.microsoft.cmdpal.network_widget.prev";
public override IconInfo Icon => Icons.NavigateBackwardIcon;
public override ICommandResult Invoke()
{
_page.HandlePrevNetwork();
return CommandResult.KeepOpen();
}
}
private sealed partial class NextNetworkCommand : InvokableCommand
{
private readonly SystemNetworkUsageWidgetPage _page;
public NextNetworkCommand(SystemNetworkUsageWidgetPage page)
{
_page = page;
}
public override string Id => "com.microsoft.cmdpal.network_widget.next";
public override IconInfo Icon => Icons.NavigateForwardIcon;
public override ICommandResult Invoke()
{
_page.HandleNextNetwork();
return CommandResult.KeepOpen();
}
}
}
internal sealed partial class SystemGPUUsageWidgetPage : WidgetPage, IDisposable
{
public override string Id => "com.microsoft.cmdpal.gpu_widget";
public override string Title => Resources.GetResource("GPU_Usage_Title");
public override IconInfo Icon => Icons.GpuIcon;
private readonly DataManager _dataManager;
private readonly string _gpuActiveEngType = "3D";
private int _gpuActiveIndex;
public SystemGPUUsageWidgetPage()
{
_dataManager = new(DataType.GPU, () => UpdateWidget());
Commands = [
new CommandContextItem(new PrevGPUCommand(this) { Name = Resources.GetResource("Previous_GPU_Title") }),
new CommandContextItem(new NextGPUCommand(this) { Name = Resources.GetResource("Next_GPU_Title") }),
new CommandContextItem(OpenTaskManagerCommand.Instance),
];
}
protected override void LoadContentData()
{
// CoreLogger.LogDebug("Getting GPU stats");
try
{
ContentData.Clear();
var timer = Stopwatch.StartNew();
var stats = _dataManager.GetGPUStats();
var dataDuration = timer.ElapsedMilliseconds;
var gpuName = stats.GetGPUName(_gpuActiveIndex);
ContentData["gpuUsage"] = FloatToPercentString(stats.GetGPUUsage(_gpuActiveIndex, _gpuActiveEngType));
ContentData["gpuName"] = gpuName;
ContentData["gpuTemp"] = stats.GetGPUTemperature(_gpuActiveIndex);
ContentData["gpuGraphUrl"] = stats.CreateGPUImageUrl(_gpuActiveIndex);
ContentData["chartHeight"] = ChartHelper.ChartHeight + "px";
ContentData["chartWidth"] = ChartHelper.ChartWidth + "px";
var contentDuration = timer.ElapsedMilliseconds - dataDuration;
// CoreLogger.LogDebug($"GPU stats retrieved in {dataDuration} ms, content prepared in {contentDuration} ms. (Total {timer.ElapsedMilliseconds} ms)");
}
catch (Exception e)
{
ContentData.Clear();
ContentData["errorMessage"] = e.Message;
return;
}
}
protected override string GetTemplatePath(WidgetPageState page)
{
return page switch
{
WidgetPageState.Content => @"DevHome\Templates\SystemGPUUsageTemplate.json",
WidgetPageState.Loading => @"DevHome\Templates\SystemGPUUsageTemplate.json",
_ => throw new NotImplementedException(),
};
}
public string GetItemTitle(bool isBandPage)
{
if (ContentData.TryGetValue("gpuName", out var name) && ContentData.TryGetValue("gpuUsage", out var usage))
{
return isBandPage ? usage : string.Format(CultureInfo.CurrentCulture, Resources.GetResource("GPU_Usage_Label"), name, usage);
}
else
{
return isBandPage ? Resources.GetResource("GPU_Usage_Unknown") : Resources.GetResource("GPU_Usage_Unknown_Label");
}
}
internal override void PushActivate()
{
base.PushActivate();
if (IsActive)
{
_dataManager.Start();
}
}
internal override void PopActivate()
{
base.PopActivate();
if (!IsActive)
{
_dataManager.Stop();
}
}
private void HandlePrevGPU()
{
_gpuActiveIndex = _dataManager.GetGPUStats().GetPrevGPUIndex(_gpuActiveIndex);
UpdateWidget();
}
private void HandleNextGPU()
{
_gpuActiveIndex = _dataManager.GetGPUStats().GetNextGPUIndex(_gpuActiveIndex);
UpdateWidget();
}
public void Dispose()
{
_dataManager.Dispose();
}
private sealed partial class PrevGPUCommand : InvokableCommand
{
private readonly SystemGPUUsageWidgetPage _page;
public PrevGPUCommand(SystemGPUUsageWidgetPage page)
{
_page = page;
}
public override string Id => "com.microsoft.cmdpal.gpu_widget.prev";
public override IconInfo Icon => Icons.NavigateBackwardIcon;
public override ICommandResult Invoke()
{
_page.HandlePrevGPU();
return CommandResult.KeepOpen();
}
}
private sealed partial class NextGPUCommand : InvokableCommand
{
private readonly SystemGPUUsageWidgetPage _page;
public NextGPUCommand(SystemGPUUsageWidgetPage page)
{
_page = page;
}
public override string Id => "com.microsoft.cmdpal.gpu_widget.next";
public override IconInfo Icon => Icons.NavigateForwardIcon;
public override ICommandResult Invoke()
{
_page.HandleNextGPU();
return CommandResult.KeepOpen();
}
}
}
internal sealed partial class OpenTaskManagerCommand : InvokableCommand
{
internal static readonly OpenTaskManagerCommand Instance = new();
public override string Id => "com.microsoft.cmdpal.open_task_manager";
public override IconInfo Icon => Icons.StackedAreaIcon; // StackedAreaIcon looks like task manager's icon
public override string Name => Resources.GetResource("Open_Task_Manager_Title");
public override ICommandResult Invoke()
{
try
{
Process.Start(new ProcessStartInfo
{
FileName = "taskmgr.exe",
UseShellExecute = true,
});
}
catch (Exception e)
{
CoreLogger.LogError("Error launching Task Manager.", e);
}
return CommandResult.Hide();
}
}

View File

@@ -0,0 +1,253 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Widget_Template.Loading" xml:space="preserve">
<value>Loading...</value>
<comment>Shown in Widget, when loading config file content</comment>
</data>
<data name="Widget_Template_Tooltip.Submit" xml:space="preserve">
<value>Submit</value>
<comment>Shown in Widget, Tooltip text</comment>
</data>
<data name="SSH_Widget_Template.Name" xml:space="preserve">
<value>SSH keychain</value>
</data>
<data name="SSH_Widget_Template.Target" xml:space="preserve">
<value>Local</value>
</data>
<data name="SSH_Widget_Template.ConfigFilePath" xml:space="preserve">
<value>Config file path</value>
</data>
<data name="SSH_Widget_Template.ConfigFileNotFound" xml:space="preserve">
<value>File not found</value>
</data>
<data name="SSH_Widget_Template.EmptyHosts" xml:space="preserve">
<value>There are no hosts in this config file.</value>
</data>
<data name="SSH_Widget_Template.NumOfHosts" xml:space="preserve">
<value>Number of hosts found</value>
</data>
<data name="SSH_Widget_Template.Connect" xml:space="preserve">
<value>Connect</value>
</data>
<data name="SSH_Widget_Template.ErrorProcessingConfigFile" xml:space="preserve">
<value>Processing config file failed</value>
</data>
<data name="Memory_Widget_Template.SystemMemory" xml:space="preserve">
<value>System memory</value>
</data>
<data name="Memory_Widget_Template.MemoryUsage" xml:space="preserve">
<value>Utilization</value>
</data>
<data name="Memory_Widget_Template.AllMemory" xml:space="preserve">
<value>All memory</value>
</data>
<data name="Memory_Widget_Template.UsedMemory" xml:space="preserve">
<value>In use (compressed)</value>
</data>
<data name="Memory_Widget_Template.Committed" xml:space="preserve">
<value>Committed</value>
</data>
<data name="Memory_Widget_Template.Cached" xml:space="preserve">
<value>Cached</value>
</data>
<data name="Memory_Widget_Template.NonPagedPool" xml:space="preserve">
<value>Non-paged pool</value>
</data>
<data name="Memory_Widget_Template.PagedPool" xml:space="preserve">
<value>Paged pool</value>
</data>
<data name="NetworkUsage_Widget_Template.Network_Usage" xml:space="preserve">
<value>Utilization</value>
</data>
<data name="NetworkUsage_Widget_Template.Sent" xml:space="preserve">
<value>Send</value>
</data>
<data name="NetworkUsage_Widget_Template.Received" xml:space="preserve">
<value>Receive</value>
</data>
<data name="NetworkUsage_Widget_Template.Network_Name" xml:space="preserve">
<value>Name</value>
</data>
<data name="Previous_Network_Title" xml:space="preserve">
<value>Previous network</value>
</data>
<data name="Next_Network_Title" xml:space="preserve">
<value>Next network</value>
</data>
<data name="NetworkUsage_Widget_Template.Ethernet_Heading" xml:space="preserve">
<value>Ethernet</value>
</data>
<data name="GPUUsage_Widget_Template.GPU_Usage" xml:space="preserve">
<value>Utilization</value>
</data>
<data name="GPUUsage_Widget_Template.GPU_Name" xml:space="preserve">
<value>Name</value>
</data>
<data name="GPUUsage_Widget_Template.GPU_Temperature" xml:space="preserve">
<value>Temperature</value>
</data>
<data name="Previous_GPU_Title" xml:space="preserve">
<value>Previous GPU</value>
</data>
<data name="Next_GPU_Title" xml:space="preserve">
<value>Next GPU</value>
</data>
<data name="CPUUsage_Widget_Template.CPU_Usage" xml:space="preserve">
<value>Utilization</value>
</data>
<data name="CPUUsage_Widget_Template.CPU_Speed" xml:space="preserve">
<value>Speed</value>
</data>
<data name="CPUUsage_Widget_Template.Processes" xml:space="preserve">
<value>Processes</value>
</data>
<data name="CPUUsage_Widget_Template.End_Process" xml:space="preserve">
<value>End process</value>
</data>
<data name="Widget_Template_Button.Preview" xml:space="preserve">
<value>Preview</value>
<comment>Shown in Widget, Button text</comment>
</data>
<data name="Widget_Template_Button.Save" xml:space="preserve">
<value>Save</value>
<comment>Shown in Widget, Button text</comment>
</data>
<data name="Widget_Template_Button.Cancel" xml:space="preserve">
<value>Cancel</value>
<comment>Shown in Widget, Button text</comment>
</data>
<data name="CPU_Usage_Subtitle" xml:space="preserve">
<value>CPU</value>
</data>
<data name="Memory_Usage_Subtitle" xml:space="preserve">
<value>Memory</value>
</data>
<data name="Network_Usage_Subtitle" xml:space="preserve">
<value>Network</value>
</data>
<data name="GPU_Usage_Subtitle" xml:space="preserve">
<value>GPU</value>
</data>
<data name="Performance_Monitor_Title" xml:space="preserve">
<value>Performance monitor</value>
</data>
<data name="CPU_Usage_Title" xml:space="preserve">
<value>CPU Usage</value>
</data>
<data name="CPU_Usage_Label" xml:space="preserve">
<value>CPU Usage: {0}</value>
<comment>{0} is the CPU usage percentage</comment>
</data>
<data name="CPU_Usage_Unknown" xml:space="preserve">
<value>???</value>
</data>
<data name="CPU_Usage_Unknown_Label" xml:space="preserve">
<value>CPU Usage: ???</value>
</data>
<data name="Memory_Usage_Title" xml:space="preserve">
<value>Memory Usage</value>
</data>
<data name="Memory_Usage_Label" xml:space="preserve">
<value>Memory Usage: {0}</value>
<comment>{0} is the memory usage percentage</comment>
</data>
<data name="Memory_Usage_Unknown" xml:space="preserve">
<value>???</value>
</data>
<data name="Memory_Usage_Unknown_Label" xml:space="preserve">
<value>Memory Usage: ???</value>
</data>
<data name="Network_Usage_Title" xml:space="preserve">
<value>Network Usage</value>
</data>
<data name="Network_Usage_Label" xml:space="preserve">
<value>Network ({0}): {1}</value>
<comment>{0} is the network adapter name, {1} is the usage percentage</comment>
</data>
<data name="Network_Usage_Unknown" xml:space="preserve">
<value>???</value>
</data>
<data name="Network_Usage_Unknown_Label" xml:space="preserve">
<value>Network Usage: ???</value>
</data>
<data name="GPU_Usage_Title" xml:space="preserve">
<value>GPU Usage</value>
</data>
<data name="GPU_Usage_Label" xml:space="preserve">
<value>GPU ({0}): {1}</value>
<comment>{0} is the GPU name, {1} is the usage percentage</comment>
</data>
<data name="GPU_Usage_Unknown" xml:space="preserve">
<value>???</value>
</data>
<data name="GPU_Usage_Unknown_Label" xml:space="preserve">
<value>GPU Usage: ???</value>
</data>
<data name="Open_Task_Manager_Title" xml:space="preserve">
<value>Open Task Manager</value>
</data>
<data name="Network_Send_Subtitle" xml:space="preserve">
<value>Send ↑</value>
</data>
<data name="Network_Receive_Subtitle" xml:space="preserve">
<value>Receive ↓</value>
</data>
</root>