mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-30 00:46:20 +01:00
Compare commits
20 Commits
jay/lsv2
...
user/yeela
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3bd9698073 | ||
|
|
cd5b76c988 | ||
|
|
f44addb29c | ||
|
|
1e3429dd3a | ||
|
|
075bbb46cb | ||
|
|
4aa27316fb | ||
|
|
f55c49e15b | ||
|
|
b06cd9f896 | ||
|
|
3e0d62d101 | ||
|
|
b89237ff94 | ||
|
|
df972447d4 | ||
|
|
668820cf2c | ||
|
|
ca4e8b2986 | ||
|
|
0472e7dc78 | ||
|
|
1f18088afe | ||
|
|
494901b52d | ||
|
|
3e213165a8 | ||
|
|
e04e6a11d1 | ||
|
|
14ff4dbc8c | ||
|
|
a8eb17d21a |
2
.github/actions/spell-check/allow/names.txt
vendored
2
.github/actions/spell-check/allow/names.txt
vendored
@@ -29,8 +29,6 @@ shortcutguide
|
||||
|
||||
# 8LWXpg is user name but user folder causes a flag
|
||||
LWXpg
|
||||
# 0x6f677548 is user name but user folder causes a flag
|
||||
x6f677548
|
||||
Adoumie
|
||||
Advaith
|
||||
alekhyareddy
|
||||
|
||||
4
.github/actions/spell-check/excludes.txt
vendored
4
.github/actions/spell-check/excludes.txt
vendored
@@ -121,6 +121,10 @@
|
||||
^src/modules/MouseWithoutBorders/App/Helper/.*\.resx$
|
||||
^src/modules/MouseWithoutBorders/ModuleInterface/generateSecurityDescriptor\.h$
|
||||
^src/modules/peek/Peek.Common/NativeMethods\.txt$
|
||||
^src/modules/peek/Peek.UITests/TestAssets/4\.qoi$
|
||||
^src/modules/powerrename/PowerRenameUITest/testItems/folder1/testCase2\.txt$
|
||||
^src/modules/powerrename/PowerRenameUITest/testItems/folder2/SpecialCase\.txt$
|
||||
^src/modules/powerrename/PowerRenameUITest/testItems/testCase1\.txt$
|
||||
^src/modules/previewpane/SvgPreviewHandler/SvgHTMLPreviewGenerator\.cs$
|
||||
^src/modules/previewpane/UnitTests-MarkdownPreviewHandler/HelperFiles/MarkdownWithHTMLImageTag\.txt$
|
||||
^src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/.*$
|
||||
|
||||
48
.github/actions/spell-check/expect.txt
vendored
48
.github/actions/spell-check/expect.txt
vendored
@@ -26,8 +26,6 @@ ADMINS
|
||||
adml
|
||||
admx
|
||||
advancedpaste
|
||||
advancedpasteui
|
||||
advancedpasteuishortcut
|
||||
advapi
|
||||
advfirewall
|
||||
AFeature
|
||||
@@ -45,7 +43,6 @@ ALLINPUT
|
||||
Allman
|
||||
Allmodule
|
||||
ALLOWUNDO
|
||||
allpc
|
||||
ALLVIEW
|
||||
ALPHATYPE
|
||||
AModifier
|
||||
@@ -136,7 +133,6 @@ bla
|
||||
BLACKFRAME
|
||||
BLENDFUNCTION
|
||||
Blockquotes
|
||||
blogs
|
||||
Blt
|
||||
BLURBEHIND
|
||||
BLURREGION
|
||||
@@ -512,7 +508,6 @@ FANCYZONESDRAWLAYOUTTEST
|
||||
FANCYZONESEDITOR
|
||||
FARPROC
|
||||
fesf
|
||||
fff
|
||||
FFFF
|
||||
FILEEXPLORER
|
||||
fileexploreraddons
|
||||
@@ -585,6 +580,7 @@ GETSCREENSAVERRUNNING
|
||||
GETSECKEY
|
||||
GETSTICKYKEYS
|
||||
GETTEXTLENGTH
|
||||
gitmodules
|
||||
GHND
|
||||
GMEM
|
||||
GNumber
|
||||
@@ -672,11 +668,7 @@ Hostx
|
||||
hotfixes
|
||||
hotkeycontrol
|
||||
HOTKEYF
|
||||
hotkeylockmachine
|
||||
hotkeyreconnect
|
||||
hotkeys
|
||||
hotkeyswitch
|
||||
hotkeytoggleeasymouse
|
||||
hotlight
|
||||
hotspot
|
||||
HPAINTBUFFER
|
||||
@@ -735,8 +727,6 @@ IMAGERESIZERCONTEXTMENU
|
||||
IMAGERESIZEREXT
|
||||
imageresizerinput
|
||||
imageresizersettings
|
||||
imagetotext
|
||||
imagetotextshortcut
|
||||
imagingdevices
|
||||
ime
|
||||
imgflip
|
||||
@@ -856,6 +846,7 @@ linkid
|
||||
LINKOVERLAY
|
||||
LINQTo
|
||||
listview
|
||||
LIVEDRAW
|
||||
LIVEZOOM
|
||||
LLKH
|
||||
llkhf
|
||||
@@ -867,7 +858,6 @@ localappdata
|
||||
localpackage
|
||||
LOCALSYSTEM
|
||||
LOCATIONCHANGE
|
||||
LOCKMACHINE
|
||||
LOCKTYPE
|
||||
LOGFONT
|
||||
LOGFONTW
|
||||
@@ -876,7 +866,6 @@ LOGMSG
|
||||
LOGPIXELSX
|
||||
LOGPIXELSY
|
||||
lng
|
||||
LOn
|
||||
lon
|
||||
longdate
|
||||
LONGNAMES
|
||||
@@ -928,12 +917,10 @@ luid
|
||||
LUMA
|
||||
lusrmgr
|
||||
LVal
|
||||
lvm
|
||||
LWA
|
||||
lwin
|
||||
LZero
|
||||
MAGTRANSFORM
|
||||
MAJORMINOR
|
||||
MAKEINTRESOURCE
|
||||
MAKEINTRESOURCEA
|
||||
MAKEINTRESOURCEW
|
||||
@@ -958,7 +945,6 @@ MDL
|
||||
mdtext
|
||||
mdtxt
|
||||
mdwn
|
||||
measuretool
|
||||
meme
|
||||
memicmp
|
||||
MENUITEMINFO
|
||||
@@ -1008,7 +994,6 @@ MOUSEHWHEEL
|
||||
MOUSEINPUT
|
||||
mousejump
|
||||
mousepointer
|
||||
mousepointercrosshairs
|
||||
mouseutils
|
||||
MOVESIZEEND
|
||||
MOVESIZESTART
|
||||
@@ -1053,7 +1038,6 @@ MWBEx
|
||||
MYICON
|
||||
NAMECHANGE
|
||||
namespaceanddescendants
|
||||
Namotion
|
||||
nao
|
||||
NCACTIVATE
|
||||
ncc
|
||||
@@ -1091,7 +1075,6 @@ NEWPLUSSHELLEXTENSIONWIN
|
||||
newrow
|
||||
nicksnettravels
|
||||
NIF
|
||||
NJson
|
||||
NLog
|
||||
NLSTEXT
|
||||
NMAKE
|
||||
@@ -1218,18 +1201,6 @@ PARENTRELATIVEFORUI
|
||||
PARENTRELATIVEPARSING
|
||||
parray
|
||||
PARTIALCONFIRMATIONDIALOGTITLE
|
||||
pasteashtmlfile
|
||||
pasteashtmlfileshortcut
|
||||
pasteasjson
|
||||
pasteasjsonshortcut
|
||||
pasteasmarkdown
|
||||
pasteasmarkdownshortcut
|
||||
pasteasplaintext
|
||||
pasteasplaintextshortcut
|
||||
pasteaspngfile
|
||||
pasteaspngfileshortcut
|
||||
pasteastxtfile
|
||||
pasteastxtfileshortcut
|
||||
PATCOPY
|
||||
PATHMUSTEXIST
|
||||
PATINVERT
|
||||
@@ -1237,6 +1208,7 @@ PATPAINT
|
||||
pbc
|
||||
pbi
|
||||
PBlob
|
||||
pbrush
|
||||
pcb
|
||||
pcch
|
||||
pcelt
|
||||
@@ -1299,7 +1271,6 @@ Pomodoro
|
||||
Popups
|
||||
POPUPWINDOW
|
||||
POSITIONITEM
|
||||
powerocr
|
||||
POWERRENAMECONTEXTMENU
|
||||
powerrenameinput
|
||||
POWERRENAMETEST
|
||||
@@ -1350,7 +1321,6 @@ PRODUCTVERSION
|
||||
Progman
|
||||
programdata
|
||||
projectname
|
||||
projitems
|
||||
PROPERTYKEY
|
||||
Propset
|
||||
PROPVARIANT
|
||||
@@ -1358,6 +1328,7 @@ PRTL
|
||||
prvpane
|
||||
psapi
|
||||
pscid
|
||||
pscustomobject
|
||||
PSECURITY
|
||||
psfgao
|
||||
psfi
|
||||
@@ -1443,7 +1414,6 @@ Removelnk
|
||||
renamable
|
||||
RENAMEONCOLLISION
|
||||
reparented
|
||||
reparenthotkey
|
||||
reparenting
|
||||
reportfileaccesses
|
||||
requery
|
||||
@@ -1469,7 +1439,6 @@ RIDEV
|
||||
RIGHTSCROLLBAR
|
||||
riid
|
||||
RKey
|
||||
Rns
|
||||
RNumber
|
||||
rop
|
||||
ROUNDSMALL
|
||||
@@ -1693,7 +1662,6 @@ STYLECHANGED
|
||||
STYLECHANGING
|
||||
subkeys
|
||||
sublang
|
||||
Subdomain
|
||||
SUBMODULEUPDATE
|
||||
subresource
|
||||
Superbar
|
||||
@@ -1766,7 +1734,6 @@ THICKFRAME
|
||||
THEMECHANGED
|
||||
THISCOMPONENT
|
||||
throughs
|
||||
thumbnailhotkey
|
||||
TILEDWINDOW
|
||||
TILLSON
|
||||
timedate
|
||||
@@ -1782,9 +1749,7 @@ tlbimp
|
||||
tlc
|
||||
tmain
|
||||
TNP
|
||||
TOGGLEEASYMOUSE
|
||||
Toolhelp
|
||||
toolkitconverters
|
||||
toolwindow
|
||||
TOPDOWNDIB
|
||||
TOUCHEVENTF
|
||||
@@ -1796,11 +1761,9 @@ tracelogging
|
||||
tracerpt
|
||||
trackbar
|
||||
trafficmanager
|
||||
transcodetomp
|
||||
transicc
|
||||
TRAYMOUSEMESSAGE
|
||||
triaging
|
||||
Tru
|
||||
trl
|
||||
trx
|
||||
tsa
|
||||
@@ -1836,7 +1799,6 @@ ULONGLONG
|
||||
ums
|
||||
uncompilable
|
||||
UNCPRIORITY
|
||||
undefining
|
||||
UNDNAME
|
||||
UNICODETEXT
|
||||
unins
|
||||
@@ -2003,6 +1965,7 @@ WMI
|
||||
WMICIM
|
||||
wmimgmt
|
||||
wmp
|
||||
wmsg
|
||||
WMSYSCOMMAND
|
||||
wnd
|
||||
WNDCLASS
|
||||
@@ -2016,6 +1979,7 @@ WORKSPACESEDITOR
|
||||
WORKSPACESLAUNCHER
|
||||
WORKSPACESSNAPSHOTTOOL
|
||||
WORKSPACESWINDOWARRANGER
|
||||
Worktree
|
||||
wox
|
||||
wparam
|
||||
wpf
|
||||
|
||||
9
.github/actions/spell-check/patterns.txt
vendored
9
.github/actions/spell-check/patterns.txt
vendored
@@ -1,5 +1,10 @@
|
||||
# See https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples:-patterns
|
||||
|
||||
# marker to ignore all code on line
|
||||
^.*/\* #no-spell-check-line \*/.*$
|
||||
# marker for ignoring a comment to the end of the line
|
||||
// #no-spell-check.*$
|
||||
|
||||
# Gaelic
|
||||
Gàidhlig
|
||||
|
||||
@@ -264,3 +269,7 @@ St&yle
|
||||
# This matches a relative clause where the relative pronoun "that" is omitted.
|
||||
# Example: "Gets or sets the window the TitleBar should configure."
|
||||
\bthe\s+\w+\s+the\b
|
||||
|
||||
# Usernames with numbers
|
||||
# 0x6f677548 is user name but user folder causes a flag
|
||||
\bx6f677548\b
|
||||
|
||||
@@ -258,6 +258,7 @@ jobs:
|
||||
-restore -graph
|
||||
/p:RestorePackagesConfig=true
|
||||
/p:CIBuild=true
|
||||
/p:BuildInParallel=true
|
||||
/bl:$(LogOutputDirectory)\build-0-main.binlog
|
||||
${{ parameters.additionalBuildOptions }}
|
||||
$(MSBuildCacheParameters)
|
||||
@@ -277,7 +278,7 @@ jobs:
|
||||
condition: ne(variables['BuildPlatform'], 'x64')
|
||||
inputs:
|
||||
solution: src/dsc/v3/PowerToys.DSC/PowerToys.DSC.csproj
|
||||
msbuildArgs: /t:Build /m /restore
|
||||
msbuildArgs: /t:Build /m /restore /p:BuildInParallel=true
|
||||
platform: x64
|
||||
configuration: $(BuildConfiguration)
|
||||
msbuildArchitecture: x64
|
||||
@@ -323,6 +324,7 @@ jobs:
|
||||
-restore -graph
|
||||
/p:RestorePackagesConfig=true
|
||||
/p:CIBuild=true
|
||||
/p:BuildInParallel=true
|
||||
/bl:$(LogOutputDirectory)\build-bug-report.binlog
|
||||
${{ parameters.additionalBuildOptions }}
|
||||
$(MSBuildCacheParameters)
|
||||
@@ -344,6 +346,7 @@ jobs:
|
||||
-restore -graph
|
||||
/p:RestorePackagesConfig=true
|
||||
/p:CIBuild=true
|
||||
/p:BuildInParallel=true
|
||||
/bl:$(LogOutputDirectory)\build-styles-report.binlog
|
||||
${{ parameters.additionalBuildOptions }}
|
||||
$(MSBuildCacheParameters)
|
||||
@@ -365,7 +368,7 @@ jobs:
|
||||
msbuildArgs: >-
|
||||
/target:Publish
|
||||
/graph
|
||||
/p:Configuration=$(BuildConfiguration);Platform=$(BuildPlatform);AppxBundle=Never
|
||||
/p:Configuration=$(BuildConfiguration);Platform=$(BuildPlatform);AppxBundle=Never;BuildInParallel=true
|
||||
/p:VCRTForwarders-IncludeDebugCRT=false
|
||||
/p:PowerToysRoot=$(Build.SourcesDirectory)
|
||||
/p:PublishProfile=InstallationPublishProfile.pubxml
|
||||
|
||||
@@ -90,6 +90,7 @@ jobs:
|
||||
/p:RestorePackagesConfig=true
|
||||
/p:BuildProjectReferences=true
|
||||
/p:CIBuild=true
|
||||
/p:BuildInParallel=true
|
||||
/bl:$(LogOutputDirectory)\build-all-uitests.binlog
|
||||
$(NUGET_RESTORE_MSBUILD_ARGS)
|
||||
platform: $(BuildPlatform)
|
||||
@@ -111,6 +112,7 @@ jobs:
|
||||
/p:RestorePackagesConfig=true
|
||||
/p:BuildProjectReferences=true
|
||||
/p:CIBuild=true
|
||||
/p:BuildInParallel=true
|
||||
/bl:$(LogOutputDirectory)\build-${{ module }}.binlog
|
||||
$(NUGET_RESTORE_MSBUILD_ARGS)
|
||||
platform: $(BuildPlatform)
|
||||
|
||||
@@ -825,6 +825,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LightSwitch.UITests", "src\
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests\Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests.csproj", "{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.UI.ViewModels.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.UI.ViewModels.UnitTests\Microsoft.CmdPal.UI.ViewModels.UnitTests.csproj", "{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|ARM64 = Debug|ARM64
|
||||
@@ -2995,6 +2997,14 @@ Global
|
||||
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Release|x64.ActiveCfg = Release|x64
|
||||
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Release|x64.Build.0 = Release|x64
|
||||
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Debug|x64.Build.0 = Debug|x64
|
||||
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Release|x64.ActiveCfg = Release|x64
|
||||
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Release|x64.Build.0 = Release|x64
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -3323,6 +3333,7 @@ Global
|
||||
{3DCCD936-D085-4869-A1DE-CA6A64152C94} = {5B201255-53C8-490B-A34F-01F05D48A477}
|
||||
{F5333ED7-06D8-4AB3-953A-36D63F08CB6F} = {3DCCD936-D085-4869-A1DE-CA6A64152C94}
|
||||
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
|
||||
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}
|
||||
|
||||
@@ -1,32 +1,36 @@
|
||||
#include <windows.h>
|
||||
#include "resource.h"
|
||||
#include "../../../common/version/version.h"
|
||||
|
||||
1 VERSIONINFO
|
||||
FILEVERSION 0,1,0,0
|
||||
PRODUCTVERSION 0,1,0,0
|
||||
FILEFLAGSMASK 0x3fL
|
||||
FILEVERSION FILE_VERSION
|
||||
PRODUCTVERSION PRODUCT_VERSION
|
||||
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS 0x1L
|
||||
FILEFLAGS VS_FF_DEBUG
|
||||
#else
|
||||
FILEFLAGS 0x0L
|
||||
FILEFLAGS 0x0L
|
||||
#endif
|
||||
FILEOS 0x40004L
|
||||
FILETYPE 0x2L
|
||||
FILESUBTYPE 0x0L
|
||||
FILEOS VOS_NT_WINDOWS32
|
||||
FILETYPE VFT_DLL
|
||||
FILESUBTYPE VFT2_UNKNOWN
|
||||
BEGIN
|
||||
BLOCK "StringFileInfo"
|
||||
BEGIN
|
||||
BLOCK "040904b0"
|
||||
BLOCK "040904b0" // US English (0x0409), Unicode (0x04B0) charset
|
||||
BEGIN
|
||||
VALUE "CompanyName", "Company Name"
|
||||
VALUE "FileDescription", "Light Switch Module"
|
||||
VALUE "FileVersion", "0.1.0.0"
|
||||
VALUE "InternalName", "Light Switch"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2019 Company Name"
|
||||
VALUE "OriginalFilename", "PowerToys.LightSwitchModuleInterface.dll"
|
||||
VALUE "ProductName", "Light Switch"
|
||||
VALUE "ProductVersion", "0.1.0.0"
|
||||
VALUE "CompanyName", COMPANY_NAME
|
||||
VALUE "FileDescription", FILE_DESCRIPTION
|
||||
VALUE "FileVersion", FILE_VERSION_STRING
|
||||
VALUE "InternalName", INTERNAL_NAME
|
||||
VALUE "LegalCopyright", COPYRIGHT_NOTE
|
||||
VALUE "OriginalFilename", ORIGINAL_FILENAME
|
||||
VALUE "ProductName", PRODUCT_NAME
|
||||
VALUE "ProductVersion", PRODUCT_VERSION_STRING
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
BEGIN
|
||||
VALUE "Translation", 0x409, 1200
|
||||
VALUE "Translation", 0x409, 1200 // US English (0x0409), Unicode (1200) charset
|
||||
END
|
||||
END
|
||||
@@ -108,7 +108,7 @@ public:
|
||||
|
||||
m_force_light_event_handle = CreateDefaultEvent(L"POWERTOYS_LIGHTSWITCH_FORCE_LIGHT");
|
||||
m_force_dark_event_handle = CreateDefaultEvent(L"POWERTOYS_LIGHTSWITCH_FORCE_DARK");
|
||||
m_manual_override_event_handle = CreateDefaultEvent(L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE");
|
||||
m_manual_override_event_handle = CreateEventW(nullptr, TRUE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE");
|
||||
|
||||
init_settings();
|
||||
};
|
||||
@@ -460,7 +460,7 @@ public:
|
||||
}
|
||||
else if (hotkeyId == 0)
|
||||
{
|
||||
// get current will return true if in light mode, otherwise false
|
||||
// get current will return true if in light mode; otherwise false
|
||||
Logger::info(L"[Light Switch] Hotkey triggered: Toggle Theme");
|
||||
if (g_settings.m_changeSystem)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
//{{NO_DEPENDENCIES}}
|
||||
// Microsoft Visual C++ generated include file.
|
||||
// Used by CalculatorEngineCommon.rc
|
||||
|
||||
//////////////////////////////
|
||||
// Non-localizable
|
||||
|
||||
#define FILE_DESCRIPTION "Light Switch Module"
|
||||
#define INTERNAL_NAME "Light Switch"
|
||||
#define ORIGINAL_FILENAME "PowerToys.LightSwitchModuleInterface.dll"
|
||||
|
||||
// Non-localizable
|
||||
//////////////////////////////
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
#include <string>
|
||||
#include <LightSwitchSettings.h>
|
||||
#include <common/utils/gpo.h>
|
||||
#include <logger/logger_settings.h>
|
||||
#include <logger/logger.h>
|
||||
#include <utils/logger_helper.h>
|
||||
|
||||
SERVICE_STATUS g_ServiceStatus = {};
|
||||
SERVICE_STATUS_HANDLE g_StatusHandle = nullptr;
|
||||
@@ -35,6 +38,8 @@ int _tmain(int argc, TCHAR* argv[])
|
||||
wchar_t serviceName[] = L"LightSwitchService";
|
||||
SERVICE_TABLE_ENTRYW table[] = { { serviceName, ServiceMain }, { nullptr, nullptr } };
|
||||
|
||||
LoggerHelpers::init_logger(L"LightSwitch", L"Service", LogSettings::lightSwitchLoggerName);
|
||||
|
||||
if (!StartServiceCtrlDispatcherW(table))
|
||||
{
|
||||
DWORD err = GetLastError();
|
||||
@@ -106,6 +111,7 @@ VOID WINAPI ServiceCtrlHandler(DWORD dwCtrl)
|
||||
SetServiceStatus(g_StatusHandle, &g_ServiceStatus);
|
||||
|
||||
// Signal the service to stop
|
||||
Logger::info(L"[LightSwitchService] Stop requested, signaling worker thread to exit.");
|
||||
SetEvent(g_ServiceStopEvent);
|
||||
break;
|
||||
|
||||
@@ -126,13 +132,21 @@ static void update_sun_times(auto& settings)
|
||||
|
||||
int newLightTime = newTimes.sunriseHour * 60 + newTimes.sunriseMinute;
|
||||
int newDarkTime = newTimes.sunsetHour * 60 + newTimes.sunsetMinute;
|
||||
try
|
||||
{
|
||||
auto values = PowerToysSettings::PowerToyValues::load_from_settings_file(L"LightSwitch");
|
||||
values.add_property(L"lightTime", newLightTime);
|
||||
values.add_property(L"darkTime", newDarkTime);
|
||||
values.save_to_settings_file();
|
||||
|
||||
auto values = PowerToysSettings::PowerToyValues::load_from_settings_file(L"LightSwitch");
|
||||
values.add_property(L"lightTime", newLightTime);
|
||||
values.add_property(L"darkTime", newDarkTime);
|
||||
values.save_to_settings_file();
|
||||
|
||||
OutputDebugString(L"[LightSwitchService] Updated sun times and saved to config.\n");
|
||||
Logger::info(L"[LightSwitchService] Updated sun times and saved to config.");
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
std::wstring wmsg(e.what(), e.what() + strlen(e.what()));
|
||||
Logger::error(L"[LightSwitchService] Exception during sun time update: {}", wmsg);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
|
||||
@@ -142,7 +156,8 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
|
||||
if (parentPid)
|
||||
hParent = OpenProcess(SYNCHRONIZE, FALSE, parentPid);
|
||||
|
||||
OutputDebugString(L"[LightSwitchService] Worker thread starting...\n");
|
||||
Logger::info(L"[LightSwitchService] Worker thread starting...");
|
||||
Logger::info(L"[LightSwitchService] Parent PID: {}", parentPid);
|
||||
|
||||
// Initialize settings system
|
||||
LightSwitchSettings::instance().InitFileWatcher();
|
||||
@@ -214,19 +229,19 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
|
||||
update_sun_times(settings);
|
||||
g_lastUpdatedDay = st.wDay;
|
||||
|
||||
OutputDebugString(L"[LightSwitchService] Recalculated sun times at new day boundary.\n");
|
||||
Logger::info(L"[LightSwitchService] Recalculated sun times at new day boundary.");
|
||||
}
|
||||
|
||||
wchar_t msg[160];
|
||||
swprintf_s(msg,
|
||||
L"[LightSwitchService] now=%02d:%02d | light=%02d:%02d | dark=%02d:%02d\n",
|
||||
L"[LightSwitchService] now=%02d:%02d | light=%02d:%02d | dark=%02d:%02d",
|
||||
st.wHour,
|
||||
st.wMinute,
|
||||
settings.lightTime / 60,
|
||||
settings.lightTime % 60,
|
||||
settings.darkTime / 60,
|
||||
settings.darkTime % 60);
|
||||
OutputDebugString(msg);
|
||||
Logger::info(msg);
|
||||
|
||||
// --- Manual override check ---
|
||||
bool manualOverrideActive = false;
|
||||
@@ -242,11 +257,11 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
|
||||
nowMinutes == (settings.darkTime + settings.sunset_offset) % 1440)
|
||||
{
|
||||
ResetEvent(hManualOverride);
|
||||
OutputDebugString(L"[LightSwitchService] Manual override cleared at boundary\n");
|
||||
Logger::info(L"[LightSwitchService] Manual override cleared at boundary\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
OutputDebugString(L"[LightSwitchService] Skipping schedule due to manual override\n");
|
||||
Logger::info(L"[LightSwitchService] Skipping schedule due to manual override\n");
|
||||
goto sleep_until_next_minute;
|
||||
}
|
||||
}
|
||||
@@ -261,10 +276,17 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
|
||||
msToNextMinute = 50;
|
||||
|
||||
DWORD wait = WaitForMultipleObjects(count, waits, FALSE, msToNextMinute);
|
||||
if (wait == WAIT_OBJECT_0) // stop event
|
||||
if (wait == WAIT_OBJECT_0)
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Stop event triggered <20> exiting worker loop.");
|
||||
break;
|
||||
if (hParent && wait == WAIT_OBJECT_0 + 1) // parent exited
|
||||
}
|
||||
if (hParent && wait == WAIT_OBJECT_0 + 1) // parent process exited
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Parent process exited <20> stopping service.");
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (hManualOverride)
|
||||
@@ -282,8 +304,8 @@ int APIENTRY wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
|
||||
wchar_t msg[160];
|
||||
swprintf_s(
|
||||
msg,
|
||||
L"Tried to start with a GPO policy setting the utility to always be disabled. Please contact your systems administrator.\n");
|
||||
OutputDebugString(msg);
|
||||
L"Tried to start with a GPO policy setting the utility to always be disabled. Please contact your systems administrator.");
|
||||
Logger::info(msg);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
@@ -28,19 +28,6 @@
|
||||
<ProjectName>LightSwitchService</ProjectName>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
@@ -54,84 +41,25 @@
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Label="ExtensionSettings">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="Shared">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<PropertyGroup>
|
||||
<OutDir>..\..\..\..\$(Platform)\$(Configuration)\$(MSBuildProjectName)\</OutDir>
|
||||
<TargetName>PowerToys.LightSwitchService</TargetName>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<PrecompiledHeader>NotUsing</PrecompiledHeader>
|
||||
<PreprocessorDefinitions>%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<AdditionalIncludeDirectories>
|
||||
./../;
|
||||
..\..\..\common\Telemetry;
|
||||
..\..\..\common;
|
||||
..\..\..\common\logger;
|
||||
..\..\..\common\utils;
|
||||
..\..\..\common\SettingsAPI;
|
||||
..\..\..\common\Telemetry;
|
||||
..\..\..\;
|
||||
..\..\..\..\deps\spdlog\include;
|
||||
./;
|
||||
@@ -145,8 +73,27 @@
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\common\logger\logger.vcxproj">
|
||||
<Project>{d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}</Project>
|
||||
<ClCompile Include="LightSwitchService.cpp" />
|
||||
<ClCompile Include="LightSwitchSettings.cpp" />
|
||||
<ClCompile Include="SettingsConstants.cpp" />
|
||||
<ClCompile Include="ThemeHelper.cpp" />
|
||||
<ClCompile Include="ThemeScheduler.cpp" />
|
||||
<ClCompile Include="WinHookEventIDs.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="LightSwitchService.rc" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="LightSwitchSettings.h" />
|
||||
<ClInclude Include="SettingsConstants.h" />
|
||||
<ClInclude Include="SettingsObserver.h" />
|
||||
<ClInclude Include="ThemeHelper.h" />
|
||||
<ClInclude Include="ThemeScheduler.h" />
|
||||
<ClInclude Include="WinHookEventIDs.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj">
|
||||
<Project>{4aed67b6-55fd-486f-b917-e543dee2cb3c}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\..\common\SettingsAPI\SettingsAPI.vcxproj">
|
||||
<Project>{6955446d-23f7-4023-9bb3-8657f904af99}</Project>
|
||||
@@ -158,62 +105,10 @@
|
||||
<Project>{8f021b46-362b-485c-bfba-ccf83e820cbd}</Project>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="LightSwitchService.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="LightSwitchSettings.cpp" />
|
||||
<ClCompile Include="SettingsConstants.cpp" />
|
||||
<ClCompile Include="ThemeHelper.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="ThemeScheduler.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="WinHookEventIDs.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="LightSwitchService.rc" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="LightSwitchSettings.h" />
|
||||
<ClInclude Include="SettingsConstants.h" />
|
||||
<ClInclude Include="SettingsObserver.h" />
|
||||
<ClInclude Include="ThemeHelper.h" />
|
||||
<ClInclude Include="ThemeScheduler.h">
|
||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">false</ExcludedFromBuild>
|
||||
</ClInclude>
|
||||
<ClInclude Include="WinHookEventIDs.h" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<Import Project="..\..\..\..\deps\spdlog.props" />
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />
|
||||
</ImportGroup>
|
||||
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
||||
<PropertyGroup>
|
||||
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
|
||||
</PropertyGroup>
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets'))" />
|
||||
</Target>
|
||||
</Project>
|
||||
@@ -24,15 +24,6 @@
|
||||
<ClCompile Include="ThemeHelper.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\..\..\common\SettingsAPI\settings_helpers.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\..\..\common\SettingsAPI\settings_objects.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\..\..\common\SettingsAPI\FileWatcher.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="LightSwitchSettings.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
@@ -43,9 +34,6 @@
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="ThemeScheduler.h">
|
||||
<Filter>Header Files</Filter>
|
||||
@@ -69,4 +57,9 @@
|
||||
<ItemGroup>
|
||||
<Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natvis" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="LightSwitchService.rc">
|
||||
<Filter>Resource Files</Filter>
|
||||
</ResourceCompile>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -12,8 +12,8 @@ enum struct FindMyMouseActivationMethod : int
|
||||
|
||||
constexpr bool FIND_MY_MOUSE_DEFAULT_DO_NOT_ACTIVATE_ON_GAME_MODE = true;
|
||||
// Default colors now include full alpha. Opacity is encoded directly in color alpha (legacy overlay_opacity migrated into A channel)
|
||||
const winrt::Windows::UI::Color FIND_MY_MOUSE_DEFAULT_BACKGROUND_COLOR = winrt::Windows::UI::ColorHelper::FromArgb(255, 0, 0, 0);
|
||||
const winrt::Windows::UI::Color FIND_MY_MOUSE_DEFAULT_SPOTLIGHT_COLOR = winrt::Windows::UI::ColorHelper::FromArgb(255, 255, 255, 255);
|
||||
const winrt::Windows::UI::Color FIND_MY_MOUSE_DEFAULT_BACKGROUND_COLOR = winrt::Windows::UI::ColorHelper::FromArgb(128, 0, 0, 0);
|
||||
const winrt::Windows::UI::Color FIND_MY_MOUSE_DEFAULT_SPOTLIGHT_COLOR = winrt::Windows::UI::ColorHelper::FromArgb(128, 255, 255, 255);
|
||||
constexpr int FIND_MY_MOUSE_DEFAULT_SPOTLIGHT_RADIUS = 100;
|
||||
constexpr int FIND_MY_MOUSE_DEFAULT_ANIMATION_DURATION_MS = 500;
|
||||
constexpr int FIND_MY_MOUSE_DEFAULT_SPOTLIGHT_INITIAL_ZOOM = 9;
|
||||
@@ -43,4 +43,4 @@ int FindMyMouseMain(HINSTANCE hinst, const FindMyMouseSettings& settings);
|
||||
void FindMyMouseDisable();
|
||||
bool FindMyMouseIsEnabled();
|
||||
void FindMyMouseApplySettings(const FindMyMouseSettings& settings);
|
||||
HWND GetSonarHwnd() noexcept;
|
||||
HWND GetSonarHwnd() noexcept;
|
||||
|
||||
@@ -1055,8 +1055,13 @@ namespace MouseWithoutBorders.Class
|
||||
|
||||
if (machineId == 0)
|
||||
{
|
||||
_properties.MachineID.Value = Common.Ran.Next();
|
||||
machineId = _properties.MachineID.Value;
|
||||
var newMachineId = Common.Ran.Next();
|
||||
_properties.MachineID.Value = newMachineId;
|
||||
machineId = newMachineId;
|
||||
if (!PauseInstantSaving)
|
||||
{
|
||||
SaveSettings();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1068,6 +1073,11 @@ namespace MouseWithoutBorders.Class
|
||||
lock (_loadingSettingsLock)
|
||||
{
|
||||
_properties.MachineID.Value = value;
|
||||
machineId = value;
|
||||
if (!PauseInstantSaving)
|
||||
{
|
||||
SaveSettings();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,7 +96,10 @@ typedef struct {
|
||||
#define SHALLOW_DESTROY 2
|
||||
#define LIVE_DRAW_ZOOM 3
|
||||
|
||||
#define PEN_COLOR_HIGHLIGHT(Pencolor) (Pencolor >> 24) != 0xFF
|
||||
#define PEN_COLOR_HIGHLIGHT(Pencolor) ((Pencolor >> 24) != 0xFF)
|
||||
#define PEN_COLOR_BLUR(Pencolor) ((Pencolor & 0x00FFFFFF) == COLOR_BLUR)
|
||||
|
||||
#define CURSOR_SAVE_MARGIN 4
|
||||
|
||||
|
||||
typedef BOOL (__stdcall *type_pGetMonitorInfo)(
|
||||
@@ -143,7 +146,14 @@ typedef BOOL(__stdcall *type_pMagSetWindowFilterList)(
|
||||
int count,
|
||||
HWND* pHWND
|
||||
);
|
||||
typedef BOOL (__stdcall *type_pMagInitialize)(VOID);
|
||||
typedef BOOL(__stdcall* type_pMagSetLensUseBitmapSmoothing)(
|
||||
_In_ HWND,
|
||||
_In_ BOOL
|
||||
);
|
||||
typedef BOOL(__stdcall* type_MagSetFullscreenUseBitmapSmoothing)(
|
||||
BOOL fUseBitmapSmoothing
|
||||
);
|
||||
typedef BOOL(__stdcall* type_pMagInitialize)(VOID);
|
||||
|
||||
typedef BOOL(__stdcall *type_pGetPointerType)(
|
||||
_In_ UINT32 pointerId,
|
||||
|
||||
@@ -121,8 +121,8 @@ FONT 8, "MS Shell Dlg", 0, 0, 0x0
|
||||
BEGIN
|
||||
DEFPUSHBUTTON "OK",IDOK,166,306,50,14
|
||||
PUSHBUTTON "Cancel",IDCANCEL,223,306,50,14
|
||||
LTEXT "ZoomIt v9.01",IDC_VERSION,42,7,73,10
|
||||
LTEXT "Copyright © 2006-2025 Mark Russinovich",IDC_COPYRIGHT,42,17,166,8
|
||||
LTEXT "ZoomIt v9.10",IDC_VERSION,42,7,73,10
|
||||
LTEXT "Copyright © 2006-2025 Mark Russinovich",IDC_COPYRIGHT,42,17,231,8
|
||||
CONTROL "<a HREF=""https://www.sysinternals.com"">Sysinternals - www.sysinternals.com</a>",IDC_LINK,
|
||||
"SysLink",WS_TABSTOP,42,26,150,9
|
||||
ICON "APPICON",IDC_STATIC,12,9,20,20
|
||||
@@ -149,7 +149,8 @@ BEGIN
|
||||
CONTROL "",IDC_TIMER_POS7,"Button",BS_AUTORADIOBUTTON,63,108,10,10
|
||||
CONTROL "",IDC_TIMER_POS8,"Button",BS_AUTORADIOBUTTON,79,108,10,10
|
||||
CONTROL "",IDC_TIMER_POS9,"Button",BS_AUTORADIOBUTTON,97,108,10,10
|
||||
CONTROL "Show background bitmap:",IDC_CHECK_BACKGROUND_FILE,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,3,122,99,10,WS_EX_RIGHT
|
||||
CONTROL "Show background bitmap:",IDC_CHECK_BACKGROUND_FILE,
|
||||
"Button",BS_AUTOCHECKBOX | WS_TABSTOP,3,122,99,10,WS_EX_RIGHT
|
||||
CONTROL "Use faded desktop as background",IDC_STATIC_DESKTOP_BACKGROUND,
|
||||
"Button",BS_AUTORADIOBUTTON | WS_GROUP | WS_TABSTOP,46,135,125,10
|
||||
CONTROL "Use image file as background",IDC_STATIC_BACKGROUND_FILE,
|
||||
@@ -165,23 +166,25 @@ BEGIN
|
||||
CONTROL "",IDC_STATIC,"Static",SS_BLACKFRAME | SS_SUNKEN,7,196,193,1,WS_EX_CLIENTEDGE
|
||||
END
|
||||
|
||||
ZOOM DIALOGEX 0, 0, 260, 158
|
||||
ZOOM DIALOGEX 0, 0, 260, 170
|
||||
STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_CLIPSIBLINGS | WS_SYSMENU
|
||||
FONT 8, "MS Shell Dlg", 400, 0, 0x1
|
||||
BEGIN
|
||||
CONTROL "",IDC_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,59,57,80,12
|
||||
LTEXT "After toggling ZoomIt you can zoom in with the mouse wheel or up and down arrow keys. Exit zoom mode with Escape or by pressing the right mouse button.",IDC_STATIC,7,6,246,26
|
||||
LTEXT "Zoom Toggle:",IDC_STATIC,7,59,51,8
|
||||
CONTROL "",IDC_ZOOM_SLIDER,"msctls_trackbar32",TBS_AUTOTICKS | TBS_BOTH | TBS_NOTICKS | WS_TABSTOP,53,104,150,15,WS_EX_TRANSPARENT
|
||||
LTEXT "Specify the initial level of magnification when zooming in:",IDC_STATIC,7,91,215,10
|
||||
LTEXT "1.25",IDC_STATIC,52,122,16,8
|
||||
LTEXT "1.5",IDC_STATIC,82,122,12,8
|
||||
LTEXT "1.75",IDC_STATIC,108,122,16,8
|
||||
LTEXT "2.0",IDC_STATIC,138,122,12,8
|
||||
LTEXT "3.0",IDC_STATIC,164,122,12,8
|
||||
LTEXT "4.0",IDC_STATIC,190,122,12,8
|
||||
CONTROL "",IDC_ZOOM_SLIDER,"msctls_trackbar32",TBS_AUTOTICKS | TBS_BOTH | TBS_NOTICKS | WS_TABSTOP,53,118,150,15,WS_EX_TRANSPARENT
|
||||
LTEXT "Specify the initial level of magnification when zooming in:",IDC_STATIC,7,105,215,10
|
||||
LTEXT "1.25",IDC_STATIC,52,136,16,8
|
||||
LTEXT "1.5",IDC_STATIC,82,136,12,8
|
||||
LTEXT "1.75",IDC_STATIC,108,136,16,8
|
||||
LTEXT "2.0",IDC_STATIC,138,136,12,8
|
||||
LTEXT "3.0",IDC_STATIC,164,136,12,8
|
||||
LTEXT "4.0",IDC_STATIC,190,136,12,8
|
||||
CONTROL "Animate zoom in and zoom out:",IDC_ANIMATE_ZOOM,"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,7,74,116,10
|
||||
LTEXT "Copy a zoomed screen with Ctrl+C or save it by typing Ctrl+S. Crop the copy or save region by entering Ctrl+Shift instead of Ctrl.",IDC_STATIC,7,34,246,17
|
||||
CONTROL "Smooth zoomed image:",IDC_SMOOTH_IMAGE,"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,7,88,116,10
|
||||
LTEXT "Copy a zoomed screen with Ctrl+C or save it by typing Ctrl+S. Crop the copy or save region by entering Ctrl+Shift instead of Ctrl.",IDC_STATIC,7,148,246,17
|
||||
LTEXT "Copy a zoomed screen with Ctrl+C or save it by typing Ctrl+S. Crop the copy or save region by entering Ctrl+Shift instead of Ctrl.",IDC_STATIC,6,34,246,18
|
||||
END
|
||||
|
||||
DRAW DIALOGEX 0, 0, 260, 228
|
||||
@@ -295,7 +298,8 @@ BEGIN
|
||||
LTEXT "DemoType toggle:",IDC_STATIC,7,157,63,8
|
||||
PUSHBUTTON "&...",IDC_DEMOTYPE_BROWSE,231,137,16,13
|
||||
CONTROL "",IDC_DEMOTYPE_SPEED_SLIDER,"msctls_trackbar32",TBS_AUTOTICKS | TBS_BOTH | TBS_NOTICKS | WS_TABSTOP,52,202,150,11,WS_EX_TRANSPARENT
|
||||
CONTROL "Drive input with typing:",IDC_DEMOTYPE_USER_DRIVEN,"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,7,173,88,10
|
||||
CONTROL "Drive input with typing:",IDC_DEMOTYPE_USER_DRIVEN,
|
||||
"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,7,173,88,10
|
||||
LTEXT "DemoType typing speed:",IDC_STATIC,7,189,215,10
|
||||
LTEXT "Slow",IDC_DEMOTYPE_STATIC1,51,213,18,8
|
||||
LTEXT "Fast",IDC_DEMOTYPE_STATIC2,186,213,17,8
|
||||
@@ -413,8 +417,8 @@ ACCELERATORS ACCELERATORS
|
||||
BEGIN
|
||||
"C", IDC_COPY, VIRTKEY, CONTROL, NOINVERT
|
||||
"S", IDC_SAVE, VIRTKEY, CONTROL, NOINVERT
|
||||
"C", IDC_COPY_CROP, VIRTKEY, SHIFT, CONTROL, NOINVERT
|
||||
"S", IDC_SAVE_CROP, VIRTKEY, SHIFT, CONTROL, NOINVERT
|
||||
"C", IDC_COPY_CROP, VIRTKEY, SHIFT, CONTROL, NOINVERT
|
||||
"S", IDC_SAVE_CROP, VIRTKEY, SHIFT, CONTROL, NOINVERT
|
||||
END
|
||||
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ DWORD g_SnipToggleKey = ((HOTKEYF_CONTROL) << 8) | '6';
|
||||
DWORD g_ShowExpiredTime = 1;
|
||||
DWORD g_SliderZoomLevel = 3;
|
||||
BOOLEAN g_AnimateZoom = TRUE;
|
||||
BOOLEAN g_SmoothImage = TRUE;
|
||||
DWORD g_PenColor = COLOR_RED;
|
||||
DWORD g_BreakPenColor = COLOR_RED;
|
||||
DWORD g_RootPenWidth = PEN_WIDTH;
|
||||
@@ -72,6 +73,7 @@ REG_SETTING RegSettings[] = {
|
||||
{ L"ShowTrayIcon", SETTING_TYPE_BOOLEAN, 0, &g_ShowTrayIcon, static_cast<DOUBLE>(g_ShowTrayIcon) },
|
||||
// NOTE: AnimateZoom is misspelled, but since it is a user setting stored in the registry we must continue to misspell it.
|
||||
{ L"AnimnateZoom", SETTING_TYPE_BOOLEAN, 0, &g_AnimateZoom, static_cast<DOUBLE>(g_AnimateZoom) },
|
||||
{ L"SmoothImage", SETTING_TYPE_BOOLEAN, 0, &g_SmoothImage, static_cast<DOUBLE>(g_SmoothImage) },
|
||||
{ L"TelescopeZoomOut", SETTING_TYPE_BOOLEAN, 0, &g_TelescopeZoomOut, static_cast<DOUBLE>(g_TelescopeZoomOut) },
|
||||
{ L"SnapToGrid", SETTING_TYPE_BOOLEAN, 0, &g_SnapToGrid, static_cast<DOUBLE>(g_SnapToGrid) },
|
||||
{ L"ZoominSliderLevel", SETTING_TYPE_DWORD, 0, &g_SliderZoomLevel, static_cast<DOUBLE>(g_SliderZoomLevel) },
|
||||
|
||||
@@ -170,6 +170,8 @@ type_pMagSetFullscreenTransform pMagSetFullscreenTransform;
|
||||
type_pMagSetInputTransform pMagSetInputTransform;
|
||||
type_pMagShowSystemCursor pMagShowSystemCursor;
|
||||
type_pMagSetWindowFilterList pMagSetWindowFilterList;
|
||||
type_MagSetFullscreenUseBitmapSmoothing pMagSetFullscreenUseBitmapSmoothing;
|
||||
type_pMagSetLensUseBitmapSmoothing pMagSetLensUseBitmapSmoothing;
|
||||
type_pMagInitialize pMagInitialize;
|
||||
type_pDwmIsCompositionEnabled pDwmIsCompositionEnabled;
|
||||
type_pGetPointerType pGetPointerType;
|
||||
@@ -1099,6 +1101,8 @@ void DrawHighlightedShape( DWORD Shape, HDC hdcScreenCompat, Gdiplus::Brush *pBr
|
||||
// Create a new bitmap that's the size of the area covered by the line + 2 * g_PenWidth
|
||||
Gdiplus::Rect lineBounds(min(x1, x2), min(y1, y2), abs(x2 - x1), abs(y2 - y1));
|
||||
|
||||
OutputDebug(L"DrawHighlightedShape\n");
|
||||
|
||||
// Expand for line drawing
|
||||
if (Shape == DRAW_LINE)
|
||||
lineBounds.Inflate(static_cast<int>(g_PenWidth / 2), static_cast<int>(g_PenWidth / 2));
|
||||
@@ -1186,7 +1190,7 @@ void DrawHighlightedShape( DWORD Shape, HDC hdcScreenCompat, Gdiplus::Brush *pBr
|
||||
DeleteDC(hdcDIBOrig);
|
||||
|
||||
// Invalidate the updated rectangle
|
||||
// InvalidateGdiplusRect(hWnd, lineBounds);
|
||||
//InvalidateGdiplusRect(hWnd, lineBounds);
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
@@ -1284,7 +1288,12 @@ void ScaleImage( HDC hdcDst, float xDst, float yDst, float wDst, float hDst,
|
||||
{
|
||||
Gdiplus::Bitmap srcBitmap( bmSrc, NULL );
|
||||
|
||||
dstGraphics.SetInterpolationMode( Gdiplus::InterpolationModeLowQuality );
|
||||
// Use high quality interpolation when smooth image is enabled
|
||||
if (g_SmoothImage) {
|
||||
dstGraphics.SetInterpolationMode( Gdiplus::InterpolationModeHighQuality );
|
||||
} else {
|
||||
dstGraphics.SetInterpolationMode( Gdiplus::InterpolationModeLowQuality );
|
||||
}
|
||||
dstGraphics.SetPixelOffsetMode( Gdiplus::PixelOffsetModeHalf );
|
||||
|
||||
dstGraphics.DrawImage( &srcBitmap, Gdiplus::RectF(xDst,yDst,wDst,hDst), xSrc, ySrc, wSrc, hSrc, Gdiplus::UnitPixel );
|
||||
@@ -2071,6 +2080,8 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message,
|
||||
IsAutostartConfigured() ? BST_CHECKED: BST_UNCHECKED );
|
||||
CheckDlgButton( g_OptionsTabs[ZOOM_PAGE].hPage, IDC_ANIMATE_ZOOM,
|
||||
g_AnimateZoom ? BST_CHECKED: BST_UNCHECKED );
|
||||
CheckDlgButton( g_OptionsTabs[ZOOM_PAGE].hPage, IDC_SMOOTH_IMAGE,
|
||||
g_SmoothImage ? BST_CHECKED: BST_UNCHECKED );
|
||||
|
||||
SendMessage( GetDlgItem(g_OptionsTabs[ZOOM_PAGE].hPage, IDC_ZOOM_SLIDER), TBM_SETRANGE, false, MAKELONG(0,_countof(g_ZoomLevels)-1) );
|
||||
SendMessage( GetDlgItem(g_OptionsTabs[ZOOM_PAGE].hPage, IDC_ZOOM_SLIDER), TBM_SETPOS, true, g_SliderZoomLevel );
|
||||
@@ -2210,6 +2221,7 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message,
|
||||
}
|
||||
g_ShowTrayIcon = IsDlgButtonChecked( hDlg, IDC_SHOW_TRAY_ICON ) == BST_CHECKED;
|
||||
g_AnimateZoom = IsDlgButtonChecked( g_OptionsTabs[ZOOM_PAGE].hPage, IDC_ANIMATE_ZOOM ) == BST_CHECKED;
|
||||
g_SmoothImage = IsDlgButtonChecked( g_OptionsTabs[ZOOM_PAGE].hPage, IDC_SMOOTH_IMAGE ) == BST_CHECKED;
|
||||
g_DemoTypeUserDriven = IsDlgButtonChecked( g_OptionsTabs[DEMOTYPE_PAGE].hPage, IDC_DEMOTYPE_USER_DRIVEN ) == BST_CHECKED;
|
||||
|
||||
newToggleKey = static_cast<DWORD>(SendMessage( GetDlgItem( g_OptionsTabs[ZOOM_PAGE].hPage, IDC_HOTKEY), HKM_GETHOTKEY, 0, 0 ));
|
||||
@@ -2723,7 +2735,6 @@ VOID DrawShape( DWORD Shape, HDC hDc, RECT *Rect, bool UseGdiPlus = false )
|
||||
bool isBlur = false;
|
||||
|
||||
Gdiplus::Graphics dstGraphics(hDc);
|
||||
|
||||
if( ( GetWindowLong( g_hWndMain, GWL_EXSTYLE ) & WS_EX_LAYERED ) == 0 )
|
||||
{
|
||||
dstGraphics.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);
|
||||
@@ -2746,6 +2757,7 @@ VOID DrawShape( DWORD Shape, HDC hDc, RECT *Rect, bool UseGdiPlus = false )
|
||||
InflateRect(Rect, g_PenWidth / 2, g_PenWidth / 2);
|
||||
isBlur = true;
|
||||
}
|
||||
OutputDebug(L"Draw shape: highlight: %d pbrush: %d\n", PEN_COLOR_HIGHLIGHT(g_PenColor), pBrush != NULL);
|
||||
|
||||
switch (Shape) {
|
||||
case DRAW_RECTANGLE:
|
||||
@@ -2920,7 +2932,7 @@ void InvalidateCursorMoveArea( HWND hWnd, float zoomLevel, int width, int height
|
||||
{
|
||||
int x, y;
|
||||
RECT rc;
|
||||
int invWidth = g_PenWidth;
|
||||
int invWidth = g_PenWidth + CURSOR_SAVE_MARGIN;
|
||||
|
||||
if( DrawHighlightedCursor( zoomLevel, width, height ) ) {
|
||||
|
||||
@@ -2945,7 +2957,7 @@ void InvalidateCursorMoveArea( HWND hWnd, float zoomLevel, int width, int height
|
||||
void SaveCursorArea( HDC hDcTarget, HDC hDcSource, POINT pt )
|
||||
{
|
||||
OutputDebug( L"SaveCursorArea\n");
|
||||
int penWidth = g_PenWidth + 2;
|
||||
int penWidth = g_PenWidth + CURSOR_SAVE_MARGIN;
|
||||
BitBlt( hDcTarget, 0, 0, penWidth +CURSOR_ARM_LENGTH*2, penWidth +CURSOR_ARM_LENGTH*2,
|
||||
hDcSource, static_cast<INT> (pt.x- penWidth /2)-CURSOR_ARM_LENGTH,
|
||||
static_cast<INT>(pt.y- penWidth /2)-CURSOR_ARM_LENGTH, SRCCOPY|CAPTUREBLT );
|
||||
@@ -2959,7 +2971,7 @@ void SaveCursorArea( HDC hDcTarget, HDC hDcSource, POINT pt )
|
||||
void RestoreCursorArea( HDC hDcTarget, HDC hDcSource, POINT pt )
|
||||
{
|
||||
OutputDebug( L"RestoreCursorArea\n");
|
||||
int penWidth = g_PenWidth + 2;
|
||||
int penWidth = g_PenWidth + CURSOR_SAVE_MARGIN;
|
||||
BitBlt( hDcTarget, static_cast<INT>(pt.x- penWidth /2)-CURSOR_ARM_LENGTH,
|
||||
static_cast<INT>(pt.y- penWidth /2)-CURSOR_ARM_LENGTH, penWidth +CURSOR_ARM_LENGTH*2,
|
||||
penWidth + CURSOR_ARM_LENGTH*2, hDcSource, 0, 0, SRCCOPY|CAPTUREBLT );
|
||||
@@ -4178,6 +4190,11 @@ LRESULT APIENTRY MainWndProc(
|
||||
}
|
||||
#endif
|
||||
}
|
||||
OutputDebug(L"LIVEDRAW SMOOTHING: %d\n", g_SmoothImage);
|
||||
if (!pMagSetLensUseBitmapSmoothing(g_hWndLiveZoomMag, g_SmoothImage))
|
||||
{
|
||||
OutputDebug(L"MagSetLensUseBitmapSmoothing failed: %d\n", GetLastError());
|
||||
}
|
||||
|
||||
if ( g_RecordToggle )
|
||||
{
|
||||
@@ -5296,6 +5313,8 @@ LRESULT APIENTRY MainWndProc(
|
||||
|
||||
if( g_Drawing ) {
|
||||
|
||||
OutputDebug(L"Mousemove: Drawing\n");
|
||||
|
||||
POINT currentPt;
|
||||
|
||||
// Are we in pen mode on a tablet?
|
||||
@@ -5334,7 +5353,15 @@ LRESULT APIENTRY MainWndProc(
|
||||
}
|
||||
else
|
||||
{
|
||||
DrawShape( g_DrawingShape, hdcScreenCompat, &g_rcRectangle );
|
||||
if (PEN_COLOR_HIGHLIGHT(g_PenColor))
|
||||
{
|
||||
// copy original bitmap to screen bitmap to erase previous highlight
|
||||
BitBlt(hdcScreenCompat, 0, 0, bmp.bmWidth, bmp.bmHeight, drawUndoList->hDc, 0, 0, SRCCOPY | CAPTUREBLT);
|
||||
}
|
||||
else
|
||||
{
|
||||
DrawShape(g_DrawingShape, hdcScreenCompat, &g_rcRectangle, PEN_COLOR_HIGHLIGHT(g_PenColor));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5380,7 +5407,7 @@ LRESULT APIENTRY MainWndProc(
|
||||
g_rcRectangle.top != g_rcRectangle.bottom) {
|
||||
|
||||
// Draw the new target rectangle.
|
||||
DrawShape(g_DrawingShape, hdcScreenCompat, &g_rcRectangle);
|
||||
DrawShape(g_DrawingShape, hdcScreenCompat, &g_rcRectangle, PEN_COLOR_HIGHLIGHT(g_PenColor));
|
||||
OutputDebug(L"SHAPE: (%d, %d) - (%d, %d)\n", g_rcRectangle.left, g_rcRectangle.top,
|
||||
g_rcRectangle.right, g_rcRectangle.bottom);
|
||||
}
|
||||
@@ -5418,9 +5445,6 @@ LRESULT APIENTRY MainWndProc(
|
||||
Gdiplus::BitmapData* lineData = LockGdiPlusBitmap(lineBitmap);
|
||||
BYTE* pPixels = static_cast<BYTE*>(lineData->Scan0);
|
||||
|
||||
// Copy the contents of the screen bitmap to the temporary bitmap
|
||||
GetOldestUndo(drawUndoList);
|
||||
|
||||
// Create a GDI bitmap that's the size of the lineBounds rectangle
|
||||
Gdiplus::Bitmap *blurBitmap = CreateGdiplusBitmap( hdcScreenCompat, // oldestUndo->hDc,
|
||||
lineBounds.X, lineBounds.Y, lineBounds.Width, lineBounds.Height);
|
||||
@@ -5445,6 +5469,8 @@ LRESULT APIENTRY MainWndProc(
|
||||
}
|
||||
else if(PEN_COLOR_HIGHLIGHT(g_PenColor)) {
|
||||
|
||||
OutputDebug(L"HIGHLIGHT\n");
|
||||
|
||||
// This is a highlighting pen color
|
||||
Gdiplus::Rect lineBounds = GetLineBounds(prevPt, currentPt, g_PenWidth);
|
||||
Gdiplus::Bitmap* lineBitmap = DrawBitmapLine(lineBounds, prevPt, currentPt, &pen);
|
||||
@@ -5784,26 +5810,30 @@ LRESULT APIENTRY MainWndProc(
|
||||
if( !g_DrawingShape ) {
|
||||
|
||||
// If the point has changed, draw a line to it
|
||||
if (prevPt.x != LOWORD(lParam) || prevPt.y != HIWORD(lParam)) {
|
||||
Gdiplus::Graphics dstGraphics(hdcScreenCompat);
|
||||
if ((GetWindowLong(g_hWndMain, GWL_EXSTYLE) & WS_EX_LAYERED) == 0)
|
||||
if (!PEN_COLOR_HIGHLIGHT(g_PenColor))
|
||||
{
|
||||
if (prevPt.x != LOWORD(lParam) || prevPt.y != HIWORD(lParam))
|
||||
{
|
||||
dstGraphics.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);
|
||||
Gdiplus::Graphics dstGraphics(hdcScreenCompat);
|
||||
if ((GetWindowLong(g_hWndMain, GWL_EXSTYLE) & WS_EX_LAYERED) == 0)
|
||||
{
|
||||
dstGraphics.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);
|
||||
}
|
||||
Gdiplus::Color color = ColorFromColorRef(g_PenColor);
|
||||
Gdiplus::Pen pen(color, static_cast<Gdiplus::REAL>(g_PenWidth));
|
||||
Gdiplus::GraphicsPath path;
|
||||
pen.SetLineJoin(Gdiplus::LineJoinRound);
|
||||
path.AddLine(prevPt.x, prevPt.y, LOWORD(lParam), HIWORD(lParam));
|
||||
dstGraphics.DrawPath(&pen, &path);
|
||||
}
|
||||
// Draw a dot at the current point, if the point hasn't changed
|
||||
else
|
||||
{
|
||||
MoveToEx(hdcScreenCompat, prevPt.x, prevPt.y, NULL);
|
||||
LineTo(hdcScreenCompat, LOWORD(lParam), HIWORD(lParam));
|
||||
InvalidateRect(hWnd, NULL, FALSE);
|
||||
}
|
||||
Gdiplus::Color color = ColorFromColorRef(g_PenColor);
|
||||
Gdiplus::Pen pen(color, static_cast<Gdiplus::REAL>(g_PenWidth));
|
||||
Gdiplus::GraphicsPath path;
|
||||
pen.SetLineJoin(Gdiplus::LineJoinRound);
|
||||
path.AddLine(prevPt.x, prevPt.y, LOWORD(lParam), HIWORD(lParam));
|
||||
dstGraphics.DrawPath(&pen, &path);
|
||||
}
|
||||
// Draw a dot at the current point, if the point hasn't changed
|
||||
else {
|
||||
MoveToEx(hdcScreenCompat, prevPt.x, prevPt.y, NULL);
|
||||
LineTo(hdcScreenCompat, LOWORD(lParam), HIWORD(lParam));
|
||||
InvalidateRect(hWnd, NULL, FALSE);
|
||||
}
|
||||
|
||||
prevPt.x = LOWORD( lParam );
|
||||
prevPt.y = HIWORD( lParam );
|
||||
|
||||
@@ -5818,8 +5848,11 @@ LRESULT APIENTRY MainWndProc(
|
||||
g_rcRectangle.left != g_rcRectangle.right ) {
|
||||
|
||||
// erase previous
|
||||
SetROP2(hdcScreenCompat, R2_NOTXORPEN);
|
||||
DrawShape( g_DrawingShape, hdcScreenCompat, &g_rcRectangle );
|
||||
if (!PEN_COLOR_HIGHLIGHT(g_PenColor))
|
||||
{
|
||||
SetROP2(hdcScreenCompat, R2_NOTXORPEN);
|
||||
DrawShape(g_DrawingShape, hdcScreenCompat, &g_rcRectangle);
|
||||
}
|
||||
|
||||
// Draw the final shape
|
||||
HBRUSH hBrush = static_cast<HBRUSH>(GetStockObject( NULL_BRUSH ));
|
||||
@@ -6185,8 +6218,14 @@ LRESULT APIENTRY MainWndProc(
|
||||
SetStretchBltMode( hInterimSaveDc, HALFTONE );
|
||||
SetStretchBltMode( hSaveDc, HALFTONE );
|
||||
#else
|
||||
SetStretchBltMode( hInterimSaveDc, COLORONCOLOR );
|
||||
SetStretchBltMode( hSaveDc, COLORONCOLOR );
|
||||
// Use HALFTONE for better quality when smooth image is enabled
|
||||
if (g_SmoothImage) {
|
||||
SetStretchBltMode( hInterimSaveDc, HALFTONE );
|
||||
SetStretchBltMode( hSaveDc, HALFTONE );
|
||||
} else {
|
||||
SetStretchBltMode( hInterimSaveDc, COLORONCOLOR );
|
||||
SetStretchBltMode( hSaveDc, COLORONCOLOR );
|
||||
}
|
||||
#endif
|
||||
StretchBlt( hInterimSaveDc,
|
||||
0, 0,
|
||||
@@ -6309,7 +6348,12 @@ LRESULT APIENTRY MainWndProc(
|
||||
#if SCALE_HALFTONE
|
||||
SetStretchBltMode( hSaveDc, HALFTONE );
|
||||
#else
|
||||
SetStretchBltMode( hSaveDc, COLORONCOLOR );
|
||||
// Use HALFTONE for better quality when smooth image is enabled
|
||||
if (g_SmoothImage) {
|
||||
SetStretchBltMode( hSaveDc, HALFTONE );
|
||||
} else {
|
||||
SetStretchBltMode( hSaveDc, COLORONCOLOR );
|
||||
}
|
||||
#endif
|
||||
StretchBlt( hSaveDc,
|
||||
0, 0,
|
||||
@@ -6646,8 +6690,8 @@ LRESULT APIENTRY MainWndProc(
|
||||
(float)x, (float)y,
|
||||
width/zoomLevel, height/zoomLevel );
|
||||
} else {
|
||||
// do a fast, less accurate render
|
||||
SetStretchBltMode( hDc, HALFTONE );
|
||||
// do a fast, less accurate render (but use smooth if enabled)
|
||||
SetStretchBltMode( hDc, g_SmoothImage ? HALFTONE : COLORONCOLOR );
|
||||
StretchBlt( ps.hdc,
|
||||
0, 0,
|
||||
bmp.bmWidth, bmp.bmHeight,
|
||||
@@ -6660,7 +6704,12 @@ LRESULT APIENTRY MainWndProc(
|
||||
#if SCALE_HALFTONE
|
||||
SetStretchBltMode( hDc, zoomLevel == zoomTelescopeTarget ? HALFTONE : COLORONCOLOR );
|
||||
#else
|
||||
SetStretchBltMode( hDc, COLORONCOLOR );
|
||||
// Use HALFTONE for better quality when smooth image is enabled
|
||||
if (g_SmoothImage) {
|
||||
SetStretchBltMode( hDc, HALFTONE );
|
||||
} else {
|
||||
SetStretchBltMode( hDc, COLORONCOLOR );
|
||||
}
|
||||
#endif
|
||||
StretchBlt( ps.hdc,
|
||||
0, 0,
|
||||
@@ -6683,7 +6732,7 @@ LRESULT APIENTRY MainWndProc(
|
||||
|
||||
BITMAP local_bmp;
|
||||
GetObject(g_hBackgroundBmp, sizeof(local_bmp), &local_bmp);
|
||||
SetStretchBltMode( hdcScreenCompat, HALFTONE );
|
||||
SetStretchBltMode( hdcScreenCompat, g_SmoothImage ? HALFTONE : COLORONCOLOR );
|
||||
if( g_BreakBackgroundStretch ) {
|
||||
StretchBlt( hdcScreenCompat, 0, 0, width, height,
|
||||
g_hDcBackgroundFile, 0, 0, local_bmp.bmWidth, local_bmp.bmHeight, SRCCOPY|CAPTUREBLT );
|
||||
@@ -6842,7 +6891,6 @@ LRESULT CALLBACK LiveZoomWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM
|
||||
WS_CHILD | MS_SHOWMAGNIFIEDCURSOR | WS_VISIBLE,
|
||||
0, 0, 0, 0, hWnd, NULL, g_hInstance, NULL );
|
||||
}
|
||||
|
||||
ShowWindow( hWnd, SW_SHOW );
|
||||
InvalidateRect( g_hWndLiveZoomMag, NULL, TRUE );
|
||||
|
||||
@@ -7555,6 +7603,10 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance
|
||||
"MagSetWindowTransform" );
|
||||
pMagSetFullscreenTransform = (type_pMagSetFullscreenTransform)GetProcAddress(LoadLibrarySafe(L"magnification.dll", DLL_LOAD_LOCATION_SYSTEM),
|
||||
"MagSetFullscreenTransform");
|
||||
pMagSetFullscreenUseBitmapSmoothing = (type_MagSetFullscreenUseBitmapSmoothing)GetProcAddress(LoadLibrarySafe(L"magnification.dll", DLL_LOAD_LOCATION_SYSTEM),
|
||||
"MagSetFullscreenUseBitmapSmoothing");
|
||||
pMagSetLensUseBitmapSmoothing = (type_pMagSetLensUseBitmapSmoothing)GetProcAddress(LoadLibrarySafe(L"magnification.dll", DLL_LOAD_LOCATION_SYSTEM),
|
||||
"MagSetLensUseBitmapSmoothing");
|
||||
pMagSetInputTransform = (type_pMagSetInputTransform)GetProcAddress(LoadLibrarySafe(L"magnification.dll", DLL_LOAD_LOCATION_SYSTEM),
|
||||
"MagSetInputTransform");
|
||||
pMagShowSystemCursor = (type_pMagShowSystemCursor)GetProcAddress(LoadLibrarySafe(L"magnification.dll", DLL_LOAD_LOCATION_SYSTEM),
|
||||
|
||||
@@ -95,6 +95,7 @@
|
||||
#define IDC_COPYRIGHT 1075
|
||||
#define IDC_PEN_WIDTH 1105
|
||||
#define IDC_TIMER 1106
|
||||
#define IDC_SMOOTH_IMAGE 1107
|
||||
#define IDC_SAVE 40002
|
||||
#define IDC_COPY 40004
|
||||
#define IDC_RECORD 40006
|
||||
@@ -109,7 +110,7 @@
|
||||
#ifndef APSTUDIO_READONLY_SYMBOLS
|
||||
#define _APS_NEXT_RESOURCE_VALUE 118
|
||||
#define _APS_NEXT_COMMAND_VALUE 40013
|
||||
#define _APS_NEXT_CONTROL_VALUE 1076
|
||||
#define _APS_NEXT_CONTROL_VALUE 1078
|
||||
#define _APS_NEXT_SYMED_VALUE 101
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// Encapsulates a navigation request within Command Palette view models.
|
||||
/// </summary>
|
||||
/// <param name="TargetViewModel">A view model that should be navigated to.</param>
|
||||
/// <param name="NavigationToken"> A <see cref="CancellationToken"/> that can be used to cancel the pending navigation.</param>
|
||||
public record AsyncNavigationRequest(object? TargetViewModel, CancellationToken NavigationToken);
|
||||
@@ -4,6 +4,4 @@
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
|
||||
public record NavigateToPageMessage(PageViewModel Page, bool WithAnimation)
|
||||
{
|
||||
}
|
||||
public record NavigateToPageMessage(PageViewModel Page, bool WithAnimation, CancellationToken CancellationToken);
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels;
|
||||
|
||||
internal sealed partial class NullPageViewModel(TaskScheduler scheduler, AppExtensionHost extensionHost)
|
||||
: PageViewModel(null, scheduler, extensionHost);
|
||||
@@ -23,6 +23,9 @@ public partial class ShellViewModel : ObservableObject,
|
||||
private readonly Lock _invokeLock = new();
|
||||
private Task? _handleInvokeTask;
|
||||
|
||||
// Cancellation token source for page loading/navigation operations
|
||||
private CancellationTokenSource? _navigationCts;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsLoaded { get; set; } = false;
|
||||
|
||||
@@ -66,6 +69,8 @@ public partial class ShellViewModel : ObservableObject,
|
||||
|
||||
public bool IsNested => _isNested;
|
||||
|
||||
public PageViewModel NullPage { get; private set; }
|
||||
|
||||
public ShellViewModel(
|
||||
TaskScheduler scheduler,
|
||||
IRootPageService rootPageService,
|
||||
@@ -77,6 +82,7 @@ public partial class ShellViewModel : ObservableObject,
|
||||
_rootPageService = rootPageService;
|
||||
_appHostService = appHostService;
|
||||
|
||||
NullPage = new NullPageViewModel(_scheduler, appHostService.GetDefaultHost());
|
||||
_currentPage = new LoadingPageViewModel(null, _scheduler, appHostService.GetDefaultHost());
|
||||
|
||||
// Register to receive messages
|
||||
@@ -113,7 +119,7 @@ public partial class ShellViewModel : ObservableObject,
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task LoadPageViewModelAsync(PageViewModel viewModel)
|
||||
private async Task LoadPageViewModelAsync(PageViewModel viewModel, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Note: We removed the general loading state, extensions sometimes use their `IsLoading`, but it's inconsistently implemented it seems.
|
||||
// IsInitialized is our main indicator of the general overall state of loading props/items from a page we use for the progress bar
|
||||
@@ -125,44 +131,80 @@ public partial class ShellViewModel : ObservableObject,
|
||||
if (!viewModel.IsInitialized
|
||||
&& viewModel.InitializeCommand is not null)
|
||||
{
|
||||
var outer = Task.Run(async () =>
|
||||
{
|
||||
// You know, this creates the situation where we wait for
|
||||
// both loading page properties, AND the items, before we
|
||||
// display anything.
|
||||
//
|
||||
// We almost need to do an async await on initialize, then
|
||||
// just a fire-and-forget on FetchItems.
|
||||
// RE: We do set the CurrentPage in ShellPage.xaml.cs as well, so, we kind of are doing two different things here.
|
||||
// Definitely some more clean-up to do, but at least its centralized to one spot now.
|
||||
viewModel.InitializeCommand.Execute(null);
|
||||
|
||||
await viewModel.InitializeCommand.ExecutionTask!;
|
||||
|
||||
if (viewModel.InitializeCommand.ExecutionTask.Status != TaskStatus.RanToCompletion)
|
||||
var outer = Task.Run(
|
||||
async () =>
|
||||
{
|
||||
if (viewModel.InitializeCommand.ExecutionTask.Exception is AggregateException ex)
|
||||
// You know, this creates the situation where we wait for
|
||||
// both loading page properties, AND the items, before we
|
||||
// display anything.
|
||||
//
|
||||
// We almost need to do an async await on initialize, then
|
||||
// just a fire-and-forget on FetchItems.
|
||||
// RE: We do set the CurrentPage in ShellPage.xaml.cs as well, so, we kind of are doing two different things here.
|
||||
// Definitely some more clean-up to do, but at least its centralized to one spot now.
|
||||
viewModel.InitializeCommand.Execute(null);
|
||||
|
||||
await viewModel.InitializeCommand.ExecutionTask!;
|
||||
|
||||
if (viewModel.InitializeCommand.ExecutionTask.Status != TaskStatus.RanToCompletion)
|
||||
{
|
||||
CoreLogger.LogError(ex.ToString());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var t = Task.Factory.StartNew(
|
||||
() =>
|
||||
if (viewModel.InitializeCommand.ExecutionTask.Exception is AggregateException ex)
|
||||
{
|
||||
CurrentPage = viewModel;
|
||||
},
|
||||
CancellationToken.None,
|
||||
TaskCreationOptions.None,
|
||||
_scheduler);
|
||||
await t;
|
||||
}
|
||||
});
|
||||
CoreLogger.LogError(ex.ToString());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var t = Task.Factory.StartNew(
|
||||
() =>
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
if (viewModel is IDisposable disposable)
|
||||
{
|
||||
try
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CoreLogger.LogError(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
CurrentPage = viewModel;
|
||||
},
|
||||
cancellationToken,
|
||||
TaskCreationOptions.None,
|
||||
_scheduler);
|
||||
await t;
|
||||
}
|
||||
},
|
||||
cancellationToken);
|
||||
await outer;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
if (viewModel is IDisposable disposable)
|
||||
{
|
||||
try
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CoreLogger.LogError(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
CurrentPage = viewModel;
|
||||
}
|
||||
}
|
||||
@@ -174,6 +216,28 @@ public partial class ShellViewModel : ObservableObject,
|
||||
|
||||
private void PerformCommand(PerformCommandMessage message)
|
||||
{
|
||||
// Create/replace the navigation cancellation token.
|
||||
// If one already exists, cancel and dispose it first.
|
||||
var newCts = new CancellationTokenSource();
|
||||
var oldCts = Interlocked.Exchange(ref _navigationCts, newCts);
|
||||
if (oldCts is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
oldCts.Cancel();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CoreLogger.LogError(ex.ToString());
|
||||
}
|
||||
finally
|
||||
{
|
||||
oldCts.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
var navigationToken = newCts.Token;
|
||||
|
||||
var command = message.Command.Unsafe;
|
||||
if (command is null)
|
||||
{
|
||||
@@ -202,15 +266,26 @@ public partial class ShellViewModel : ObservableObject,
|
||||
}
|
||||
|
||||
// Kick off async loading of our ViewModel
|
||||
LoadPageViewModelAsync(pageViewModel)
|
||||
LoadPageViewModelAsync(pageViewModel, navigationToken)
|
||||
.ContinueWith(
|
||||
(Task t) =>
|
||||
{
|
||||
OnUIThread(() => { WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(null)); });
|
||||
WeakReferenceMessenger.Default.Send<NavigateToPageMessage>(new(pageViewModel, message.WithAnimation));
|
||||
// clean up the navigation token if it's still ours
|
||||
if (Interlocked.CompareExchange(ref _navigationCts, null, newCts) == newCts)
|
||||
{
|
||||
newCts.Dispose();
|
||||
}
|
||||
|
||||
// When we're done loading the page, then update the command bar to match
|
||||
WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(null));
|
||||
},
|
||||
navigationToken,
|
||||
TaskContinuationOptions.None,
|
||||
_scheduler);
|
||||
|
||||
// While we're loading in the background, immediately move to the next page.
|
||||
WeakReferenceMessenger.Default.Send<NavigateToPageMessage>(new(pageViewModel, message.WithAnimation, navigationToken));
|
||||
|
||||
// Note: Originally we set our page back in the ViewModel here, but that now happens in response to the Frame navigating triggered from the above
|
||||
// See RootFrame_Navigated event handler.
|
||||
}
|
||||
@@ -368,4 +443,9 @@ public partial class ShellViewModel : ObservableObject,
|
||||
TaskCreationOptions.None,
|
||||
_scheduler);
|
||||
}
|
||||
|
||||
public void CancelNavigation()
|
||||
{
|
||||
_navigationCts?.Cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ using ManagedCommon;
|
||||
using Microsoft.CmdPal.Core.Common.Helpers;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.Ext.Apps;
|
||||
using Microsoft.CmdPal.Ext.Apps.Programs;
|
||||
using Microsoft.CmdPal.Ext.Apps.State;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Properties;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
@@ -36,6 +38,7 @@ public partial class MainListPage : DynamicListPage,
|
||||
private List<Scored<IListItem>>? _filteredItems;
|
||||
private List<Scored<IListItem>>? _filteredApps;
|
||||
private List<Scored<IListItem>>? _fallbackItems;
|
||||
private IEnumerable<Scored<IListItem>>? _scoredFallbackItems;
|
||||
private bool _includeApps;
|
||||
private bool _filteredItemsIncludesApps;
|
||||
private int _appResultLimit = 10;
|
||||
@@ -160,7 +163,7 @@ public partial class MainListPage : DynamicListPage,
|
||||
{
|
||||
lock (_tlcManager.TopLevelCommands)
|
||||
{
|
||||
List<Scored<IListItem>> limitedApps = new List<Scored<IListItem>>();
|
||||
var limitedApps = new List<Scored<IListItem>>();
|
||||
|
||||
// Fuzzy matching can produce a lot of results, so we want to limit the
|
||||
// number of apps we show at once if it's a large set.
|
||||
@@ -171,6 +174,7 @@ public partial class MainListPage : DynamicListPage,
|
||||
|
||||
var items = Enumerable.Empty<Scored<IListItem>>()
|
||||
.Concat(_filteredItems is not null ? _filteredItems : [])
|
||||
.Concat(_scoredFallbackItems is not null ? _scoredFallbackItems : [])
|
||||
.Concat(limitedApps)
|
||||
.OrderByDescending(o => o.Score)
|
||||
|
||||
@@ -184,6 +188,14 @@ public partial class MainListPage : DynamicListPage,
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearResults()
|
||||
{
|
||||
_filteredItems = null;
|
||||
_filteredApps = null;
|
||||
_fallbackItems = null;
|
||||
_scoredFallbackItems = null;
|
||||
}
|
||||
|
||||
public override void UpdateSearchText(string oldSearch, string newSearch)
|
||||
{
|
||||
var timer = new Stopwatch();
|
||||
@@ -216,8 +228,7 @@ public partial class MainListPage : DynamicListPage,
|
||||
lock (_tlcManager.TopLevelCommands)
|
||||
{
|
||||
_filteredItemsIncludesApps = _includeApps;
|
||||
_filteredItems = null;
|
||||
_filteredApps = null;
|
||||
ClearResults();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,9 +255,7 @@ public partial class MainListPage : DynamicListPage,
|
||||
if (string.IsNullOrEmpty(newSearch))
|
||||
{
|
||||
_filteredItemsIncludesApps = _includeApps;
|
||||
_filteredItems = null;
|
||||
_filteredApps = null;
|
||||
_fallbackItems = null;
|
||||
ClearResults();
|
||||
RaiseItemsChanged(commands.Count);
|
||||
return;
|
||||
}
|
||||
@@ -255,17 +264,13 @@ public partial class MainListPage : DynamicListPage,
|
||||
// re-use previous results. Reset _filteredItems, and keep er moving.
|
||||
if (!newSearch.StartsWith(oldSearch, StringComparison.CurrentCultureIgnoreCase))
|
||||
{
|
||||
_filteredItems = null;
|
||||
_filteredApps = null;
|
||||
_fallbackItems = null;
|
||||
ClearResults();
|
||||
}
|
||||
|
||||
// If the internal state has changed, reset _filteredItems to reset the list.
|
||||
if (_filteredItemsIncludesApps != _includeApps)
|
||||
{
|
||||
_filteredItems = null;
|
||||
_filteredApps = null;
|
||||
_fallbackItems = null;
|
||||
ClearResults();
|
||||
}
|
||||
|
||||
if (token.IsCancellationRequested)
|
||||
@@ -273,9 +278,9 @@ public partial class MainListPage : DynamicListPage,
|
||||
return;
|
||||
}
|
||||
|
||||
IEnumerable<IListItem> newFilteredItems = Enumerable.Empty<IListItem>();
|
||||
IEnumerable<IListItem> newFallbacks = Enumerable.Empty<IListItem>();
|
||||
IEnumerable<IListItem> newApps = Enumerable.Empty<IListItem>();
|
||||
var newFilteredItems = Enumerable.Empty<IListItem>();
|
||||
var newFallbacks = Enumerable.Empty<IListItem>();
|
||||
var newApps = Enumerable.Empty<IListItem>();
|
||||
|
||||
if (_filteredItems is not null)
|
||||
{
|
||||
@@ -314,7 +319,7 @@ public partial class MainListPage : DynamicListPage,
|
||||
// We're going to start over with our fallbacks
|
||||
newFallbacks = Enumerable.Empty<IListItem>();
|
||||
|
||||
newFilteredItems = commands.Where(s => !s.IsFallback || _specialFallbacks.Contains(s.CommandProviderId));
|
||||
newFilteredItems = commands.Where(s => !s.IsFallback);
|
||||
|
||||
// Fallbacks are always included in the list, even if they
|
||||
// don't match the search text. But we don't want to
|
||||
@@ -330,7 +335,20 @@ public partial class MainListPage : DynamicListPage,
|
||||
|
||||
if (_includeApps)
|
||||
{
|
||||
newApps = AllAppsCommandProvider.Page.GetItems().ToList();
|
||||
var allNewApps = AllAppsCommandProvider.Page.GetItems().ToList();
|
||||
|
||||
// We need to remove pinned apps from allNewApps so they don't show twice.
|
||||
var pinnedApps = PinnedAppsManager.Instance.GetPinnedAppIdentifiers();
|
||||
|
||||
if (pinnedApps.Length > 0)
|
||||
{
|
||||
newApps = allNewApps.Where(w =>
|
||||
pinnedApps.IndexOf(((AppListItem)w).AppIdentifier) < 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
newApps = allNewApps;
|
||||
}
|
||||
}
|
||||
|
||||
if (token.IsCancellationRequested)
|
||||
@@ -339,8 +357,25 @@ public partial class MainListPage : DynamicListPage,
|
||||
}
|
||||
}
|
||||
|
||||
var history = _serviceProvider.GetService<AppStateModel>()!.RecentCommands!;
|
||||
Func<string, IListItem, int> scoreItem = (a, b) => { return ScoreTopLevelItem(a, b, history); };
|
||||
|
||||
// Produce a list of everything that matches the current filter.
|
||||
_filteredItems = [.. ListHelpers.FilterListWithScores<IListItem>(newFilteredItems ?? [], SearchText, ScoreTopLevelItem)];
|
||||
_filteredItems = [.. ListHelpers.FilterListWithScores<IListItem>(newFilteredItems ?? [], SearchText, scoreItem)];
|
||||
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IEnumerable<IListItem> newFallbacksForScoring = commands.Where(s => s.IsFallback && _specialFallbacks.Contains(s.CommandProviderId));
|
||||
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_scoredFallbackItems = ListHelpers.FilterListWithScores<IListItem>(newFallbacksForScoring ?? [], SearchText, scoreItem);
|
||||
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
@@ -358,7 +393,7 @@ public partial class MainListPage : DynamicListPage,
|
||||
// Produce a list of filtered apps with the appropriate limit
|
||||
if (newApps.Any())
|
||||
{
|
||||
var scoredApps = ListHelpers.FilterListWithScores<IListItem>(newApps, SearchText, ScoreTopLevelItem);
|
||||
var scoredApps = ListHelpers.FilterListWithScores<IListItem>(newApps, SearchText, scoreItem);
|
||||
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
@@ -425,7 +460,7 @@ public partial class MainListPage : DynamicListPage,
|
||||
// Almost verbatim ListHelpers.ScoreListItem, but also accounting for the
|
||||
// fact that we want fallback handlers down-weighted, so that they don't
|
||||
// _always_ show up first.
|
||||
private int ScoreTopLevelItem(string query, IListItem topLevelOrAppItem)
|
||||
internal static int ScoreTopLevelItem(string query, IListItem topLevelOrAppItem, IRecentCommandsManager history)
|
||||
{
|
||||
var title = topLevelOrAppItem.Title;
|
||||
if (string.IsNullOrWhiteSpace(title))
|
||||
@@ -501,10 +536,9 @@ public partial class MainListPage : DynamicListPage,
|
||||
// here we add the recent command weight boost
|
||||
//
|
||||
// Otherwise something like `x` will still match everything you've run before
|
||||
var finalScore = matchSomething;
|
||||
var finalScore = matchSomething * 10;
|
||||
if (matchSomething > 0)
|
||||
{
|
||||
var history = _serviceProvider.GetService<AppStateModel>()!.RecentCommands;
|
||||
var recentWeightBoost = history.GetCommandHistoryWeight(id);
|
||||
finalScore += recentWeightBoost;
|
||||
}
|
||||
@@ -521,7 +555,7 @@ public partial class MainListPage : DynamicListPage,
|
||||
AppStateModel.SaveState(state);
|
||||
}
|
||||
|
||||
private string IdForTopLevelOrAppItem(IListItem topLevelOrAppItem)
|
||||
private static string IdForTopLevelOrAppItem(IListItem topLevelOrAppItem)
|
||||
{
|
||||
if (topLevelOrAppItem is TopLevelViewModel topLevel)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Microsoft.CmdPal.UI.ViewModels.UnitTests")]
|
||||
@@ -7,7 +7,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class RecentCommandsManager : ObservableObject
|
||||
public partial class RecentCommandsManager : ObservableObject, IRecentCommandsManager
|
||||
{
|
||||
[JsonInclude]
|
||||
internal List<HistoryItem> History { get; set; } = [];
|
||||
@@ -80,3 +80,10 @@ public partial class RecentCommandsManager : ObservableObject
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface IRecentCommandsManager
|
||||
{
|
||||
int GetCommandHistoryWeight(string commandId);
|
||||
|
||||
void AddHistoryItem(string commandId);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
xmlns:toolkit="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:ui="using:CommunityToolkit.WinUI"
|
||||
xmlns:viewModels="using:Microsoft.CmdPal.UI.ViewModels"
|
||||
Background="Transparent"
|
||||
PreviewKeyDown="UserControl_PreviewKeyDown"
|
||||
mc:Ignorable="d">
|
||||
|
||||
@@ -22,7 +21,7 @@
|
||||
<ResourceDictionary>
|
||||
<cmdpalUI:KeyChordToStringConverter x:Key="KeyChordToStringConverter" />
|
||||
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
|
||||
|
||||
<Thickness x:Key="DefaultContextMenuItemPadding">12,8,12,8</Thickness>
|
||||
<cmdpalUI:ContextItemTemplateSelector
|
||||
x:Key="ContextItemTemplateSelector"
|
||||
Critical="{StaticResource CriticalContextMenuViewModelTemplate}"
|
||||
@@ -31,7 +30,7 @@
|
||||
|
||||
<!-- Template for context items in the context item menu -->
|
||||
<DataTemplate x:Key="DefaultContextMenuViewModelTemplate" x:DataType="coreViewModels:CommandContextItemViewModel">
|
||||
<Grid AutomationProperties.Name="{x:Bind Title, Mode=OneWay}">
|
||||
<Grid Padding="{StaticResource DefaultContextMenuItemPadding}" AutomationProperties.Name="{x:Bind Title, Mode=OneWay}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="32" />
|
||||
<ColumnDefinition Width="*" />
|
||||
@@ -71,7 +70,7 @@
|
||||
|
||||
<!-- Template for context items flagged as critical -->
|
||||
<DataTemplate x:Key="CriticalContextMenuViewModelTemplate" x:DataType="coreViewModels:CommandContextItemViewModel">
|
||||
<Grid AutomationProperties.Name="{x:Bind Title, Mode=OneWay}">
|
||||
<Grid Padding="{StaticResource DefaultContextMenuItemPadding}" AutomationProperties.Name="{x:Bind Title, Mode=OneWay}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="32" />
|
||||
<ColumnDefinition Width="*" />
|
||||
@@ -114,7 +113,7 @@
|
||||
<DataTemplate x:Key="SeparatorContextMenuViewModelTemplate" x:DataType="coreViewModels:SeparatorViewModel">
|
||||
<Rectangle
|
||||
Height="1"
|
||||
Margin="-16,-12,-12,-12"
|
||||
Margin="0,2,0,2"
|
||||
Fill="{ThemeResource MenuFlyoutSeparatorBackground}" />
|
||||
</DataTemplate>
|
||||
</ResourceDictionary>
|
||||
@@ -125,35 +124,39 @@
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<StackPanel x:Name="CommandsPanel">
|
||||
<ListView
|
||||
x:Name="CommandsDropdown"
|
||||
MinWidth="248"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="CommandsDropdown_ItemClick"
|
||||
ItemTemplateSelector="{StaticResource ContextItemTemplateSelector}"
|
||||
ItemsSource="{x:Bind ViewModel.FilteredItems, Mode=OneWay}"
|
||||
PreviewKeyDown="CommandsDropdown_PreviewKeyDown"
|
||||
SelectionMode="Single">
|
||||
<ListView.ItemContainerStyle>
|
||||
<Style BasedOn="{StaticResource DefaultListViewItemStyle}" TargetType="ListViewItem">
|
||||
<Setter Property="MinHeight" Value="0" />
|
||||
<Setter Property="Padding" Value="12,8" />
|
||||
</Style>
|
||||
</ListView.ItemContainerStyle>
|
||||
<ListView.ItemContainerTransitions>
|
||||
<TransitionCollection />
|
||||
</ListView.ItemContainerTransitions>
|
||||
</ListView>
|
||||
</StackPanel>
|
||||
<ListView
|
||||
x:Name="CommandsDropdown"
|
||||
MinWidth="248"
|
||||
Margin="0,4,0,2"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="CommandsDropdown_ItemClick"
|
||||
ItemTemplateSelector="{StaticResource ContextItemTemplateSelector}"
|
||||
ItemsSource="{x:Bind ViewModel.FilteredItems, Mode=OneWay}"
|
||||
PreviewKeyDown="CommandsDropdown_PreviewKeyDown"
|
||||
SelectionMode="Single">
|
||||
<ListView.ItemContainerStyle>
|
||||
<Style BasedOn="{StaticResource DefaultListViewItemStyle}" TargetType="ListViewItem">
|
||||
<Setter Property="MinHeight" Value="0" />
|
||||
<Setter Property="Padding" Value="0" />
|
||||
</Style>
|
||||
</ListView.ItemContainerStyle>
|
||||
<ListView.ItemContainerTransitions>
|
||||
<TransitionCollection />
|
||||
</ListView.ItemContainerTransitions>
|
||||
</ListView>
|
||||
<Border BorderBrush="{ThemeResource MenuFlyoutSeparatorBackground}" BorderThickness="0,0,0,1" />
|
||||
<TextBox
|
||||
x:Name="ContextFilterBox"
|
||||
x:Uid="ContextFilterBox"
|
||||
Margin="4"
|
||||
Margin="0"
|
||||
Padding="10,7,6,8"
|
||||
Background="{ThemeResource AcrylicBackgroundFillColorBaseBrush}"
|
||||
BorderThickness="0,0,0,2"
|
||||
CornerRadius="8, 8, 0, 0"
|
||||
IsTextScaleFactorEnabled="True"
|
||||
KeyDown="ContextFilterBox_KeyDown"
|
||||
PreviewKeyDown="ContextFilterBox_PreviewKeyDown"
|
||||
Style="{StaticResource SearchTextBoxStyle}"
|
||||
TextChanged="ContextFilterBox_TextChanged" />
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="ContextMenuOrder">
|
||||
@@ -162,9 +165,11 @@
|
||||
<ui:IsEqualStateTrigger Value="{x:Bind ViewModel.FilterOnTop, Mode=OneWay}" To="True" />
|
||||
</VisualState.StateTriggers>
|
||||
<VisualState.Setters>
|
||||
<Setter Target="CommandsPanel.(Grid.Row)" Value="1" />
|
||||
<Setter Target="CommandsDropdown.(Grid.Row)" Value="1" />
|
||||
<Setter Target="ContextFilterBox.(Grid.Row)" Value="0" />
|
||||
<Setter Target="CommandsDropdown.Margin" Value="0, 0, 0, 4" />
|
||||
<Setter Target="CommandsDropdown.Margin" Value="0, 3, 0, 4" />
|
||||
<Setter Target="ContextFilterBox.CornerRadius" Value="8, 8, 0, 0" />
|
||||
<Setter Target="ContextFilterBox.Margin" Value="0,0,0,-1" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="FilterOnBottom">
|
||||
@@ -172,9 +177,11 @@
|
||||
<ui:IsEqualStateTrigger Value="{x:Bind ViewModel.FilterOnTop, Mode=OneWay}" To="False" />
|
||||
</VisualState.StateTriggers>
|
||||
<VisualState.Setters>
|
||||
<Setter Target="CommandsPanel.(Grid.Row)" Value="0" />
|
||||
<Setter Target="CommandsDropdown.(Grid.Row)" Value="0" />
|
||||
<Setter Target="ContextFilterBox.(Grid.Row)" Value="1" />
|
||||
<Setter Target="CommandsDropdown.Margin" Value="0, 4, 0, 0" />
|
||||
<Setter Target="CommandsDropdown.Margin" Value="0, 4, 0, 4" />
|
||||
<Setter Target="ContextFilterBox.CornerRadius" Value="0, 0, 8, 8" />
|
||||
<Setter Target="ContextFilterBox.Margin" Value="0,0,0,-2" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
|
||||
@@ -46,11 +46,18 @@ public sealed partial class ContentPage : Page,
|
||||
|
||||
protected override void OnNavigatedTo(NavigationEventArgs e)
|
||||
{
|
||||
if (e.Parameter is ContentPageViewModel vm)
|
||||
if (e.Parameter is not AsyncNavigationRequest navigationRequest)
|
||||
{
|
||||
ViewModel = vm;
|
||||
throw new InvalidOperationException($"Invalid navigation parameter: {nameof(e.Parameter)} must be {nameof(AsyncNavigationRequest)}");
|
||||
}
|
||||
|
||||
if (navigationRequest.TargetViewModel is not ContentPageViewModel contentPageViewModel)
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid navigation target: AsyncNavigationRequest.{nameof(AsyncNavigationRequest.TargetViewModel)} must be {nameof(ContentPageViewModel)}");
|
||||
}
|
||||
|
||||
ViewModel = contentPageViewModel;
|
||||
|
||||
if (!WeakReferenceMessenger.Default.IsRegistered<ActivateSelectedListItemMessage>(this))
|
||||
{
|
||||
WeakReferenceMessenger.Default.Register<ActivateSelectedListItemMessage>(this);
|
||||
|
||||
@@ -59,11 +59,18 @@ public sealed partial class ListPage : Page,
|
||||
|
||||
protected override void OnNavigatedTo(NavigationEventArgs e)
|
||||
{
|
||||
if (e.Parameter is ListViewModel lvm)
|
||||
if (e.Parameter is not AsyncNavigationRequest navigationRequest)
|
||||
{
|
||||
ViewModel = lvm;
|
||||
throw new InvalidOperationException($"Invalid navigation parameter: {nameof(e.Parameter)} must be {nameof(AsyncNavigationRequest)}");
|
||||
}
|
||||
|
||||
if (navigationRequest.TargetViewModel is not ListViewModel listViewModel)
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid navigation target: AsyncNavigationRequest.{nameof(AsyncNavigationRequest.TargetViewModel)} must be {nameof(ListViewModel)}");
|
||||
}
|
||||
|
||||
ViewModel = listViewModel;
|
||||
|
||||
if (e.NavigationMode == NavigationMode.Back
|
||||
|| (e.NavigationMode == NavigationMode.New && ItemView.Items.Count > 0))
|
||||
{
|
||||
|
||||
@@ -23,24 +23,30 @@ public sealed partial class LoadingPage : Page
|
||||
|
||||
protected override void OnNavigatedTo(NavigationEventArgs e)
|
||||
{
|
||||
if (e.Parameter is ShellViewModel shellVM
|
||||
&& shellVM.LoadCommand is not null)
|
||||
if (e.Parameter is not AsyncNavigationRequest request)
|
||||
{
|
||||
// This will load the built-in commands, then navigate to the main page.
|
||||
// Once the mainpage loads, we'll start loading extensions.
|
||||
shellVM.LoadCommand.Execute(null);
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await shellVM.LoadCommand.ExecutionTask!;
|
||||
|
||||
if (shellVM.LoadCommand.ExecutionTask.Status != TaskStatus.RanToCompletion)
|
||||
{
|
||||
// TODO: Handle failure case
|
||||
}
|
||||
});
|
||||
throw new InvalidOperationException($"Invalid navigation parameter: {nameof(e.Parameter)} must be {nameof(AsyncNavigationRequest)}");
|
||||
}
|
||||
|
||||
if (request.TargetViewModel is not ShellViewModel shellVM)
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid navigation target: AsyncNavigationRequest.{nameof(AsyncNavigationRequest.TargetViewModel)} must be {nameof(ShellViewModel)}");
|
||||
}
|
||||
|
||||
// This will load the built-in commands, then navigate to the main page.
|
||||
// Once the mainpage loads, we'll start loading extensions.
|
||||
shellVM.LoadCommand.Execute(null);
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await shellVM.LoadCommand.ExecutionTask!;
|
||||
|
||||
if (shellVM.LoadCommand.ExecutionTask.Status != TaskStatus.RanToCompletion)
|
||||
{
|
||||
// TODO: Handle failure case
|
||||
}
|
||||
});
|
||||
|
||||
base.OnNavigatedTo(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
AddHandler(KeyDownEvent, new KeyEventHandler(ShellPage_OnKeyDown), false);
|
||||
AddHandler(PointerPressedEvent, new PointerEventHandler(ShellPage_OnPointerPressed), true);
|
||||
|
||||
RootFrame.Navigate(typeof(LoadingPage), ViewModel);
|
||||
RootFrame.Navigate(typeof(LoadingPage), new AsyncNavigationRequest(ViewModel, CancellationToken.None));
|
||||
|
||||
var pageAnnouncementFormat = ResourceLoaderInstance.GetString("ScreenReader_Announcement_NavigatedToPage0");
|
||||
_pageNavigatedAnnouncement = CompositeFormat.Parse(pageAnnouncementFormat);
|
||||
@@ -153,7 +153,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
ContentPageViewModel => typeof(ContentPage),
|
||||
_ => throw new NotSupportedException(),
|
||||
},
|
||||
message.Page,
|
||||
new AsyncNavigationRequest(message.Page, message.CancellationToken),
|
||||
message.WithAnimation ? DefaultPageAnimation : _noAnimation);
|
||||
|
||||
PowerToysTelemetry.Log.WriteEvent(new OpenPage(RootFrame.BackStackDepth));
|
||||
@@ -403,6 +403,8 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
{
|
||||
HideDetails();
|
||||
|
||||
ViewModel.CancelNavigation();
|
||||
|
||||
// Note: That we restore the VM state below in RootFrame_Navigated call back after this occurs.
|
||||
// In the future, we may want to manage the back stack ourselves vs. relying on Frame
|
||||
// We could replace Frame with a ContentPresenter, but then have to manage transition animations ourselves.
|
||||
@@ -456,11 +458,32 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
// This listens to the root frame to ensure that we also track the content's page VM as well that we passed as a parameter.
|
||||
// This is currently used for both forward and backward navigation.
|
||||
// As when we go back that we restore ourselves to the proper state within our VM
|
||||
if (e.Parameter is PageViewModel page)
|
||||
if (e.Parameter is AsyncNavigationRequest request)
|
||||
{
|
||||
// Note, this shortcuts and fights a bit with our LoadPageViewModel above, but we want to better fast display and incrementally load anyway
|
||||
// We just need to reconcile our loading systems a bit more in the future.
|
||||
ViewModel.CurrentPage = page;
|
||||
if (request.NavigationToken.IsCancellationRequested && e.NavigationMode is not (Microsoft.UI.Xaml.Navigation.NavigationMode.Back or Microsoft.UI.Xaml.Navigation.NavigationMode.Forward))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
switch (request.TargetViewModel)
|
||||
{
|
||||
case PageViewModel pageViewModel:
|
||||
ViewModel.CurrentPage = pageViewModel;
|
||||
break;
|
||||
case ShellViewModel:
|
||||
// This one is an exception, for now (LoadingPage is tied to ShellViewModel,
|
||||
// but ShellViewModel is not PageViewModel.
|
||||
ViewModel.CurrentPage = ViewModel.NullPage;
|
||||
break;
|
||||
default:
|
||||
ViewModel.CurrentPage = ViewModel.NullPage;
|
||||
Logger.LogWarning($"Invalid navigation target: AsyncNavigationRequest.{nameof(AsyncNavigationRequest.TargetViewModel)} must be {nameof(PageViewModel)}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning("Unrecognized target for shell navigation: " + e.Parameter);
|
||||
}
|
||||
|
||||
if (e.Content is Page element)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<!-- Look at Directory.Build.props in root for common stuff as well -->
|
||||
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<RootNamespace>Microsoft.CmdPal.UI.ViewModels.UnitTests</RootNamespace>
|
||||
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal\tests\</OutputPath>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="MSTest" />
|
||||
<PackageReference Include="WyHash" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Microsoft.CmdPal.UI.ViewModels\Microsoft.CmdPal.UI.ViewModels.csproj" />
|
||||
<ProjectReference Include="..\Microsoft.CmdPal.Ext.UnitTestsBase\Microsoft.CmdPal.Ext.UnitTestBase.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,444 @@
|
||||
// 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.Linq;
|
||||
using Microsoft.CmdPal.Ext.UnitTestBase;
|
||||
using Microsoft.CmdPal.UI.ViewModels.MainPage;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Windows.Foundation;
|
||||
using WyHash;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.UnitTests;
|
||||
|
||||
[TestClass]
|
||||
public partial class RecentCommandsTests : CommandPaletteUnitTestBase
|
||||
{
|
||||
private static RecentCommandsManager CreateHistory(IList<string>? commandIds = null)
|
||||
{
|
||||
var history = new RecentCommandsManager();
|
||||
if (commandIds != null)
|
||||
{
|
||||
foreach (var item in commandIds)
|
||||
{
|
||||
history.AddHistoryItem(item);
|
||||
}
|
||||
}
|
||||
|
||||
return history;
|
||||
}
|
||||
|
||||
private static RecentCommandsManager CreateBasicHistoryService()
|
||||
{
|
||||
var commonCommands = new List<string>
|
||||
{
|
||||
"com.microsoft.cmdpal.shell",
|
||||
"com.microsoft.cmdpal.windowwalker",
|
||||
"Visual Studio 2022 Preview_6533433915015224980",
|
||||
"com.microsoft.cmdpal.reload",
|
||||
"com.microsoft.cmdpal.shell",
|
||||
};
|
||||
|
||||
return CreateHistory(commonCommands);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateHistoryFunctionality()
|
||||
{
|
||||
// Setup
|
||||
var history = CreateHistory();
|
||||
|
||||
// Act
|
||||
history.AddHistoryItem("com.microsoft.cmdpal.shell");
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(history.GetCommandHistoryWeight("com.microsoft.cmdpal.shell") > 0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateHistoryWeighting()
|
||||
{
|
||||
// Setup
|
||||
var history = CreateBasicHistoryService();
|
||||
|
||||
// Act
|
||||
var shellWeight = history.GetCommandHistoryWeight("com.microsoft.cmdpal.shell");
|
||||
var windowWalkerWeight = history.GetCommandHistoryWeight("com.microsoft.cmdpal.windowwalker");
|
||||
var vsWeight = history.GetCommandHistoryWeight("Visual Studio 2022 Preview_6533433915015224980");
|
||||
var reloadWeight = history.GetCommandHistoryWeight("com.microsoft.cmdpal.reload");
|
||||
var nonExistentWeight = history.GetCommandHistoryWeight("non.existent.command");
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(shellWeight > windowWalkerWeight, "Shell should be weighted higher than Window Walker, more uses");
|
||||
Assert.IsTrue(vsWeight > windowWalkerWeight, "Visual Studio should be weighted higher than Window Walker, because recency");
|
||||
Assert.AreEqual(reloadWeight, vsWeight, "both reload and VS were used in the last three commands, same weight");
|
||||
Assert.IsTrue(shellWeight > vsWeight, "VS and run were both used in the last 3, but shell has 2 more frequency");
|
||||
Assert.AreEqual(0, nonExistentWeight, "Nonexistent command should have zero weight");
|
||||
}
|
||||
|
||||
private sealed partial record ListItemMock(
|
||||
string Title,
|
||||
string? Subtitle = "",
|
||||
string? GivenId = "",
|
||||
string? ProviderId = "") : IListItem
|
||||
{
|
||||
public string Id => string.IsNullOrEmpty(GivenId) ? GenerateId() : GivenId;
|
||||
|
||||
public IDetails Details => throw new System.NotImplementedException();
|
||||
|
||||
public string Section => throw new System.NotImplementedException();
|
||||
|
||||
public ITag[] Tags => throw new System.NotImplementedException();
|
||||
|
||||
public string TextToSuggest => throw new System.NotImplementedException();
|
||||
|
||||
public ICommand Command => new NoOpCommand() { Id = Id };
|
||||
|
||||
public IIconInfo Icon => throw new System.NotImplementedException();
|
||||
|
||||
public IContextItem[] MoreCommands => throw new System.NotImplementedException();
|
||||
|
||||
#pragma warning disable CS0067
|
||||
public event TypedEventHandler<object, IPropChangedEventArgs>? PropChanged;
|
||||
#pragma warning restore CS0067
|
||||
|
||||
private string GenerateId()
|
||||
{
|
||||
// Use WyHash64 to generate stable ID hashes.
|
||||
// manually seeding with 0, so that the hash is stable across launches
|
||||
var result = WyHash64.ComputeHash64(ProviderId + Title + Subtitle, seed: 0);
|
||||
return $"{ProviderId}{result}";
|
||||
}
|
||||
}
|
||||
|
||||
private static RecentCommandsManager CreateHistory(IList<ListItemMock> items)
|
||||
{
|
||||
var history = new RecentCommandsManager();
|
||||
foreach (var item in items)
|
||||
{
|
||||
history.AddHistoryItem(item.Id);
|
||||
}
|
||||
|
||||
return history;
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateMocksWork()
|
||||
{
|
||||
// Setup
|
||||
var items = new List<ListItemMock>
|
||||
{
|
||||
new("Command A", "Subtitle A", "idA", "providerA"),
|
||||
new("Command B", "Subtitle B", GivenId: "idB"),
|
||||
new("Command C", "Subtitle C", ProviderId: "providerC"),
|
||||
new("Command A", "Subtitle A", "idA", "providerA"), // Duplicate to test incrementing uses
|
||||
};
|
||||
|
||||
// Act
|
||||
var history = CreateHistory(items);
|
||||
|
||||
// Assert
|
||||
foreach (var item in items)
|
||||
{
|
||||
var weight = history.GetCommandHistoryWeight(item.Id);
|
||||
Assert.IsTrue(weight > 0, $"Item {item.Title} should have a weight greater than zero.");
|
||||
}
|
||||
|
||||
// Check that the duplicate item has a higher weight due to increased uses
|
||||
var weightA = history.GetCommandHistoryWeight("idA");
|
||||
var weightB = history.GetCommandHistoryWeight("idB");
|
||||
var weightC = history.GetCommandHistoryWeight(items[2].Id); // providerC generated ID
|
||||
Assert.IsTrue(weightA > weightB, "Item A should have a higher weight than Item B due to more uses.");
|
||||
Assert.IsTrue(weightA > weightC, "Item A should have a higher weight than Item C due to more uses.");
|
||||
Assert.AreEqual(weightC, weightB, "Item C and Item B were used in the last 3 commands");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateHistoryBuckets()
|
||||
{
|
||||
// Setup
|
||||
// (these will be checked in reverse order, so that A is the most recent)
|
||||
var items = new List<ListItemMock>
|
||||
{
|
||||
new("Command A", "Subtitle A", GivenId: "idA"), // #0 -> bucket 0
|
||||
new("Command B", "Subtitle B", GivenId: "idB"), // #1 -> bucket 0
|
||||
new("Command C", "Subtitle C", GivenId: "idC"), // #2 -> bucket 0
|
||||
new("Command D", "Subtitle D", GivenId: "idD"), // #3 -> bucket 1
|
||||
new("Command E", "Subtitle E", GivenId: "idE"), // #4 -> bucket 1
|
||||
new("Command F", "Subtitle F", GivenId: "idF"), // #5 -> bucket 1
|
||||
new("Command G", "Subtitle G", GivenId: "idG"), // #6 -> bucket 1
|
||||
new("Command H", "Subtitle H", GivenId: "idH"), // #7 -> bucket 1
|
||||
new("Command I", "Subtitle I", GivenId: "idI"), // #8 -> bucket 1
|
||||
new("Command J", "Subtitle J", GivenId: "idJ"), // #9 -> bucket 1
|
||||
new("Command K", "Subtitle K", GivenId: "idK"), // #10 -> bucket 1
|
||||
new("Command L", "Subtitle L", GivenId: "idL"), // #11 -> bucket 2
|
||||
new("Command M", "Subtitle M", GivenId: "idM"), // #12 -> bucket 2
|
||||
new("Command N", "Subtitle N", GivenId: "idN"), // #13 -> bucket 2
|
||||
new("Command O", "Subtitle O", GivenId: "idO"), // #14 -> bucket 2
|
||||
};
|
||||
|
||||
for (var i = items.Count; i <= 50; i++)
|
||||
{
|
||||
items.Add(new ListItemMock($"Command #{i}", GivenId: $"id{i}"));
|
||||
}
|
||||
|
||||
// Act
|
||||
var history = CreateHistory(items.Reverse<ListItemMock>().ToList());
|
||||
|
||||
// Assert
|
||||
// First three items should be in the top bucket
|
||||
var weightA = history.GetCommandHistoryWeight("idA");
|
||||
var weightB = history.GetCommandHistoryWeight("idB");
|
||||
var weightC = history.GetCommandHistoryWeight("idC");
|
||||
|
||||
Assert.AreEqual(weightA, weightB, "Items A and B were used in the last 3 commands");
|
||||
Assert.AreEqual(weightB, weightC, "Items B and C were used in the last 3 commands");
|
||||
|
||||
// Next eight items (3-10 inclusive) should be in the second bucket
|
||||
var weightD = history.GetCommandHistoryWeight("idD");
|
||||
var weightE = history.GetCommandHistoryWeight("idE");
|
||||
var weightF = history.GetCommandHistoryWeight("idF");
|
||||
var weightG = history.GetCommandHistoryWeight("idG");
|
||||
var weightH = history.GetCommandHistoryWeight("idH");
|
||||
var weightI = history.GetCommandHistoryWeight("idI");
|
||||
var weightJ = history.GetCommandHistoryWeight("idJ");
|
||||
var weightK = history.GetCommandHistoryWeight("idK");
|
||||
|
||||
Assert.AreEqual(weightD, weightE, "Items D and E were used in the last 10 commands");
|
||||
Assert.AreEqual(weightE, weightF, "Items E and F were used in the last 10 commands");
|
||||
Assert.AreEqual(weightF, weightG, "Items F and G were used in the last 10 commands");
|
||||
Assert.AreEqual(weightG, weightH, "Items G and H were used in the last 10 commands");
|
||||
Assert.AreEqual(weightH, weightI, "Items H and I were used in the last 10 commands");
|
||||
Assert.AreEqual(weightI, weightJ, "Items I and J were used in the last 10 commands");
|
||||
Assert.AreEqual(weightJ, weightK, "Items J and K were used in the last 10 commands");
|
||||
|
||||
// Items up to the 15th should be in the third bucket
|
||||
var weightL = history.GetCommandHistoryWeight("idL");
|
||||
var weightM = history.GetCommandHistoryWeight("idM");
|
||||
var weightN = history.GetCommandHistoryWeight("idN");
|
||||
var weightO = history.GetCommandHistoryWeight("idO");
|
||||
var weight15 = history.GetCommandHistoryWeight("id15");
|
||||
Assert.AreEqual(weightL, weightM, "Items L and M were used in the last 15 commands");
|
||||
Assert.AreEqual(weightM, weightN, "Items M and N were used in the last 15 commands");
|
||||
Assert.AreEqual(weightN, weightO, "Items N and O were used in the last 15 commands");
|
||||
Assert.AreEqual(weightO, weight15, "Items O and 15 were used in the last 15 commands");
|
||||
|
||||
// Items after that should be in the lowest buckets
|
||||
var weight0 = history.GetCommandHistoryWeight(items[0].Id);
|
||||
var weight3 = history.GetCommandHistoryWeight(items[3].Id);
|
||||
var weight11 = history.GetCommandHistoryWeight(items[11].Id);
|
||||
var weight16 = history.GetCommandHistoryWeight("id16");
|
||||
var weight20 = history.GetCommandHistoryWeight("id20");
|
||||
var weight30 = history.GetCommandHistoryWeight("id30");
|
||||
var weight40 = history.GetCommandHistoryWeight("id40");
|
||||
var weight49 = history.GetCommandHistoryWeight("id49");
|
||||
|
||||
Assert.IsTrue(weight0 > weight3);
|
||||
Assert.IsTrue(weight3 > weight11);
|
||||
Assert.IsTrue(weight11 > weight16);
|
||||
|
||||
Assert.AreEqual(weight16, weight20);
|
||||
Assert.AreEqual(weight20, weight30);
|
||||
Assert.IsTrue(weight30 > weight40);
|
||||
Assert.AreEqual(weight40, weight49);
|
||||
|
||||
// The 50th item has fallen out of the list now
|
||||
var weight50 = history.GetCommandHistoryWeight("id50");
|
||||
Assert.AreEqual(0, weight50, "Item 50 should have fallen out of the history list");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateSimpleScoring()
|
||||
{
|
||||
// Setup
|
||||
var items = new List<ListItemMock>
|
||||
{
|
||||
new("Command A", "Subtitle A", GivenId: "idA"), // #0 -> bucket 0
|
||||
new("Command B", "Subtitle B", GivenId: "idB"), // #1 -> bucket 0
|
||||
new("Command C", "Subtitle C", GivenId: "idC"), // #2 -> bucket 0
|
||||
};
|
||||
|
||||
var history = CreateHistory(items.Reverse<ListItemMock>().ToList());
|
||||
|
||||
var scoreA = MainListPage.ScoreTopLevelItem("C", items[0], history);
|
||||
var scoreB = MainListPage.ScoreTopLevelItem("C", items[1], history);
|
||||
var scoreC = MainListPage.ScoreTopLevelItem("C", items[2], history);
|
||||
|
||||
// Assert
|
||||
// All of these equally match the query, and they're all in the same bucket,
|
||||
// so they should all have the same score.
|
||||
Assert.AreEqual(scoreA, scoreB, "Items A and B should have the same score");
|
||||
Assert.AreEqual(scoreB, scoreC, "Items B and C should have the same score");
|
||||
}
|
||||
|
||||
private static List<ListItemMock> CreateMockHistoryItems()
|
||||
{
|
||||
var items = new List<ListItemMock>
|
||||
{
|
||||
new("Visual Studio 2022"), // #0 -> bucket 0
|
||||
new("Visual Studio Code"), // #1 -> bucket 0
|
||||
new("Explore Mastodon", GivenId: "social.mastodon.explore"), // #2 -> bucket 0
|
||||
new("Run commands", Subtitle: "Executes commands (e.g. ping, cmd)", GivenId: "com.microsoft.cmdpal.run"), // #3 -> bucket 1
|
||||
new("Windows Settings"), // #4 -> bucket 1
|
||||
new("Command Prompt"), // #5 -> bucket 1
|
||||
new("Terminal Canary"), // #6 -> bucket 1
|
||||
};
|
||||
return items;
|
||||
}
|
||||
|
||||
private static RecentCommandsManager CreateMockHistoryService(List<ListItemMock>? items = null)
|
||||
{
|
||||
var history = CreateHistory((items ?? CreateMockHistoryItems()).Reverse<ListItemMock>().ToList());
|
||||
return history;
|
||||
}
|
||||
|
||||
private sealed record ScoredItem(ListItemMock Item, int Score)
|
||||
{
|
||||
public string Title => Item.Title;
|
||||
|
||||
public override string ToString() => $"[{Score}]{Title}";
|
||||
}
|
||||
|
||||
private static IEnumerable<ScoredItem> TieScoresToMatches(List<ListItemMock> items, List<int> scores)
|
||||
{
|
||||
if (items.Count != scores.Count)
|
||||
{
|
||||
throw new ArgumentException("Items and scores must have the same number of elements");
|
||||
}
|
||||
|
||||
for (var i = 0; i < items.Count; i++)
|
||||
{
|
||||
yield return new ScoredItem(items[i], scores[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<ScoredItem> GetMatches(IEnumerable<ScoredItem> scoredItems)
|
||||
{
|
||||
var matches = scoredItems
|
||||
.Where(x => x.Score > 0)
|
||||
.OrderByDescending(x => x.Score)
|
||||
.ToList();
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
private static IEnumerable<ScoredItem> GetMatches(List<ListItemMock> items, List<int> scores)
|
||||
{
|
||||
return GetMatches(TieScoresToMatches(items, scores));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateScoredWeightingSimple()
|
||||
{
|
||||
var items = CreateMockHistoryItems();
|
||||
var emptyHistory = CreateMockHistoryService(new());
|
||||
var history = CreateMockHistoryService(items);
|
||||
|
||||
var unweightedScores = items.Select(item => MainListPage.ScoreTopLevelItem("C", item, emptyHistory)).ToList();
|
||||
var weightedScores = items.Select(item => MainListPage.ScoreTopLevelItem("C", item, history)).ToList();
|
||||
Assert.AreEqual(unweightedScores.Count, weightedScores.Count, "Both score lists should have the same number of items");
|
||||
for (var i = 0; i < unweightedScores.Count; i++)
|
||||
{
|
||||
var unweighted = unweightedScores[i];
|
||||
var weighted = weightedScores[i];
|
||||
var item = items[i];
|
||||
if (item.Title.Contains('C', System.StringComparison.CurrentCultureIgnoreCase))
|
||||
{
|
||||
Assert.IsTrue(unweighted >= 0, $"Item {item.Title} didn't match the query, so should have a weighted score of zero");
|
||||
Assert.IsTrue(weighted > unweighted, $"Item {item.Title} should have a higher weighted ({weighted}) score than unweighted ({unweighted})");
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.AreEqual(unweighted, 0, $"Item {item.Title} didn't match the query, so should have a weighted score of zero");
|
||||
Assert.AreEqual(unweighted, weighted);
|
||||
}
|
||||
}
|
||||
|
||||
var unweightedMatches = GetMatches(items, unweightedScores).ToList();
|
||||
Assert.AreEqual(4, unweightedMatches.Count);
|
||||
Assert.AreEqual("Command Prompt", unweightedMatches[0].Title, "Command Prompt should be the top match");
|
||||
Assert.AreEqual("Visual Studio Code", unweightedMatches[1].Title, "Visual Studio Code should be the second match");
|
||||
Assert.AreEqual("Terminal Canary", unweightedMatches[2].Title);
|
||||
Assert.AreEqual("Run commands", unweightedMatches[3].Title);
|
||||
|
||||
// Even after weighting for 1 use, Command Prompt should still be the top match.
|
||||
var weightedMatches = GetMatches(items, weightedScores).ToList();
|
||||
Assert.AreEqual(4, weightedMatches.Count);
|
||||
Assert.AreEqual("Command Prompt", weightedMatches[0].Title);
|
||||
Assert.AreEqual("Visual Studio Code", weightedMatches[1].Title);
|
||||
Assert.AreEqual("Terminal Canary", weightedMatches[2].Title);
|
||||
Assert.AreEqual("Run commands", weightedMatches[3].Title);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateTitlesAreMoreImportantThanHistory()
|
||||
{
|
||||
var items = CreateMockHistoryItems();
|
||||
var emptyHistory = CreateMockHistoryService(new());
|
||||
var history = CreateMockHistoryService(items);
|
||||
var weightedScores = items.Select(item => MainListPage.ScoreTopLevelItem("te", item, history)).ToList();
|
||||
var weightedMatches = GetMatches(items, weightedScores).ToList();
|
||||
|
||||
Assert.AreEqual(3, weightedMatches.Count, "Find Terminal, VsCode and Run commands");
|
||||
|
||||
// Terminal is in bucket 1, VS Code is in bucket 0, but Terminal matches
|
||||
// the title better
|
||||
Assert.AreEqual("Terminal Canary", weightedMatches[0].Title, "Terminal should be the top match, title match");
|
||||
Assert.AreEqual("Visual Studio Code", weightedMatches[1].Title, "VsCode does fuzzy match, but is less relevant than Terminal");
|
||||
Assert.AreEqual("Run commands", weightedMatches[2].Title, "run only matches on the subtitle");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateTitlesAreMoreImportantThanUsage()
|
||||
{
|
||||
var items = CreateMockHistoryItems();
|
||||
var emptyHistory = CreateMockHistoryService(new());
|
||||
var history = CreateMockHistoryService(items);
|
||||
|
||||
// Add extra uses of VS Code to try and push it above Terminal
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
history.AddHistoryItem(items[1].Id);
|
||||
}
|
||||
|
||||
var weightedScores = items.Select(item => MainListPage.ScoreTopLevelItem("te", item, history)).ToList();
|
||||
var weightedMatches = GetMatches(items, weightedScores).ToList();
|
||||
|
||||
Assert.AreEqual(3, weightedMatches.Count, "Find Terminal, VsCode and Run commands");
|
||||
|
||||
// Terminal is in bucket 1, VS Code is in bucket 0, but Terminal matches
|
||||
// the title better
|
||||
Assert.AreEqual("Terminal Canary", weightedMatches[0].Title, "Terminal should be the top match, title match");
|
||||
Assert.AreEqual("Visual Studio Code", weightedMatches[1].Title, "VsCode does fuzzy match, but is less relevant than Terminal");
|
||||
Assert.AreEqual("Run commands", weightedMatches[2].Title, "run only matches on the subtitle");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateUsageEventuallyHelps()
|
||||
{
|
||||
var items = CreateMockHistoryItems();
|
||||
var emptyHistory = CreateMockHistoryService(new());
|
||||
var history = CreateMockHistoryService(items);
|
||||
|
||||
// We're gonna run this test and keep adding more uses of VS Code till
|
||||
// it breaks past Command Prompt
|
||||
var vsCodeId = items[1].Id;
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
history.AddHistoryItem(vsCodeId);
|
||||
|
||||
var weightedScores = items.Select(item => MainListPage.ScoreTopLevelItem("C", item, history)).ToList();
|
||||
var weightedMatches = GetMatches(items, weightedScores).ToList();
|
||||
Assert.AreEqual(4, weightedMatches.Count);
|
||||
|
||||
var expectedCmdIndex = i < 5 ? 0 : 1;
|
||||
var expectedCodeIndex = i < 5 ? 1 : 0;
|
||||
Assert.AreEqual("Command Prompt", weightedMatches[expectedCmdIndex].Title);
|
||||
Assert.AreEqual("Visual Studio Code", weightedMatches[expectedCodeIndex].Title);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Apps.Programs;
|
||||
|
||||
internal sealed partial class AppListItem : ListItem
|
||||
public sealed partial class AppListItem : ListItem
|
||||
{
|
||||
private static readonly Tag _appTag = new("App");
|
||||
|
||||
|
||||
@@ -103,7 +103,8 @@ public class UWPApplication : IUWPApplication
|
||||
new CommandContextItem(
|
||||
new OpenFileCommand(Location)
|
||||
{
|
||||
Name = Resources.open_containing_folder,
|
||||
Icon = new("\uE838"),
|
||||
Name = Resources.open_location,
|
||||
})
|
||||
{
|
||||
RequestedShortcut = KeyChords.OpenFileLocation,
|
||||
|
||||
@@ -207,7 +207,10 @@ public class Win32Program : IProgram
|
||||
});
|
||||
|
||||
commands.Add(new CommandContextItem(
|
||||
new OpenFileCommand(ParentDirectory))
|
||||
new ShowFileInFolderCommand(!string.IsNullOrEmpty(LnkFilePath) ? LnkFilePath : FullPath)
|
||||
{
|
||||
Name = Resources.open_location,
|
||||
})
|
||||
{
|
||||
RequestedShortcut = KeyChords.OpenFileLocation,
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace Microsoft.CmdPal.Ext.Apps.Properties {
|
||||
// class via a tool like ResGen or Visual Studio.
|
||||
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||
// with the /str option, or rebuild your VS project.
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
internal class Resources {
|
||||
@@ -241,7 +241,7 @@ namespace Microsoft.CmdPal.Ext.Apps.Properties {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Open location.
|
||||
/// Looks up a localized string similar to Open file location.
|
||||
/// </summary>
|
||||
internal static string open_location {
|
||||
get {
|
||||
|
||||
@@ -161,7 +161,7 @@
|
||||
<value>File</value>
|
||||
</data>
|
||||
<data name="open_location" xml:space="preserve">
|
||||
<value>Open location</value>
|
||||
<value>Open file location</value>
|
||||
</data>
|
||||
<data name="copy_path" xml:space="preserve">
|
||||
<value>Copy path</value>
|
||||
@@ -237,4 +237,4 @@
|
||||
<data name="limit_none" xml:space="preserve">
|
||||
<value>Unlimited</value>
|
||||
</data>
|
||||
</root>
|
||||
</root>
|
||||
@@ -16,7 +16,6 @@ internal sealed partial class FallbackExecuteItem : FallbackCommandItem, IDispos
|
||||
private readonly Action<string>? _addToHistory;
|
||||
private readonly ITelemetryService _telemetryService;
|
||||
private CancellationTokenSource? _cancellationTokenSource;
|
||||
private Task? _currentUpdateTask;
|
||||
|
||||
public FallbackExecuteItem(SettingsManager settings, Action<string>? addToHistory, ITelemetryService telemetryService)
|
||||
: base(
|
||||
@@ -40,44 +39,22 @@ internal sealed partial class FallbackExecuteItem : FallbackCommandItem, IDispos
|
||||
|
||||
try
|
||||
{
|
||||
// Save the latest update task
|
||||
_currentUpdateTask = DoUpdateQueryAsync(query, cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// DO NOTHING HERE
|
||||
return;
|
||||
DoUpdateQuery(query, cancellationToken);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Handle other exceptions
|
||||
return;
|
||||
}
|
||||
|
||||
// Await the task to ensure only the latest one gets processed
|
||||
_ = ProcessUpdateResultsAsync(_currentUpdateTask);
|
||||
}
|
||||
|
||||
private async Task ProcessUpdateResultsAsync(Task updateTask)
|
||||
{
|
||||
try
|
||||
{
|
||||
await updateTask;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Handle cancellation gracefully
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Handle other exceptions
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DoUpdateQueryAsync(string query, CancellationToken cancellationToken)
|
||||
private void DoUpdateQuery(string query, CancellationToken cancellationToken)
|
||||
{
|
||||
// Check for cancellation at the start
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var searchText = query.Trim();
|
||||
Expand(ref searchText);
|
||||
@@ -105,22 +82,8 @@ internal sealed partial class FallbackExecuteItem : FallbackCommandItem, IDispos
|
||||
using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
|
||||
var timeoutToken = combinedCts.Token;
|
||||
|
||||
// Use Task.Run with timeout for file system operations
|
||||
var fileSystemTask = Task.Run(
|
||||
() =>
|
||||
{
|
||||
exeExists = ShellListPageHelpers.FileExistInPath(exe, out fullExePath);
|
||||
pathIsDir = Directory.Exists(exe);
|
||||
},
|
||||
CancellationToken.None);
|
||||
|
||||
// Wait for either completion or timeout
|
||||
await fileSystemTask.WaitAsync(timeoutToken);
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
// Main cancellation token was cancelled, re-throw
|
||||
throw;
|
||||
exeExists = ShellListPageHelpers.FileExistInPath(exe, out fullExePath, cancellationToken);
|
||||
pathIsDir = Directory.Exists(exe);
|
||||
}
|
||||
catch (TimeoutException)
|
||||
{
|
||||
@@ -139,7 +102,10 @@ internal sealed partial class FallbackExecuteItem : FallbackCommandItem, IDispos
|
||||
}
|
||||
|
||||
// Check for cancellation before updating UI properties
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (exeExists)
|
||||
{
|
||||
@@ -172,7 +138,10 @@ internal sealed partial class FallbackExecuteItem : FallbackCommandItem, IDispos
|
||||
}
|
||||
|
||||
// Final cancellation check
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Threading;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace SamplePagesExtension;
|
||||
|
||||
internal sealed partial class SlowListPage : ListPage
|
||||
{
|
||||
public SlowListPage()
|
||||
{
|
||||
Icon = new IconInfo("\uEA79");
|
||||
Name = "Slow List Page";
|
||||
Title = "This page simulates a slow load";
|
||||
}
|
||||
|
||||
public override IListItem[] GetItems()
|
||||
{
|
||||
Thread.Sleep(5000);
|
||||
|
||||
return [
|
||||
new ListItem(new NoOpCommand())
|
||||
{
|
||||
Title = "This is a basic item in the list",
|
||||
Subtitle = "I don't do anything though",
|
||||
},
|
||||
new ListItem(new NoOpCommand())
|
||||
{
|
||||
Title = "This is another item in the list",
|
||||
Subtitle = "Still nothing",
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,11 @@ public partial class SamplesListPage : ListPage
|
||||
Title = "Sample Icon Page",
|
||||
Subtitle = "A demo of using icons in various ways",
|
||||
},
|
||||
new ListItem(new SlowListPage())
|
||||
{
|
||||
Title = "Slow loading list page",
|
||||
Subtitle = "A demo of a list page that takes a while to load",
|
||||
},
|
||||
|
||||
// Content pages
|
||||
new ListItem(new SampleContentPage())
|
||||
|
||||
@@ -58,8 +58,8 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
IncludeWinKey = new BoolProperty(false);
|
||||
ActivationShortcut = DefaultActivationShortcut;
|
||||
DoNotActivateOnGameMode = new BoolProperty(true);
|
||||
BackgroundColor = new StringProperty("#FF000000"); // ARGB (#AARRGGBB)
|
||||
SpotlightColor = new StringProperty("#FFFFFFFF");
|
||||
BackgroundColor = new StringProperty("#80000000"); // ARGB (#AARRGGBB)
|
||||
SpotlightColor = new StringProperty("#80FFFFFF");
|
||||
SpotlightRadius = new IntProperty(100);
|
||||
AnimationDurationMs = new IntProperty(500);
|
||||
SpotlightInitialZoom = new IntProperty(9);
|
||||
|
||||
@@ -81,6 +81,8 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
[JsonPropertyName("AnimnateZoom")]
|
||||
public BoolProperty AnimateZoom { get; set; }
|
||||
|
||||
public BoolProperty SmoothImage { get; set; }
|
||||
|
||||
public IntProperty ZoominSliderLevel { get; set; }
|
||||
|
||||
public IntProperty RecordScaling { get; set; }
|
||||
|
||||
@@ -54,6 +54,9 @@
|
||||
<tkcontrols:SettingsCard Name="ZoomItToggleAnimateZoom" x:Uid="ZoomIt_Toggle_AnimateZoom">
|
||||
<ToggleSwitch x:Uid="ToggleSwitch" IsOn="{x:Bind ViewModel.AnimateZoom, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard Name="ZoomItSmoothZoomedImage" x:Uid="ZoomIt_Toggle_SmoothZoomedImage">
|
||||
<ToggleSwitch x:Uid="ToggleSwitch" IsOn="{x:Bind ViewModel.SmoothImage, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard Name="ZoomItSliderInitialMagnification" x:Uid="ZoomIt_Slider_InitialMagnification">
|
||||
<Slider
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
|
||||
@@ -2893,7 +2893,7 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<value>Crosshairs fixed length (px)</value>
|
||||
<comment>px = pixels</comment>
|
||||
</data>
|
||||
<data name="MouseUtils_MousePointerCrosshairs_CrosshairsOrientation" xml:space="preserve">
|
||||
<data name="MouseUtils_MousePointerCrosshairs_CrosshairsOrientation.Header" xml:space="preserve">
|
||||
<value>Crosshairs orientation</value>
|
||||
</data>
|
||||
<data name="MouseUtils_MousePointerCrosshairs_CrosshairsOrientation_Both.Content" xml:space="preserve">
|
||||
@@ -4662,6 +4662,9 @@ Copy a zoomed screen with Ctrl+C or save it by typing Ctrl+S. Crop the copy or s
|
||||
<data name="ZoomIt_Toggle_AnimateZoom.Header" xml:space="preserve">
|
||||
<value>Animate zoom in and zoom out</value>
|
||||
</data>
|
||||
<data name="ZoomIt_Toggle_SmoothZoomedImage.Header" xml:space="preserve">
|
||||
<value>Smooth zoomed image</value>
|
||||
</data>
|
||||
<data name="ZoomIt_Slider_InitialMagnification.Header" xml:space="preserve">
|
||||
<value>Specify the initial level of magnification when zooming in</value>
|
||||
</data>
|
||||
|
||||
@@ -50,10 +50,10 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
_findMyMouseDoNotActivateOnGameMode = FindMyMouseSettingsConfig.Properties.DoNotActivateOnGameMode.Value;
|
||||
|
||||
string backgroundColor = FindMyMouseSettingsConfig.Properties.BackgroundColor.Value;
|
||||
_findMyMouseBackgroundColor = !string.IsNullOrEmpty(backgroundColor) ? backgroundColor : "#000000";
|
||||
_findMyMouseBackgroundColor = !string.IsNullOrEmpty(backgroundColor) ? backgroundColor : "#80000000";
|
||||
|
||||
string spotlightColor = FindMyMouseSettingsConfig.Properties.SpotlightColor.Value;
|
||||
_findMyMouseSpotlightColor = !string.IsNullOrEmpty(spotlightColor) ? spotlightColor : "#FFFFFF";
|
||||
_findMyMouseSpotlightColor = !string.IsNullOrEmpty(spotlightColor) ? spotlightColor : "#80FFFFFF";
|
||||
|
||||
_findMyMouseSpotlightRadius = FindMyMouseSettingsConfig.Properties.SpotlightRadius.Value;
|
||||
_findMyMouseAnimationDurationMs = FindMyMouseSettingsConfig.Properties.AnimationDurationMs.Value;
|
||||
|
||||
@@ -197,6 +197,20 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
public bool SmoothImage
|
||||
{
|
||||
get => _zoomItSettings.Properties.SmoothImage.Value;
|
||||
set
|
||||
{
|
||||
if (_zoomItSettings.Properties.SmoothImage.Value != value)
|
||||
{
|
||||
_zoomItSettings.Properties.SmoothImage.Value = value;
|
||||
OnPropertyChanged(nameof(SmoothImage));
|
||||
NotifySettingsChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int ZoominSliderLevel
|
||||
{
|
||||
get => _zoomItSettings.Properties.ZoominSliderLevel.Value;
|
||||
|
||||
4
tools/build/Delete-Worktree.cmd
Normal file
4
tools/build/Delete-Worktree.cmd
Normal file
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
setlocal
|
||||
set SCRIPT_DIR=%~dp0
|
||||
pwsh -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%Delete-Worktree.ps1" %*
|
||||
130
tools/build/Delete-Worktree.ps1
Normal file
130
tools/build/Delete-Worktree.ps1
Normal file
@@ -0,0 +1,130 @@
|
||||
<#!
|
||||
.SYNOPSIS
|
||||
Remove a git worktree (and optionally its local branch and orphan fork remote).
|
||||
|
||||
.DESCRIPTION
|
||||
Locates a worktree by branch/path pattern (supports wildcards). Ensures the primary repository
|
||||
root is never removed. Optionally discards local changes with -Force. Deletes associated branch
|
||||
unless -KeepBranch. If the branch tracked a non-origin remote with no remaining tracking
|
||||
branches, that remote is removed unless -KeepRemote.
|
||||
|
||||
.PARAMETER Pattern
|
||||
Branch name or path fragment (wildcards * ? allowed). If multiple matches found they are listed
|
||||
and no deletion occurs.
|
||||
|
||||
.PARAMETER Force
|
||||
Discard uncommitted changes and attempt aggressive cleanup on failure.
|
||||
|
||||
.PARAMETER KeepBranch
|
||||
Preserve the local branch (only remove the worktree directory entry).
|
||||
|
||||
.PARAMETER KeepRemote
|
||||
Preserve any orphan fork remote even if no branches still track it.
|
||||
|
||||
.EXAMPLE
|
||||
./Delete-Worktree.ps1 -Pattern feature/login
|
||||
|
||||
.EXAMPLE
|
||||
./Delete-Worktree.ps1 -Pattern fork-user-featureX -Force
|
||||
|
||||
.EXAMPLE
|
||||
./Delete-Worktree.ps1 -Pattern hotfix -KeepBranch
|
||||
|
||||
.NOTES
|
||||
Manual recovery:
|
||||
git worktree list --porcelain
|
||||
git worktree prune
|
||||
Remove-Item -LiteralPath <path> -Recurse -Force
|
||||
git branch -D <branch>
|
||||
git remote remove <remote>
|
||||
git worktree prune
|
||||
#>
|
||||
|
||||
param(
|
||||
[string] $Pattern,
|
||||
[switch] $Force,
|
||||
[switch] $KeepBranch,
|
||||
[switch] $KeepRemote,
|
||||
[switch] $Help
|
||||
)
|
||||
. "$PSScriptRoot/WorktreeLib.ps1"
|
||||
if ($Help -or -not $Pattern) { Show-FileEmbeddedHelp -ScriptPath $MyInvocation.MyCommand.Path; return }
|
||||
try {
|
||||
$repoRoot = Get-RepoRoot
|
||||
$entries = Get-WorktreeEntries
|
||||
if (-not $entries -or $entries.Count -eq 0) { throw 'No worktrees found.' }
|
||||
$hasWildcard = $Pattern -match '[\*\?]'
|
||||
$matchPattern = if ($hasWildcard) { $Pattern } else { "*${Pattern}*" }
|
||||
$found = $entries | Where-Object { $_.Branch -and ( $_.Branch -like $matchPattern -or $_.Path -like $matchPattern ) }
|
||||
if (-not $found -or $found.Count -eq 0) { throw "No worktree matches pattern '$Pattern'" }
|
||||
if ($found.Count -gt 1) {
|
||||
Warn 'Pattern matches multiple worktrees:'
|
||||
$found | ForEach-Object { Info (" {0} {1}" -f $_.Branch, $_.Path) }
|
||||
return
|
||||
}
|
||||
$target = $found | Select-Object -First 1
|
||||
$branch = $target.Branch
|
||||
$folder = $target.Path
|
||||
if (-not $branch) { throw 'Resolved worktree has no branch (detached); refusing removal.' }
|
||||
try { $folder = (Resolve-Path -LiteralPath $folder -ErrorAction Stop).ProviderPath } catch {}
|
||||
$primary = (Resolve-Path -LiteralPath $repoRoot).ProviderPath
|
||||
if ([IO.Path]::GetFullPath($folder).TrimEnd('\\/') -ieq [IO.Path]::GetFullPath($primary).TrimEnd('\\/')) { throw 'Refusing to remove the primary worktree (repository root).' }
|
||||
$status = git -C $folder status --porcelain 2>$null
|
||||
if ($LASTEXITCODE -ne 0) { throw "Unable to get git status for $folder" }
|
||||
if (-not $Force -and $status) { throw 'Worktree has uncommitted changes. Use -Force to discard.' }
|
||||
if ($Force -and $status) {
|
||||
Warn '[Force] Discarding local changes'
|
||||
git -C $folder reset --hard HEAD | Out-Null
|
||||
git -C $folder clean -fdx | Out-Null
|
||||
}
|
||||
if ($Force) { git worktree remove --force $folder } else { git worktree remove $folder }
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
$exit1 = $LASTEXITCODE
|
||||
$errMsg = "git worktree remove failed (exit $exit1)"
|
||||
if ($Force) {
|
||||
Warn 'Primary removal failed; performing aggressive fallback (Force implies brute).'
|
||||
try { git -C $folder submodule deinit -f --all 2>$null | Out-Null } catch {}
|
||||
try { git -C $folder clean -dfx 2>$null | Out-Null } catch {}
|
||||
try { Get-ChildItem -LiteralPath $folder -Recurse -Force -ErrorAction SilentlyContinue | ForEach-Object { try { $_.IsReadOnly = $false } catch {} } } catch {}
|
||||
if (Test-Path $folder) { try { Remove-Item -LiteralPath $folder -Recurse -Force -ErrorAction Stop } catch { Err "Manual directory removal failed: $($_.Exception.Message)" } }
|
||||
git worktree prune 2>$null | Out-Null
|
||||
if (Test-Path $folder) { throw "$errMsg and aggressive cleanup did not fully remove directory: $folder" } else { Info "Aggressive cleanup removed directory $folder." }
|
||||
} else {
|
||||
throw "$errMsg. Rerun with -Force to attempt aggressive cleanup."
|
||||
}
|
||||
}
|
||||
# Determine upstream before potentially deleting branch
|
||||
$upRemote = Get-BranchUpstreamRemote -Branch $branch
|
||||
$looksForkName = $branch -like 'fork-*'
|
||||
|
||||
if (-not $KeepBranch) {
|
||||
git branch -D $branch 2>$null | Out-Null
|
||||
if (-not $KeepRemote -and $upRemote -and $upRemote -ne 'origin') {
|
||||
$otherTracking = git for-each-ref --format='%(refname:short)|%(upstream:short)' refs/heads 2>$null |
|
||||
Where-Object { $_ -and ($_ -notmatch "^$branch\|") } |
|
||||
ForEach-Object { $parts = $_.Split('|',2); if ($parts[1] -match '^(?<r>[^/]+)/'){ $parts[0],$Matches.r } } |
|
||||
Where-Object { $_[1] -eq $upRemote }
|
||||
if (-not $otherTracking) {
|
||||
Warn "Removing orphan remote '$upRemote' (no more tracking branches)"
|
||||
git remote remove $upRemote 2>$null | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { Warn "Failed to remove remote '$upRemote' (you may remove manually)." }
|
||||
} else { Info "Remote '$upRemote' retained (other branches still track it)." }
|
||||
} elseif ($looksForkName -and -not $KeepRemote -and -not $upRemote) {
|
||||
Warn 'Branch looks like a fork branch (name pattern), but has no upstream remote; nothing to clean.'
|
||||
}
|
||||
}
|
||||
|
||||
Info "Removed worktree ($branch) at $folder."; if (-not $KeepBranch) { Info 'Branch deleted.' }
|
||||
Show-WorktreeExecutionSummary -CurrentBranch $branch
|
||||
} catch {
|
||||
Err "Error: $($_.Exception.Message)"
|
||||
Warn 'Manual cleanup guidelines:'
|
||||
Info ' git worktree list --porcelain'
|
||||
Info ' git worktree prune'
|
||||
Info ' # If still present:'
|
||||
Info ' Remove-Item -LiteralPath <path> -Recurse -Force'
|
||||
Info ' git branch -D <branch> (if you also want to drop local branch)'
|
||||
Info ' git remote remove <remote> (if orphan fork remote remains)'
|
||||
Info ' git worktree prune'
|
||||
exit 1
|
||||
}
|
||||
4
tools/build/New-WorktreeFromBranch.cmd
Normal file
4
tools/build/New-WorktreeFromBranch.cmd
Normal file
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
setlocal
|
||||
set SCRIPT_DIR=%~dp0
|
||||
pwsh -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%New-WorktreeFromBranch.ps1" %*
|
||||
78
tools/build/New-WorktreeFromBranch.ps1
Normal file
78
tools/build/New-WorktreeFromBranch.ps1
Normal file
@@ -0,0 +1,78 @@
|
||||
<#!
|
||||
.SYNOPSIS
|
||||
Create (or reuse) a worktree for an existing local or remote (origin) branch.
|
||||
|
||||
.DESCRIPTION
|
||||
Normalizes origin/<name> to <name>. If the branch does not exist locally (and -NoFetch is not
|
||||
provided) it will fetch and create a tracking branch from origin. Reuses any existing worktree
|
||||
bound to the branch; otherwise creates a new one adjacent to the repository root.
|
||||
|
||||
.PARAMETER Branch
|
||||
Branch name (local or origin/<name> form) to materialize as a worktree.
|
||||
|
||||
.PARAMETER VSCodeProfile
|
||||
VS Code profile to open (Default).
|
||||
|
||||
.PARAMETER NoFetch
|
||||
Skip fetch if branch missing locally; script will error instead of creating it.
|
||||
|
||||
.EXAMPLE
|
||||
./New-WorktreeFromBranch.ps1 -Branch feature/login
|
||||
|
||||
.EXAMPLE
|
||||
./New-WorktreeFromBranch.ps1 -Branch origin/bugfix/nullref
|
||||
|
||||
.EXAMPLE
|
||||
./New-WorktreeFromBranch.ps1 -Branch release/v1 -NoFetch
|
||||
|
||||
.NOTES
|
||||
Manual recovery:
|
||||
git fetch origin && git checkout <branch>
|
||||
git worktree add ../RepoName-XX <branch>
|
||||
code ../RepoName-XX --profile Default
|
||||
#>
|
||||
|
||||
param(
|
||||
[string] $Branch,
|
||||
[Alias('Profile')][string] $VSCodeProfile = 'Default',
|
||||
[switch] $NoFetch,
|
||||
[switch] $Help
|
||||
)
|
||||
. "$PSScriptRoot/WorktreeLib.ps1"
|
||||
|
||||
if ($Help -or -not $Branch) { Show-FileEmbeddedHelp -ScriptPath $MyInvocation.MyCommand.Path; return }
|
||||
|
||||
# Normalize origin/<name> to <name>
|
||||
if ($Branch -match '^(origin|upstream|main|master)/.+') {
|
||||
if ($Branch -match '^(origin|upstream)/(.+)$') { $Branch = $Matches[2] }
|
||||
}
|
||||
|
||||
try {
|
||||
git show-ref --verify --quiet "refs/heads/$Branch"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
if (-not $NoFetch) {
|
||||
Warn "Local branch '$Branch' not found; attempting remote fetch..."
|
||||
git fetch --all --prune 2>$null | Out-Null
|
||||
$remoteRef = "origin/$Branch"
|
||||
git show-ref --verify --quiet "refs/remotes/$remoteRef"
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
git branch --track $Branch $remoteRef 2>$null | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { throw "Failed to create tracking branch '$Branch' from $remoteRef" }
|
||||
Info "Created local tracking branch '$Branch' from $remoteRef."
|
||||
} else { throw "Branch '$Branch' not found locally or on origin. Use git fetch or specify a valid branch." }
|
||||
} else { throw "Branch '$Branch' does not exist locally (remote fetch disabled with -NoFetch)." }
|
||||
}
|
||||
|
||||
New-WorktreeForExistingBranch -Branch $Branch -VSCodeProfile $VSCodeProfile
|
||||
$after = Get-WorktreeEntries | Where-Object { $_.Branch -eq $Branch }
|
||||
$path = ($after | Select-Object -First 1).Path
|
||||
Show-WorktreeExecutionSummary -CurrentBranch $Branch -WorktreePath $path
|
||||
} catch {
|
||||
Err "Error: $($_.Exception.Message)"
|
||||
Warn 'Manual steps:'
|
||||
Info ' git fetch origin'
|
||||
Info " git checkout $Branch (or: git branch --track $Branch origin/$Branch)"
|
||||
Info ' git worktree add ../<Repo>-XX <branch>'
|
||||
Info ' code ../<Repo>-XX'
|
||||
exit 1
|
||||
}
|
||||
4
tools/build/New-WorktreeFromFork.cmd
Normal file
4
tools/build/New-WorktreeFromFork.cmd
Normal file
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
setlocal
|
||||
set SCRIPT_DIR=%~dp0
|
||||
pwsh -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%New-WorktreeFromFork.ps1" %*
|
||||
127
tools/build/New-WorktreeFromFork.ps1
Normal file
127
tools/build/New-WorktreeFromFork.ps1
Normal file
@@ -0,0 +1,127 @@
|
||||
<#!
|
||||
.SYNOPSIS
|
||||
Create (or reuse) a worktree from a branch in a personal fork: <ForkUser>:<ForkBranch>.
|
||||
|
||||
.DESCRIPTION
|
||||
Adds a transient uniquely named fork remote (fork-xxxxx) unless -RemoteName specified.
|
||||
Fetches only the target branch (fallback full fetch once if needed), creates a local tracking
|
||||
branch (fork-<user>-<sanitized-branch> or custom alias), and delegates worktree creation/reuse
|
||||
to shared helpers in WorktreeLib.
|
||||
|
||||
.PARAMETER Spec
|
||||
Fork spec in the form <ForkUser>:<ForkBranch>.
|
||||
|
||||
.PARAMETER ForkRepo
|
||||
Repository name in the fork (default: PowerToys).
|
||||
|
||||
.PARAMETER RemoteName
|
||||
Desired remote name; if left as 'fork' a unique suffix will be generated.
|
||||
|
||||
.PARAMETER BranchAlias
|
||||
Optional local branch name override; defaults to fork-<user>-<sanitized-branch>.
|
||||
|
||||
.PARAMETER VSCodeProfile
|
||||
VS Code profile to pass through to worktree opening (Default profile by default).
|
||||
|
||||
.EXAMPLE
|
||||
./New-WorktreeFromFork.ps1 -Spec alice:feature/new-ui
|
||||
|
||||
.EXAMPLE
|
||||
./New-WorktreeFromFork.ps1 -Spec bob:bugfix/crash -BranchAlias fork-bob-crash
|
||||
|
||||
.NOTES
|
||||
Manual equivalent if this script fails:
|
||||
git remote add fork-temp https://github.com/<user>/<repo>.git
|
||||
git fetch fork-temp
|
||||
git branch --track fork-<user>-<branch> fork-temp/<branch>
|
||||
git worktree add ../Repo-XX fork-<user>-<branch>
|
||||
code ../Repo-XX
|
||||
#>
|
||||
param(
|
||||
[string] $Spec,
|
||||
[string] $ForkRepo = 'PowerToys',
|
||||
[string] $RemoteName = 'fork',
|
||||
[string] $BranchAlias,
|
||||
[Alias('Profile')][string] $VSCodeProfile = 'Default',
|
||||
[switch] $Help
|
||||
)
|
||||
|
||||
. "$PSScriptRoot/WorktreeLib.ps1"
|
||||
if ($Help -or -not $Spec) { Show-FileEmbeddedHelp -ScriptPath $MyInvocation.MyCommand.Path; return }
|
||||
|
||||
$repoRoot = git rev-parse --show-toplevel 2>$null
|
||||
if (-not $repoRoot) { throw 'Not inside a git repository.' }
|
||||
|
||||
# Parse spec
|
||||
if ($Spec -notmatch '^[^:]+:.+$') { throw "Spec must be <ForkUser>:<ForkBranch>, got '$Spec'" }
|
||||
$ForkUser,$ForkBranch = $Spec.Split(':',2)
|
||||
|
||||
$forkUrl = "https://github.com/$ForkUser/$ForkRepo.git"
|
||||
|
||||
# Auto-suffix remote name if user left default 'fork'
|
||||
$allRemotes = @(git remote 2>$null)
|
||||
if ($RemoteName -eq 'fork') {
|
||||
$chars = 'abcdefghijklmnopqrstuvwxyz0123456789'
|
||||
do {
|
||||
$suffix = -join ((1..5) | ForEach-Object { $chars[(Get-Random -Max $chars.Length)] })
|
||||
$candidate = "fork-$suffix"
|
||||
} while ($allRemotes -contains $candidate)
|
||||
$RemoteName = $candidate
|
||||
Info "Assigned unique remote name: $RemoteName"
|
||||
}
|
||||
|
||||
$existing = $allRemotes | Where-Object { $_ -eq $RemoteName }
|
||||
if (-not $existing) {
|
||||
Info "Adding remote $RemoteName -> $forkUrl"
|
||||
git remote add $RemoteName $forkUrl | Out-Null
|
||||
} else {
|
||||
$currentUrl = git remote get-url $RemoteName 2>$null
|
||||
if ($currentUrl -ne $forkUrl) { Warn "Remote $RemoteName points to $currentUrl (expected $forkUrl). Using existing." }
|
||||
}
|
||||
|
||||
## Note: Verbose fetch & stale lock auto-clean removed for simplicity.
|
||||
|
||||
try {
|
||||
Info "Fetching branch '$ForkBranch' from $RemoteName..."
|
||||
& git fetch $RemoteName $ForkBranch 1>$null 2>$null
|
||||
$fetchExit = $LASTEXITCODE
|
||||
if ($fetchExit -ne 0) {
|
||||
# Retry full fetch silently once (covers servers not supporting branch-only fetch syntax)
|
||||
& git fetch $RemoteName 1>$null 2>$null
|
||||
$fetchExit = $LASTEXITCODE
|
||||
}
|
||||
if ($fetchExit -ne 0) { throw "Fetch failed for remote $RemoteName (branch $ForkBranch)." }
|
||||
|
||||
$remoteRef = "refs/remotes/$RemoteName/$ForkBranch"
|
||||
git show-ref --verify --quiet $remoteRef
|
||||
if ($LASTEXITCODE -ne 0) { throw "Remote branch not found: $RemoteName/$ForkBranch" }
|
||||
|
||||
$sanitizedBranch = ($ForkBranch -replace '[\\/:*?"<>|]','-')
|
||||
if ($BranchAlias) { $localBranch = $BranchAlias } else { $localBranch = "fork-$ForkUser-$sanitizedBranch" }
|
||||
|
||||
git show-ref --verify --quiet "refs/heads/$localBranch"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Info "Creating local tracking branch $localBranch from $RemoteName/$ForkBranch"
|
||||
git branch --track $localBranch "$RemoteName/$ForkBranch" 2>$null | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { throw "Failed to create local tracking branch $localBranch" }
|
||||
} else { Info "Local branch $localBranch already exists." }
|
||||
|
||||
New-WorktreeForExistingBranch -Branch $localBranch -VSCodeProfile $VSCodeProfile
|
||||
# Ensure upstream so future 'git push' works
|
||||
Set-BranchUpstream -LocalBranch $localBranch -RemoteName $RemoteName -RemoteBranchPath $ForkBranch
|
||||
$after = Get-WorktreeEntries | Where-Object { $_.Branch -eq $localBranch }
|
||||
$path = ($after | Select-Object -First 1).Path
|
||||
Show-WorktreeExecutionSummary -CurrentBranch $localBranch -WorktreePath $path
|
||||
Warn "Remote $RemoteName ready (URL: $forkUrl)"
|
||||
$hasUp = git rev-parse --abbrev-ref --symbolic-full-name "$localBranch@{upstream}" 2>$null
|
||||
if ($hasUp) { Info "Push with: git push (upstream: $hasUp)" } else { Warn 'Upstream not set; run: git push -u <remote> <local>:<remoteBranch>' }
|
||||
} catch {
|
||||
Err "Error: $($_.Exception.Message)"
|
||||
Warn 'Manual steps:'
|
||||
Info " git remote add temp-fork $forkUrl"
|
||||
Info " git fetch temp-fork"
|
||||
Info " git branch --track fork-<user>-<branch> temp-fork/$ForkBranch"
|
||||
Info ' git worktree add ../<Repo>-XX fork-<user>-<branch>'
|
||||
Info ' code ../<Repo>-XX'
|
||||
exit 1
|
||||
}
|
||||
4
tools/build/New-WorktreeFromIssue.cmd
Normal file
4
tools/build/New-WorktreeFromIssue.cmd
Normal file
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
setlocal
|
||||
set SCRIPT_DIR=%~dp0
|
||||
pwsh -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%New-WorktreeFromIssue.ps1" %*
|
||||
78
tools/build/New-WorktreeFromIssue.ps1
Normal file
78
tools/build/New-WorktreeFromIssue.ps1
Normal file
@@ -0,0 +1,78 @@
|
||||
<#!
|
||||
.SYNOPSIS
|
||||
Create (or reuse) a worktree for a new issue branch derived from a base ref.
|
||||
|
||||
.DESCRIPTION
|
||||
Composes a branch name as issue/<number> or issue/<number>-<slug> (slug from optional -Title).
|
||||
If the branch does not already exist, it is created from -Base (default origin/main). Then a
|
||||
worktree is created or reused.
|
||||
|
||||
.PARAMETER Number
|
||||
Issue number used to construct the branch name.
|
||||
|
||||
.PARAMETER Title
|
||||
Optional descriptive title; slug into the branch name.
|
||||
|
||||
.PARAMETER Base
|
||||
Base ref to branch from (default origin/main).
|
||||
|
||||
.PARAMETER VSCodeProfile
|
||||
VS Code profile to open (Default).
|
||||
|
||||
.EXAMPLE
|
||||
./New-WorktreeFromIssue.ps1 -Number 1234 -Title "Crash on launch"
|
||||
|
||||
.EXAMPLE
|
||||
./New-WorktreeFromIssue.ps1 -Number 42 -Base origin/develop
|
||||
|
||||
.NOTES
|
||||
Manual recovery:
|
||||
git fetch origin
|
||||
git checkout -b issue/<num>-<slug> <base>
|
||||
git worktree add ../Repo-XX issue/<num>-<slug>
|
||||
code ../Repo-XX
|
||||
#>
|
||||
|
||||
param(
|
||||
[int] $Number,
|
||||
[string] $Title,
|
||||
[string] $Base = 'origin/main',
|
||||
[Alias('Profile')][string] $VSCodeProfile = 'Default',
|
||||
[switch] $Help
|
||||
)
|
||||
. "$PSScriptRoot/WorktreeLib.ps1"
|
||||
$scriptPath = $MyInvocation.MyCommand.Path
|
||||
if ($Help -or -not $Number) { Show-FileEmbeddedHelp -ScriptPath $scriptPath; return }
|
||||
|
||||
# Compose branch name
|
||||
if ($Title) {
|
||||
$slug = ($Title -replace '[^\w\- ]','').ToLower() -replace ' +','-'
|
||||
$branch = "issue/$Number-$slug"
|
||||
} else {
|
||||
$branch = "issue/$Number"
|
||||
}
|
||||
|
||||
try {
|
||||
# Create branch if missing
|
||||
git show-ref --verify --quiet "refs/heads/$branch"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Info "Creating branch $branch from $Base"
|
||||
git branch $branch $Base 2>$null | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { throw "Failed to create branch $branch from $Base" }
|
||||
} else {
|
||||
Info "Branch $branch already exists locally."
|
||||
}
|
||||
|
||||
New-WorktreeForExistingBranch -Branch $branch -VSCodeProfile $VSCodeProfile
|
||||
$after = Get-WorktreeEntries | Where-Object { $_.Branch -eq $branch }
|
||||
$path = ($after | Select-Object -First 1).Path
|
||||
Show-WorktreeExecutionSummary -CurrentBranch $branch -WorktreePath $path
|
||||
} catch {
|
||||
Err "Error: $($_.Exception.Message)"
|
||||
Warn 'Manual steps:'
|
||||
Info " git fetch origin"
|
||||
Info " git checkout -b $branch $Base (if branch missing)"
|
||||
Info " git worktree add ../<Repo>-XX $branch"
|
||||
Info ' code ../<Repo>-XX'
|
||||
exit 1
|
||||
}
|
||||
94
tools/build/Worktree-Guidelines.md
Normal file
94
tools/build/Worktree-Guidelines.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# PowerToys Worktree Helper Scripts
|
||||
|
||||
This folder contains helper scripts to create and manage parallel Git worktree for developing multiple changes (including Copilot suggestions) concurrently without cloning the full repository each time.
|
||||
|
||||
## Why worktree?
|
||||
Git worktree let you have several checked‑out branches sharing a single `.git` object store. Benefits:
|
||||
- Fast context switching: no re-clone, no duplicate large binary/object downloads.
|
||||
- Lower disk usage versus multiple full clones.
|
||||
- Keeps each change isolated in its own folder so you can run builds/tests independently.
|
||||
- Enables working in parallel with Copilot generated branches (e.g., feature + quick fix + perf experiment) while the main clone stays clean.
|
||||
|
||||
Recommended: keep active parallel worktree(s) to **≤ 3** per developer to reduce cognitive load and avoid excessive incremental build invalidations.
|
||||
|
||||
## Scripts Overview
|
||||
| Script | Purpose |
|
||||
|--------|---------|
|
||||
| `New-WorktreeFromFork.ps1/.cmd` | Create a worktree from a branch in a personal fork (`<User>:<branch>` spec). Adds a temporary unique remote (e.g. `fork-abc12`). |
|
||||
| `New-WorktreeFromBranch.ps1/.cmd` | Create/reuse a worktree for an existing local or remote (origin) branch. Can normalize `origin/branch` to `branch`. |
|
||||
| `New-WorktreeFromIssue.ps1/.cmd` | Start a new issue branch from a base (default `origin/main`) using naming `issue/<number>-<slug>`. |
|
||||
| `Delete-Worktree.ps1/.cmd` | Remove a worktree and optionally its local branch / orphan fork remote. |
|
||||
| `WorktreeLib.ps1` | Shared helpers: unique folder naming, worktree listing, upstream setup, summary output, logging helpers. |
|
||||
|
||||
## Typical Flows
|
||||
### 1. Create from a fork branch
|
||||
```
|
||||
./New-WorktreeFromFork.ps1 -Spec alice:feature/perf-tweak
|
||||
```
|
||||
Creates remote `fork-xxxxx`, fetches just that branch, creates local branch `fork-alice-feature-perf-tweak`, makes a new worktree beside the repo root.
|
||||
|
||||
### 2. Create from an existing or remote branch
|
||||
```
|
||||
./New-WorktreeFromBranch.ps1 -Branch origin/feature/new-ui
|
||||
```
|
||||
Fetches if needed and creates a tracking branch if missing, then creates/reuses the worktree.
|
||||
|
||||
### 3. Start a new issue branch
|
||||
```
|
||||
./New-WorktreeFromIssue.ps1 -Number 1234 -Title "Crash on launch"
|
||||
```
|
||||
Creates branch `issue/1234-crash-on-launch` off `origin/main` (or `-Base`), then worktree.
|
||||
|
||||
### 4. Delete a worktree when done
|
||||
```
|
||||
./Delete-Worktree.ps1 -Pattern feature/perf-tweak
|
||||
```
|
||||
If only one match, removes the worktree directory. Add `-Force` to discard local changes. Use `-KeepBranch` if you still need the branch, `-KeepRemote` to retain a fork remote.
|
||||
|
||||
## After Creating a Worktree
|
||||
Inside the new worktree directory:
|
||||
1. Run the minimal build bootstrap in VSCode terminal:
|
||||
```
|
||||
tools\build\build-essentials.cmd
|
||||
```
|
||||
2. Build only the module(s) you need (e.g., open solution filter or run targeted project build) instead of a full PowerToys build. This speeds iteration and reduces noise.
|
||||
3. Make changes, commit, push.
|
||||
4. Finally delete the worktree when done.
|
||||
|
||||
## Naming & Locations
|
||||
- Worktree is created as sibling folders of the repo root (e.g., `PowerToys` + `PowerToys-ab12`), using a hash/short pattern to avoid collisions.
|
||||
- Fork-based branches get local names `fork-<user>-<sanitized-branch>`.
|
||||
- Issue branches: `issue/<number>` or `issue/<number>-<slug>`.
|
||||
|
||||
## Scenarios Covered / Limitations
|
||||
Covered scenarios:
|
||||
1. From a fork branch (personal fork on GitHub).
|
||||
2. From an existing local or origin remote branch.
|
||||
3. Creating a new branch for an issue.
|
||||
|
||||
Not covered (manual steps needed):
|
||||
- Creating from a non-origin upstream other than a fork (add remote manually then use branch script).
|
||||
- Batch creation of multiple worktree in one command.
|
||||
- Automatic rebase / sync of many worktree at once (do that manually or script separately).
|
||||
|
||||
## Best Practices
|
||||
- Keep ≤ 3 active parallel worktree(s) (e.g., main dev, a long-lived feature, a quick fix / experiment) plus the root clone.
|
||||
- Delete stale worktree early; each adds file watchers & potential incremental build churn.
|
||||
- Avoid editing the same file across multiple worktree simultaneously to reduce merge friction.
|
||||
- Run `git fetch --all --prune` periodically in the primary repo, not in every worktree.
|
||||
|
||||
## Troubleshooting
|
||||
| Symptom | Hint |
|
||||
|---------|------|
|
||||
| Fetch failed for fork remote | Branch name typo or fork private without auth. Try manual `git fetch <remote> <branch>`.
|
||||
| Cannot lock ref *.lock | Stale lock: run `git worktree prune` or manually delete the `.lock` file then retry.
|
||||
| Worktree already exists error | Use `git worktree list` to locate existing path; open that folder instead of creating a duplicate.
|
||||
| Local branch missing for remote | Use `git branch --track <name> origin/<name>` then re-run the branch script.
|
||||
|
||||
## Security & Safety Notes
|
||||
- Scripts avoid force-deleting unless you pass `-Force` (Delete script).
|
||||
- No network credentials are stored; they rely on your existing Git credential helper.
|
||||
- Always review a new fork remote URL before pushing.
|
||||
|
||||
---
|
||||
Maintainers: Keep the scripts lean; avoid adding heavy dependencies or global state. Update this doc if parameters or flows change.
|
||||
151
tools/build/WorktreeLib.ps1
Normal file
151
tools/build/WorktreeLib.ps1
Normal file
@@ -0,0 +1,151 @@
|
||||
# WorktreeLib.ps1 - shared helpers
|
||||
|
||||
function Info { param([string]$Message) Write-Host $Message -ForegroundColor Cyan }
|
||||
function Warn { param([string]$Message) Write-Host $Message -ForegroundColor Yellow }
|
||||
function Err { param([string]$Message) Write-Host $Message -ForegroundColor Red }
|
||||
|
||||
function Get-RepoRoot {
|
||||
$root = git rev-parse --show-toplevel 2>$null
|
||||
if (-not $root) { throw 'Not inside a git repository.' }
|
||||
return $root
|
||||
}
|
||||
|
||||
function Get-WorktreeBasePath {
|
||||
param([string]$RepoRoot)
|
||||
# Always use parent of repo root (folder that contains the main repo directory)
|
||||
$parent = Split-Path -Parent $RepoRoot
|
||||
if (-not (Test-Path $parent)) { throw "Parent path for repo root not found: $parent" }
|
||||
return (Resolve-Path $parent).ProviderPath
|
||||
}
|
||||
|
||||
function Get-ShortHashFromString {
|
||||
param([Parameter(Mandatory)][string]$Text)
|
||||
$md5 = [System.Security.Cryptography.MD5]::Create()
|
||||
try {
|
||||
$bytes = [Text.Encoding]::UTF8.GetBytes($Text)
|
||||
$digest = $md5.ComputeHash($bytes)
|
||||
return -join ($digest[0..1] | ForEach-Object { $_.ToString('x2') })
|
||||
} finally { $md5.Dispose() }
|
||||
}
|
||||
|
||||
function Initialize-SubmodulesIfAny {
|
||||
param([string]$RepoRoot,[string]$WorktreePath)
|
||||
$hasGitmodules = Test-Path (Join-Path $RepoRoot '.gitmodules')
|
||||
if ($hasGitmodules) {
|
||||
git -C $WorktreePath submodule sync --recursive | Out-Null
|
||||
git -C $WorktreePath submodule update --init --recursive | Out-Null
|
||||
return $true
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
function New-WorktreeForExistingBranch {
|
||||
param(
|
||||
[Parameter(Mandatory)][string] $Branch,
|
||||
[Parameter(Mandatory)][string] $VSCodeProfile
|
||||
)
|
||||
$repoRoot = Get-RepoRoot
|
||||
git show-ref --verify --quiet "refs/heads/$Branch"; if ($LASTEXITCODE -ne 0) { throw "Branch '$Branch' does not exist locally." }
|
||||
|
||||
# Detect existing worktree for this branch
|
||||
$entries = Get-WorktreeEntries
|
||||
$match = $entries | Where-Object { $_.Branch -eq $Branch } | Select-Object -First 1
|
||||
if ($match) {
|
||||
Info "Reusing existing worktree for '$Branch': $($match.Path)"
|
||||
code --new-window "$($match.Path)" --profile "$VSCodeProfile" | Out-Null
|
||||
return
|
||||
}
|
||||
|
||||
$safeBranch = ($Branch -replace '[\\/:*?"<>|]','-')
|
||||
$hash = Get-ShortHashFromString -Text $safeBranch
|
||||
$folderName = "$(Split-Path -Leaf $repoRoot)-$hash"
|
||||
$base = Get-WorktreeBasePath -RepoRoot $repoRoot
|
||||
$folder = Join-Path $base $folderName
|
||||
git worktree add $folder $Branch
|
||||
$inited = Initialize-SubmodulesIfAny -RepoRoot $repoRoot -WorktreePath $folder
|
||||
code --new-window "$folder" --profile "$VSCodeProfile" | Out-Null
|
||||
Info "Created worktree for branch '$Branch' at $folder."; if ($inited) { Info 'Submodules initialized.' }
|
||||
}
|
||||
|
||||
function Get-WorktreeEntries {
|
||||
# Returns objects with Path and Branch (branch without refs/heads/ prefix)
|
||||
$lines = git worktree list --porcelain 2>$null
|
||||
if (-not $lines) { return @() }
|
||||
$entries = @(); $current=@{}
|
||||
foreach($l in $lines){
|
||||
if ($l -eq '') { if ($current.path -and $current.branch){ $entries += ,([pscustomobject]@{ Path=$current.path; Branch=($current.branch -replace '^refs/heads/','') }) }; $current=@{}; continue }
|
||||
if ($l -like 'worktree *'){ $current.path = ($l -split ' ',2)[1] }
|
||||
elseif ($l -like 'branch *'){ $current.branch = ($l -split ' ',2)[1].Trim() }
|
||||
}
|
||||
if ($current.path -and $current.branch){ $entries += ,([pscustomobject]@{ Path=$current.path; Branch=($current.branch -replace '^refs/heads/','') }) }
|
||||
return ($entries | Sort-Object Path,Branch -Unique)
|
||||
}
|
||||
|
||||
function Get-BranchUpstreamRemote {
|
||||
param([Parameter(Mandatory)][string]$Branch)
|
||||
# Returns remote name if branch has an upstream, else $null
|
||||
$ref = git rev-parse --abbrev-ref --symbolic-full-name "$Branch@{upstream}" 2>$null
|
||||
if ($LASTEXITCODE -ne 0 -or -not $ref) { return $null }
|
||||
if ($ref -match '^(?<remote>[^/]+)/.+$') { return $Matches.remote }
|
||||
return $null
|
||||
}
|
||||
|
||||
function Show-IssueFarmCommonFooter {
|
||||
Info '--- Common Manual Steps ---'
|
||||
Info 'List worktree: git worktree list --porcelain'
|
||||
Info 'List branches: git branch -vv'
|
||||
Info 'List remotes: git remote -v'
|
||||
Info 'Prune worktree: git worktree prune'
|
||||
Info 'Remove worktree dir: Remove-Item -Recurse -Force <path>'
|
||||
Info 'Reset branch: git reset --hard HEAD'
|
||||
}
|
||||
|
||||
function Show-WorktreeExecutionSummary {
|
||||
param(
|
||||
[string]$CurrentBranch,
|
||||
[string]$WorktreePath
|
||||
)
|
||||
Info '--- Summary ---'
|
||||
if ($CurrentBranch) { Info "Branch: $CurrentBranch" }
|
||||
if ($WorktreePath) { Info "Worktree path: $WorktreePath" }
|
||||
$entries = Get-WorktreeEntries
|
||||
if ($entries.Count -gt 0) {
|
||||
Info 'Existing worktrees:'
|
||||
$entries | ForEach-Object { Info (" {0} -> {1}" -f $_.Branch,$_.Path) }
|
||||
}
|
||||
Info 'Remotes:'
|
||||
git remote -v 2>$null | Sort-Object | Get-Unique | ForEach-Object { Info " $_" }
|
||||
}
|
||||
|
||||
function Show-FileEmbeddedHelp {
|
||||
param([string]$ScriptPath)
|
||||
if (-not (Test-Path $ScriptPath)) { throw "Cannot load help; file missing: $ScriptPath" }
|
||||
$content = Get-Content -LiteralPath $ScriptPath -ErrorAction Stop
|
||||
$inBlock=$false
|
||||
foreach($line in $content){
|
||||
if ($line -match '^<#!') { $inBlock=$true; continue }
|
||||
if ($line -match '#>$') { break }
|
||||
if ($inBlock) { Write-Host $line }
|
||||
}
|
||||
Show-IssueFarmCommonFooter
|
||||
}
|
||||
|
||||
function Set-BranchUpstream {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$LocalBranch,
|
||||
[Parameter(Mandatory)][string]$RemoteName,
|
||||
[Parameter(Mandatory)][string]$RemoteBranchPath
|
||||
)
|
||||
$current = git rev-parse --abbrev-ref --symbolic-full-name "$LocalBranch@{upstream}" 2>$null
|
||||
if (-not $current) {
|
||||
Info "Setting upstream: $LocalBranch -> $RemoteName/$RemoteBranchPath"
|
||||
git branch --set-upstream-to "$RemoteName/$RemoteBranchPath" $LocalBranch 2>$null | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { Warn "Failed to set upstream automatically. Run: git branch --set-upstream-to $RemoteName/$RemoteBranchPath $LocalBranch" }
|
||||
return
|
||||
}
|
||||
if ($current -ne "$RemoteName/$RemoteBranchPath") {
|
||||
Warn "Upstream mismatch ($current != $RemoteName/$RemoteBranchPath); updating..."
|
||||
git branch --set-upstream-to "$RemoteName/$RemoteBranchPath" $LocalBranch 2>$null | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { Warn "Could not update upstream; manual fix: git branch --set-upstream-to $RemoteName/$RemoteBranchPath $LocalBranch" } else { Info 'Upstream corrected.' }
|
||||
} else { Info "Upstream already: $current" }
|
||||
}
|
||||
@@ -52,8 +52,10 @@ function RunMSBuild {
|
||||
|
||||
$base = @(
|
||||
$Solution
|
||||
"/m"
|
||||
"/p:Platform=$Platform"
|
||||
"/p:Configuration=$Configuration"
|
||||
"/p:BuildInParallel=true"
|
||||
"/verbosity:normal"
|
||||
'/clp:Summary;PerformanceSummary;ErrorsOnly;WarningsOnly'
|
||||
"/fileLoggerParameters:LogFile=$allLog;Verbosity=detailed"
|
||||
@@ -92,9 +94,7 @@ function RestoreThenBuild {
|
||||
RunMSBuild $Solution $restoreArgs $Platform $Configuration
|
||||
|
||||
if (-not $RestoreOnly) {
|
||||
$buildArgs = '/m'
|
||||
if ($ExtraArgs) { $buildArgs = "$buildArgs $ExtraArgs" }
|
||||
RunMSBuild $Solution $buildArgs $Platform $Configuration
|
||||
RunMSBuild $Solution $ExtraArgs $Platform $Configuration
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,9 +133,7 @@ function BuildProjectsInDirectory {
|
||||
if ($f.Extension -eq '.sln') {
|
||||
RestoreThenBuild $f.FullName $ExtraArgs $Platform $Configuration $RestoreOnly
|
||||
} else {
|
||||
$buildArgs = '/m'
|
||||
if ($ExtraArgs) { $buildArgs = "$buildArgs $ExtraArgs" }
|
||||
RunMSBuild $f.FullName $buildArgs $Platform $Configuration
|
||||
RunMSBuild $f.FullName $ExtraArgs $Platform $Configuration
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -63,6 +63,7 @@ if ($ForceRebuildExecutable -or -not (Test-Path $exePath)) {
|
||||
'/m',
|
||||
"/p:Configuration=$BuildConfiguration",
|
||||
"/p:Platform=x64",
|
||||
"/p:BuildInParallel=true",
|
||||
'/restore'
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user