mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-01-01 09:56:32 +01:00
Compare commits
48 Commits
jay/lsv2
...
jay/1025-r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8596c1d7f2 | ||
|
|
ea1a6af9a8 | ||
|
|
d4ebdbf925 | ||
|
|
e9d4e21b1c | ||
|
|
7fc9b5a451 | ||
|
|
a7080840e8 | ||
|
|
3f560702fc | ||
|
|
1a1957b39b | ||
|
|
0f51168031 | ||
|
|
868d8badc3 | ||
|
|
ad5539f427 | ||
|
|
004e92c055 | ||
|
|
5682a11ec8 | ||
|
|
1887571503 | ||
|
|
39a73399aa | ||
|
|
0c4b1c6fd2 | ||
|
|
d7df7946c6 | ||
|
|
7cc6ab2dde | ||
|
|
b27dca3714 | ||
|
|
1c1da85c16 | ||
|
|
ef905e14e3 | ||
|
|
05aa647927 | ||
|
|
e915325355 | ||
|
|
97e62b3253 | ||
|
|
cd5b76c988 | ||
|
|
f44addb29c | ||
|
|
1e3429dd3a | ||
|
|
075bbb46cb | ||
|
|
4aa27316fb | ||
|
|
f55c49e15b | ||
|
|
b06cd9f896 | ||
|
|
3e0d62d101 | ||
|
|
b89237ff94 | ||
|
|
df972447d4 | ||
|
|
668820cf2c | ||
|
|
ca4e8b2986 | ||
|
|
0472e7dc78 | ||
|
|
6196e84e35 | ||
|
|
1f18088afe | ||
|
|
494901b52d | ||
|
|
3e213165a8 | ||
|
|
e04e6a11d1 | ||
|
|
14ff4dbc8c | ||
|
|
a8eb17d21a | ||
|
|
240923beb2 | ||
|
|
54ca8fd0f7 | ||
|
|
3d5f1ea3af | ||
|
|
db62dbd827 |
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/.*$
|
||||
|
||||
49
.github/actions/spell-check/expect.txt
vendored
49
.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
|
||||
@@ -375,6 +371,7 @@ DEVMODEW
|
||||
devpal
|
||||
DFX
|
||||
DIALOGEX
|
||||
diffs
|
||||
digicert
|
||||
DINORMAL
|
||||
DISABLEASACTIONKEY
|
||||
@@ -512,7 +509,6 @@ FANCYZONESDRAWLAYOUTTEST
|
||||
FANCYZONESEDITOR
|
||||
FARPROC
|
||||
fesf
|
||||
fff
|
||||
FFFF
|
||||
FILEEXPLORER
|
||||
fileexploreraddons
|
||||
@@ -585,6 +581,7 @@ GETSCREENSAVERRUNNING
|
||||
GETSECKEY
|
||||
GETSTICKYKEYS
|
||||
GETTEXTLENGTH
|
||||
gitmodules
|
||||
GHND
|
||||
GMEM
|
||||
GNumber
|
||||
@@ -672,11 +669,7 @@ Hostx
|
||||
hotfixes
|
||||
hotkeycontrol
|
||||
HOTKEYF
|
||||
hotkeylockmachine
|
||||
hotkeyreconnect
|
||||
hotkeys
|
||||
hotkeyswitch
|
||||
hotkeytoggleeasymouse
|
||||
hotlight
|
||||
hotspot
|
||||
HPAINTBUFFER
|
||||
@@ -735,8 +728,6 @@ IMAGERESIZERCONTEXTMENU
|
||||
IMAGERESIZEREXT
|
||||
imageresizerinput
|
||||
imageresizersettings
|
||||
imagetotext
|
||||
imagetotextshortcut
|
||||
imagingdevices
|
||||
ime
|
||||
imgflip
|
||||
@@ -856,6 +847,7 @@ linkid
|
||||
LINKOVERLAY
|
||||
LINQTo
|
||||
listview
|
||||
LIVEDRAW
|
||||
LIVEZOOM
|
||||
LLKH
|
||||
llkhf
|
||||
@@ -867,7 +859,6 @@ localappdata
|
||||
localpackage
|
||||
LOCALSYSTEM
|
||||
LOCATIONCHANGE
|
||||
LOCKMACHINE
|
||||
LOCKTYPE
|
||||
LOGFONT
|
||||
LOGFONTW
|
||||
@@ -876,7 +867,6 @@ LOGMSG
|
||||
LOGPIXELSX
|
||||
LOGPIXELSY
|
||||
lng
|
||||
LOn
|
||||
lon
|
||||
longdate
|
||||
LONGNAMES
|
||||
@@ -928,12 +918,10 @@ luid
|
||||
LUMA
|
||||
lusrmgr
|
||||
LVal
|
||||
lvm
|
||||
LWA
|
||||
lwin
|
||||
LZero
|
||||
MAGTRANSFORM
|
||||
MAJORMINOR
|
||||
MAKEINTRESOURCE
|
||||
MAKEINTRESOURCEA
|
||||
MAKEINTRESOURCEW
|
||||
@@ -958,7 +946,6 @@ MDL
|
||||
mdtext
|
||||
mdtxt
|
||||
mdwn
|
||||
measuretool
|
||||
meme
|
||||
memicmp
|
||||
MENUITEMINFO
|
||||
@@ -1008,7 +995,6 @@ MOUSEHWHEEL
|
||||
MOUSEINPUT
|
||||
mousejump
|
||||
mousepointer
|
||||
mousepointercrosshairs
|
||||
mouseutils
|
||||
MOVESIZEEND
|
||||
MOVESIZESTART
|
||||
@@ -1053,7 +1039,6 @@ MWBEx
|
||||
MYICON
|
||||
NAMECHANGE
|
||||
namespaceanddescendants
|
||||
Namotion
|
||||
nao
|
||||
NCACTIVATE
|
||||
ncc
|
||||
@@ -1091,7 +1076,6 @@ NEWPLUSSHELLEXTENSIONWIN
|
||||
newrow
|
||||
nicksnettravels
|
||||
NIF
|
||||
NJson
|
||||
NLog
|
||||
NLSTEXT
|
||||
NMAKE
|
||||
@@ -1218,18 +1202,6 @@ PARENTRELATIVEFORUI
|
||||
PARENTRELATIVEPARSING
|
||||
parray
|
||||
PARTIALCONFIRMATIONDIALOGTITLE
|
||||
pasteashtmlfile
|
||||
pasteashtmlfileshortcut
|
||||
pasteasjson
|
||||
pasteasjsonshortcut
|
||||
pasteasmarkdown
|
||||
pasteasmarkdownshortcut
|
||||
pasteasplaintext
|
||||
pasteasplaintextshortcut
|
||||
pasteaspngfile
|
||||
pasteaspngfileshortcut
|
||||
pasteastxtfile
|
||||
pasteastxtfileshortcut
|
||||
PATCOPY
|
||||
PATHMUSTEXIST
|
||||
PATINVERT
|
||||
@@ -1237,6 +1209,7 @@ PATPAINT
|
||||
pbc
|
||||
pbi
|
||||
PBlob
|
||||
pbrush
|
||||
pcb
|
||||
pcch
|
||||
pcelt
|
||||
@@ -1299,7 +1272,6 @@ Pomodoro
|
||||
Popups
|
||||
POPUPWINDOW
|
||||
POSITIONITEM
|
||||
powerocr
|
||||
POWERRENAMECONTEXTMENU
|
||||
powerrenameinput
|
||||
POWERRENAMETEST
|
||||
@@ -1350,7 +1322,6 @@ PRODUCTVERSION
|
||||
Progman
|
||||
programdata
|
||||
projectname
|
||||
projitems
|
||||
PROPERTYKEY
|
||||
Propset
|
||||
PROPVARIANT
|
||||
@@ -1358,6 +1329,7 @@ PRTL
|
||||
prvpane
|
||||
psapi
|
||||
pscid
|
||||
pscustomobject
|
||||
PSECURITY
|
||||
psfgao
|
||||
psfi
|
||||
@@ -1443,7 +1415,6 @@ Removelnk
|
||||
renamable
|
||||
RENAMEONCOLLISION
|
||||
reparented
|
||||
reparenthotkey
|
||||
reparenting
|
||||
reportfileaccesses
|
||||
requery
|
||||
@@ -1469,7 +1440,6 @@ RIDEV
|
||||
RIGHTSCROLLBAR
|
||||
riid
|
||||
RKey
|
||||
Rns
|
||||
RNumber
|
||||
rop
|
||||
ROUNDSMALL
|
||||
@@ -1693,7 +1663,6 @@ STYLECHANGED
|
||||
STYLECHANGING
|
||||
subkeys
|
||||
sublang
|
||||
Subdomain
|
||||
SUBMODULEUPDATE
|
||||
subresource
|
||||
Superbar
|
||||
@@ -1766,7 +1735,6 @@ THICKFRAME
|
||||
THEMECHANGED
|
||||
THISCOMPONENT
|
||||
throughs
|
||||
thumbnailhotkey
|
||||
TILEDWINDOW
|
||||
TILLSON
|
||||
timedate
|
||||
@@ -1782,9 +1750,7 @@ tlbimp
|
||||
tlc
|
||||
tmain
|
||||
TNP
|
||||
TOGGLEEASYMOUSE
|
||||
Toolhelp
|
||||
toolkitconverters
|
||||
toolwindow
|
||||
TOPDOWNDIB
|
||||
TOUCHEVENTF
|
||||
@@ -1796,11 +1762,9 @@ tracelogging
|
||||
tracerpt
|
||||
trackbar
|
||||
trafficmanager
|
||||
transcodetomp
|
||||
transicc
|
||||
TRAYMOUSEMESSAGE
|
||||
triaging
|
||||
Tru
|
||||
trl
|
||||
trx
|
||||
tsa
|
||||
@@ -1836,7 +1800,6 @@ ULONGLONG
|
||||
ums
|
||||
uncompilable
|
||||
UNCPRIORITY
|
||||
undefining
|
||||
UNDNAME
|
||||
UNICODETEXT
|
||||
unins
|
||||
@@ -2003,6 +1966,7 @@ WMI
|
||||
WMICIM
|
||||
wmimgmt
|
||||
wmp
|
||||
wmsg
|
||||
WMSYSCOMMAND
|
||||
wnd
|
||||
WNDCLASS
|
||||
@@ -2016,6 +1980,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
|
||||
|
||||
@@ -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}
|
||||
|
||||
313
README.md
313
README.md
@@ -10,11 +10,11 @@
|
||||
|
||||
<h3 align="center">
|
||||
<a href="#-installation">Installation</a>
|
||||
<span> · </span>
|
||||
<span> . </span>
|
||||
<a href="https://aka.ms/powertoys-docs">Documentation</a>
|
||||
<span> · </span>
|
||||
<span> . </span>
|
||||
<a href="https://aka.ms/powertoys-releaseblog">Blog</a>
|
||||
<span> · </span>
|
||||
<span> . </span>
|
||||
<a href="#-whats-new">Release notes</a>
|
||||
</h3>
|
||||
<br/><br/>
|
||||
@@ -27,11 +27,12 @@ Microsoft PowerToys is a collection of utilities that help you customize Windows
|
||||
| [<img src="doc/images/icons/Color%20Picker.png" alt="Color Picker icon" height="16"> Color Picker](https://aka.ms/PowerToysOverview_ColorPicker) | [<img src="doc/images/icons/Command%20Not%20Found.png" alt="Command Not Found icon" height="16"> Command Not Found](https://aka.ms/PowerToysOverview_CmdNotFound) | [<img src="doc/images/icons/Command Palette.png" alt="Command Palette icon" height="16"> Command Palette](https://aka.ms/PowerToysOverview_CmdPal) |
|
||||
| [<img src="doc/images/icons/Crop%20And%20Lock.png" alt="Crop and Lock icon" height="16"> Crop And Lock](https://aka.ms/PowerToysOverview_CropAndLock) | [<img src="doc/images/icons/Environment%20Manager.png" alt="Environment Variables icon" height="16"> Environment Variables](https://aka.ms/PowerToysOverview_EnvironmentVariables) | [<img src="doc/images/icons/FancyZones.png" alt="FancyZones icon" height="16"> FancyZones](https://aka.ms/PowerToysOverview_FancyZones) |
|
||||
| [<img src="doc/images/icons/File%20Explorer%20Preview.png" alt="File Explorer Add-ons icon" height="16"> File Explorer Add-ons](https://aka.ms/PowerToysOverview_FileExplorerAddOns) | [<img src="doc/images/icons/File%20Locksmith.png" alt="File Locksmith icon" height="16"> File Locksmith](https://aka.ms/PowerToysOverview_FileLocksmith) | [<img src="doc/images/icons/Host%20File%20Editor.png" alt="Hosts File Editor icon" height="16"> Hosts File Editor](https://aka.ms/PowerToysOverview_HostsFileEditor) |
|
||||
| [<img src="doc/images/icons/Image%20Resizer.png" alt="Image Resizer icon" height="16"> Image Resizer](https://aka.ms/PowerToysOverview_ImageResizer) | [<img src="doc/images/icons/Keyboard%20Manager.png" alt="Keyboard Manager icon" height="16"> Keyboard Manager](https://aka.ms/PowerToysOverview_KeyboardManager) | [<img src="doc/images/icons/Find My Mouse.png" alt="Mouse Utilities icon" height="16"> Mouse Utilities](https://aka.ms/PowerToysOverview_MouseUtilities) |
|
||||
| [<img src="doc/images/icons/MouseWithoutBorders.png" alt="Mouse Without Borders icon" height="16"> Mouse Without Borders](https://aka.ms/PowerToysOverview_MouseWithoutBorders) | [<img src="doc/images/icons/NewPlus.png" alt="New+ icon" height="16"> New+](https://aka.ms/PowerToysOverview_NewPlus) | [<img src="doc/images/icons/Peek.png" alt="Peek icon" height="16"> Peek](https://aka.ms/PowerToysOverview_Peek) |
|
||||
| [<img src="doc/images/icons/PowerRename.png" alt="PowerRename icon" height="16"> PowerRename](https://aka.ms/PowerToysOverview_PowerRename) | [<img src="doc/images/icons/PowerToys%20Run.png" alt="PowerToys Run icon" height="16"> PowerToys Run](https://aka.ms/PowerToysOverview_PowerToysRun) | [<img src="doc/images/icons/PowerAccent.png" alt="Quick Accent icon" height="16"> Quick Accent](https://aka.ms/PowerToysOverview_QuickAccent) |
|
||||
| [<img src="doc/images/icons/Registry%20Preview.png" alt="Registry Preview icon" height="16"> Registry Preview](https://aka.ms/PowerToysOverview_RegistryPreview) | [<img src="doc/images/icons/MeasureTool.png" alt="Screen Ruler icon" height="16"> Screen Ruler](https://aka.ms/PowerToysOverview_ScreenRuler) | [<img src="doc/images/icons/Shortcut%20Guide.png" alt="Shortcut Guide icon" height="16"> Shortcut Guide](https://aka.ms/PowerToysOverview_ShortcutGuide) |
|
||||
| [<img src="doc/images/icons/PowerOCR.png" alt="Text Extractor icon" height="16"> Text Extractor](https://aka.ms/PowerToysOverview_TextExtractor) | [<img src="doc/images/icons/Workspaces.png" alt="Workspaces icon" height="16"> Workspaces](https://aka.ms/PowerToysOverview_Workspaces) | [<img src="doc/images/icons/ZoomIt.png" alt="ZoomIt icon" height="16"> ZoomIt](https://aka.ms/PowerToysOverview_ZoomIt) |
|
||||
| [<img src="doc/images/icons/Image%20Resizer.png" alt="Image Resizer icon" height="16"> Image Resizer](https://aka.ms/PowerToysOverview_ImageResizer) | [<img src="doc/images/icons/Keyboard%20Manager.png" alt="Keyboard Manager icon" height="16"> Keyboard Manager](https://aka.ms/PowerToysOverview_KeyboardManager) | [<img src="doc/images/icons/Light Switch.png" alt="Light Switch icon" height="16"> Light Switch](https://aka.ms/PowerToysOverview_LightSwitch) |
|
||||
| [<img src="doc/images/icons/Find My Mouse.png" alt="Mouse Utilities icon" height="16"> Mouse Utilities](https://aka.ms/PowerToysOverview_MouseUtilities) | [<img src="doc/images/icons/MouseWithoutBorders.png" alt="Mouse Without Borders icon" height="16"> Mouse Without Borders](https://aka.ms/PowerToysOverview_MouseWithoutBorders) | [<img src="doc/images/icons/NewPlus.png" alt="New+ icon" height="16"> New+](https://aka.ms/PowerToysOverview_NewPlus) |
|
||||
| [<img src="doc/images/icons/Peek.png" alt="Peek icon" height="16"> Peek](https://aka.ms/PowerToysOverview_Peek) | [<img src="doc/images/icons/PowerRename.png" alt="PowerRename icon" height="16"> PowerRename](https://aka.ms/PowerToysOverview_PowerRename) | [<img src="doc/images/icons/PowerToys%20Run.png" alt="PowerToys Run icon" height="16"> PowerToys Run](https://aka.ms/PowerToysOverview_PowerToysRun) |
|
||||
| [<img src="doc/images/icons/PowerAccent.png" alt="Quick Accent icon" height="16"> Quick Accent](https://aka.ms/PowerToysOverview_QuickAccent) | [<img src="doc/images/icons/Registry%20Preview.png" alt="Registry Preview icon" height="16"> Registry Preview](https://aka.ms/PowerToysOverview_RegistryPreview) | [<img src="doc/images/icons/MeasureTool.png" alt="Screen Ruler icon" height="16"> Screen Ruler](https://aka.ms/PowerToysOverview_ScreenRuler) |
|
||||
| [<img src="doc/images/icons/Shortcut%20Guide.png" alt="Shortcut Guide icon" height="16"> Shortcut Guide](https://aka.ms/PowerToysOverview_ShortcutGuide) | [<img src="doc/images/icons/PowerOCR.png" alt="Text Extractor icon" height="16"> Text Extractor](https://aka.ms/PowerToysOverview_TextExtractor) | [<img src="doc/images/icons/Workspaces.png" alt="Workspaces icon" height="16"> Workspaces](https://aka.ms/PowerToysOverview_Workspaces) |
|
||||
| [<img src="doc/images/icons/ZoomIt.png" alt="ZoomIt icon" height="16"> ZoomIt](https://aka.ms/PowerToysOverview_ZoomIt) | | |
|
||||
|
||||
|
||||
## 📋 Installation
|
||||
@@ -53,19 +54,19 @@ Choose one of the installation methods below:
|
||||
Go to the [PowerToys GitHub releases][github-release-link], click Assets to reveal the downloads, and choose the installer that matches your architecture and install scope. For most devices, that's the x64 per-user installer.
|
||||
|
||||
<!-- items that need to be updated release to release -->
|
||||
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.95%22
|
||||
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.94%22
|
||||
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.94.0/PowerToysUserSetup-0.94.0-x64.exe
|
||||
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.94.0/PowerToysUserSetup-0.94.0-arm64.exe
|
||||
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.94.0/PowerToysSetup-0.94.0-x64.exe
|
||||
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.94.0/PowerToysSetup-0.94.0-arm64.exe
|
||||
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.96%22
|
||||
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.95%22
|
||||
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.95.0/PowerToysUserSetup-0.95.0-x64.exe
|
||||
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.95.0/PowerToysUserSetup-0.95.0-arm64.exe
|
||||
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.95.0/PowerToysSetup-0.95.0-x64.exe
|
||||
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.95.0/PowerToysSetup-0.95.0-arm64.exe
|
||||
|
||||
| Description | Filename |
|
||||
|----------------|----------|
|
||||
| Per user - x64 | [PowerToysUserSetup-0.94.0-x64.exe][ptUserX64] |
|
||||
| Per user - ARM64 | [PowerToysUserSetup-0.94.0-arm64.exe][ptUserArm64] |
|
||||
| Machine wide - x64 | [PowerToysSetup-0.94.0-x64.exe][ptMachineX64] |
|
||||
| Machine wide - ARM64 | [PowerToysSetup-0.94.0-arm64.exe][ptMachineArm64] |
|
||||
| Per user - x64 | [PowerToysUserSetup-0.95.0-x64.exe][ptUserX64] |
|
||||
| Per user - ARM64 | [PowerToysUserSetup-0.95.0-arm64.exe][ptUserArm64] |
|
||||
| Machine wide - x64 | [PowerToysSetup-0.95.0-x64.exe][ptMachineX64] |
|
||||
| Machine wide - ARM64 | [PowerToysSetup-0.95.0-arm64.exe][ptMachineArm64] |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -105,175 +106,179 @@ There are [community driven install methods](./doc/unofficialInstallMethods.md)
|
||||
</details>
|
||||
|
||||
## ✨ What's new
|
||||
**Version 0.94 (September 2025)**
|
||||
**Version 0.95 (October 2025)**
|
||||
|
||||
For an in-depth look at the latest changes, visit the [Windows Command Line blog](https://aka.ms/powertoys-releaseblog).
|
||||
|
||||
**✨ Highlights**
|
||||
|
||||
- PowerToys Settings added a Settings search with fuzzy matching, suggestions, a results page, and UX polish to make finding options faster.
|
||||
- A comprehensive hotkey conflict detection system was introduced in Settings to surface and help resolve conflicting shortcuts. Note that the default hotkey settings (Win+Ctrl+Shift+T, Win+Ctrl+V, Win+Ctrl+T, Win+Shift+T) may overlap with existing Windows system shortcuts. This is expected. You can resolve the conflict by assigning different hotkeys.
|
||||
- Mouse Utilities added a “Gliding cursor” accessibility feature to Mouse Pointer Crosshairs for single‑button cursor movement and clicking. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
|
||||
- The installer was upgraded to WiX 5 after WiX 3 reached end-of-life; this move improved installer security, reliability, and community support.
|
||||
- Tons of bug fixes and improvements for Command Palette, including visual updates and new support for filters on ListPages (handy for extension developers).
|
||||
- Hosts Editor now has a “No leading spaces” option so active host entries can start at column 0 even if others are disabled. Thanks [@mohammed-saalim](https://github.com/mohammed-saalim)!
|
||||
- Context menu registration was moved from the installer to runtime to avoid loading disabled modules (runtime registrations).
|
||||
- Quick Accent now supports Maltese, and frequently used accents appear first (and are remembered across sessions). Thanks [@rovercoder](https://github.com/rovercoder)! [@davidegiacometti](https://github.com/davidegiacometti)!
|
||||
|
||||
### Always On Top
|
||||
|
||||
- Fixed the border hover cursor so it shows the arrow instead of the wait cursor. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
|
||||
- **NEW:** The **Light Switch** utility in PowerToys allows you to automatically switch between light and dark themes in Windows based on the time of day.
|
||||
- Command Palette delivered major search performance gains (new fuzzy matcher and smarter fallbacks) improving relevance and speed.
|
||||
- Peek can now be activated using just the Spacebar!
|
||||
- Find My Mouse added transparent spotlight with independent backdrop opacity, boosting focus and accessibility.
|
||||
- Settings now lets you delete shortcuts entirely and ignore conflicts.
|
||||
- Mouse Pointer Crosshairs gained orientation options (vertical / horizontal / both) for customizable accessibility. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
|
||||
- PowerRename fixed enumeration counter skipping ensuring reliable batch renames. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
- ZoomIt restored legacy draw and snipping behaviors, and fixed recording issues, improving reliability. Thanks [@chakrik73](https://github.com/chakrik73)!
|
||||
|
||||
### Command Palette
|
||||
- Applied conditional margin for icon-only tags to tighten layout. Thanks [@samrueby](https://github.com/samrueby)
|
||||
- Improved the reliability of accessing Command Palette settings through PowerToys Settings and executing other x-cmdpal:// protocol commands. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Enabled AOT by default for improved performance while simplifying publish configs.
|
||||
- Replaced service state color dots with play/pause/stop icons for enhanced accessibility. Thanks [@samrueby](https://github.com/samrueby)
|
||||
- Fixed filter dropdown sync and crash by binding SelectedValue and raising UI-thread notifications. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Ensured long links wrap correctly in details view.
|
||||
- Removed animation and enforced minimum width on filter dropdown for clarity. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Restored focus to More button after ESC closes context menu, improving keyboard flow. Thanks [@chatasweetie](https://github.com/chatasweetie)
|
||||
- Marked main and toast windows as tool windows to keep them out of Alt+Tab while preserving style. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Fixed AOT template and theming issues for filter separators. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Introduced grid layouts (small, medium, gallery) for richer page presentation.
|
||||
- Materialized result lists to avoid rescoring overhead.
|
||||
- Disabled problematic selection TextToSuggest behind environment flag.
|
||||
- Major search performance improvements (new fuzzy matcher, smarter fallbacks, fewer exceptions).
|
||||
- Added context menu "Show Details" command when details pane is hidden.
|
||||
- Reduced window flicker by avoiding unnecessary cloaking. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Restored EmptyContent rendering for blank states. Thanks [@DevLGuilherme](https://github.com/DevLGuilherme)
|
||||
- Saved new state even if prior app state file was corrupt (better resilience). Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Migrated settings window to WinUI TitleBar control. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Prevented crash on duplicate keybindings and simplified matching. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Hotkeys now always respect the “Ignore shortcut in fullscreen” setting. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Hid search box on content pages, improving focus and accessibility, and added Home title. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Blocked Ctrl+I from inserting stray tabs in search box.
|
||||
- Logged HRESULT codes in error logs for deeper diagnostics. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Advanced font and emoji icon classification and alignment improvements. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Ensured that fallback command icons are visible on the extension settings page. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Fixed breadcrumb margin misalignment (visual polish). Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Truncated overly long command labels with ellipsis to prevent overflow.
|
||||
- Added a setting to configure the page transition animation.
|
||||
- Collection of small improvements and nits for Run Commands.
|
||||
- Improved bookmarks performance and experience. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Added Ctrl+O shortcut in Clipboard History to open links directly.
|
||||
- Resolved conflict with external software that blocked Command Palette from hiding.
|
||||
- Updated context menu items to reflect name and icon changes, and ensured application icons are displayed correctly. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Added Alt+Home shortcut to return immediately to the Command Palette home page. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Fixed a crash when displaying code blocks in markdown on detail or content pages. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Fixed an issue where the search bar icon and title were not updated when rapidly switching pages. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Improved the appearance of the search box in the context menu.
|
||||
|
||||
- Applied single-click activation only to pointer input; keyboard always activates immediately. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Let context menus open at the cursor by removing window-bound constraints. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Made error messages clearer with timestamps, HRESULTs, and full details for easier diagnosis. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Prevented crashes and improved robustness when updating providers without commands. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Ensured the Settings window reliably comes to the front when opened. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Replaced the Clipboard History icon with a colorful Fluent icon. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Hardened ContentIcon to avoid duplicate parenting and improve diagnostics. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Standardized null checks using C# pattern matching for safer behavior.
|
||||
- Improved accessibility by focusing the activation shortcut dialog and making text reachable. Thanks [@chatasweetie](https://github.com/chatasweetie)!
|
||||
- Moved the extension SDK to a stable Windows SDK and cleaned up message namespaces.
|
||||
- Added path shortcuts: ~ to home, and / or \\ to system root, plus UNC support. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
|
||||
- Fixed a race in cancellation handling to avoid InvalidOperationException. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Aligned separator styling with WinUI 3 for consistent visuals. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added ARM64 PDBs to the Extensions SDK NuGet for better debugging.
|
||||
- Added single-select filters to DynamicListPage and updated Windows Services sample.
|
||||
- Updated main page placeholder text to better describe what can be searched. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Removed explicit WinAppSDK/WebView2 dependencies from toolkit and API. Thanks [@rluengen](https://github.com/rluengen)!
|
||||
- Added a local keyboard hook to handle the GoBack key reliably. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Propagated alias changes safely and resolved conflicts across view models.
|
||||
- Allowed providers to override Dispose with a virtual method.
|
||||
- Fixed memory leaks by cleaning up removed or cancelled list items.
|
||||
- Sorted DateTime extension results by relevance for better usability.
|
||||
- Reduced search text "jiggling" by avoiding redundant change notifications.
|
||||
- Centralized automation notifications in a UIHelper for better accessibility. Thanks [@chatasweetie](https://github.com/chatasweetie)!
|
||||
- Preserved Adaptive Card action types during trimming via DynamicDependency.
|
||||
- Added an acrylic backdrop and refined styling to the context menu. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Prevented disposed pages and Settings windows from handling stale messages. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Made the extension API easier to evolve without breaking clients.
|
||||
- Added "evil" sample pages to help reproduce tricky bugs.
|
||||
- Fixed WinGet trim-safety issues by replacing LINQ with manual iteration.
|
||||
- Cancelled stale list fetches to avoid older results overwriting newer ones in CmdPal.
|
||||
|
||||
### Command Palette extensions
|
||||
### Command Palette Extensions
|
||||
- Replaced localized WebSearch setting keys with stable literals and numeric history count. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Enabled advanced markdown tables and emphasis extensions. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added setting to choose Clipboard History primary action (Paste vs Copy). Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Added actionable empty-state hints for File Search (search PC / open indexing settings). Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Ensured all WinGet extension assets copy reliably to output. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Improved Run command line parsing for paths with spaces; sped up related tests.
|
||||
- Updated WebSearch extension icon set for enhanced clarity and contrast. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added Terminal profile sort order setting including MRU tracking. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added Uninstall Application command (UWP direct, Win32 via Settings). Thanks [@mKpwnz](https://github.com/mKpwnz)!
|
||||
- Deferred WinGet details loading and added timing logs.
|
||||
- Removed LINQ from All Apps extension for performance.
|
||||
- Added standardized key chord system + shortcuts to File Search commands. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added Terminal channel filter & remembered selection option. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Enabled loading local/data/app images in markdown with sizing hints. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added external extension reload via x-cmdpal://reload (configurable). Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Instant WebSearch history updates with in-memory store & events. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added keep-after-paste option and safe delete with confirmation for Clipboard History. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
|
||||
- Improved empty states and ranking logic for multiple extensions. Thanks [@htcfreek](https://github.com/htcfreek)!
|
||||
- Added app icons to the All Apps "Run" context command when available.
|
||||
- Restored missing builtin icons by standardizing extension dependencies.
|
||||
- Unblocked local deployment by adding WinAppSDK to two sample extensions.
|
||||
### Environment Variables
|
||||
- Replaced custom window chrome with WinUI TitleBar for cleaner, maintainable Environment Variables UI.
|
||||
|
||||
### File Locksmith
|
||||
- Adopted WinUI TitleBar to simplify window chrome while preserving appearance.
|
||||
|
||||
### Find My Mouse
|
||||
- Added transparent spotlight support with separate backdrop opacity; migrated to Windows App SDK composition APIs.
|
||||
|
||||
### Hosts File Editor
|
||||
- Migrated to native WinUI TitleBar for cleaner, maintainable window chrome.
|
||||
|
||||
- Added a "No leading spaces" option so active hosts entries can start at column 0 even when others are disabled. Thanks [@mohammed-saalim](https://github.com/mohammed-saalim)!
|
||||
### Light Switch
|
||||
- Introduced as a brand-new PowerToy module.
|
||||
- Automatically switches between light and dark themes.
|
||||
- Supports time-based scheduling or location-based sunrise/sunset switching.
|
||||
- Supports using a keyboard shortcut to force a change.
|
||||
- Supports filtering changes for Apps and/or System Theme.
|
||||
|
||||
### Image Resizer
|
||||
|
||||
- Fixed Image Resizer localization by installing satellite resources under the WinUI 3 apps culture path.
|
||||
|
||||
### Mouse Utilities
|
||||
|
||||
- Introduced "Gliding cursor" to control the pointer and click with a single hotkey for better accessibility. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
|
||||
### Mouse Pointer Crosshairs
|
||||
- Added Esc key to cancel active gliding cursor sequence. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
|
||||
- Added orientation option (vertical / horizontal / both) for crosshairs customization. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
|
||||
|
||||
### Mouse Without Borders
|
||||
- Continued Common class refactor (part 5/7) by extracting clipboard and init/cleanup logic into focused classes. Thanks [@mikeclayton](https://github.com/mikeclayton)!
|
||||
|
||||
- Blocked Easy Mouse from switching machines during fullscreen apps, with an allow-list for exceptions. Thanks [@dot-tb](https://github.com/dot-tb)!
|
||||
- Fix connection failures caused by conflicting MachineId across machines. Thanks [@noraa-junker](https://github.com/noraa-junker) for troubleshooting!
|
||||
|
||||
### Peek
|
||||
|
||||
- Added Visual Studio shared project file types to XML preview and fixed bgcode handler registration. Thanks [@rezanid](https://github.com/rezanid)!
|
||||
- Fixes bgcode preview handler registration and events for reliable previews. Thanks [@pedrolamas](https://github.com/pedrolamas)!
|
||||
- Added the option to activate Peek with just the Spacebar.
|
||||
|
||||
### PowerRename
|
||||
|
||||
- Changed the Explorer accelerator key to PowErRename to avoid clashing with the New menu. Thanks [@aaron-ni](https://github.com/aaron-ni)!
|
||||
- Fixed enumeration counter skipping when regex replacement equals original filename (counters now advance reliably). Thanks [@daverayment](https://github.com/daverayment)!
|
||||
|
||||
### Quick Accent
|
||||
- Expanded Welsh layout with acute, grave, and dieresis variants for vowels (consistent ordering). Thanks [@PesBandi](https://github.com/PesBandi)!
|
||||
|
||||
- Remembered character usage across sessions so frequently used accents appear first. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
|
||||
- Added Maltese language support with specific characters and the Euro symbol. Thanks [@rovercoder](https://github.com/rovercoder)!
|
||||
- Reduced GPU usage issues by making the window Topmost only when the picker is visible. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
### Registry Preview
|
||||
- Migrated to native TitleBar and AppWindow APIs for cleaner window chrome.
|
||||
|
||||
### Screen Ruler
|
||||
- Fixed ARM64 crash by aligning cursor position structure to 8-byte boundary.
|
||||
|
||||
### Settings
|
||||
- Added ability to ignore specific hotkey conflicts to reduce noise.
|
||||
- Stopped creating backup directory during dry-run status checks (cleaner first-run).
|
||||
- Standardized casing and localization for ZoomIt and modules header.
|
||||
- Improved search results page accessibility and conditional module grouping.
|
||||
|
||||
- Added telemetry to track usage of the new shortcut conflict detection workflow.
|
||||
- Moved the shutdown action from the title bar to a footer menu item with confirmation. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
|
||||
- Implemented comprehensive hotkey conflict detection with a dedicated resolution dialog.
|
||||
- Added branded visuals for Office and Copilot keys in the KeyVisual control.
|
||||
- Introduced Settings search with fuzzy matching and navigation to specific controls.
|
||||
- Corrected Spanish localization so product names like Awake remain in English across Settings and OOBE.
|
||||
- Simplified the Advanced Paste description in Settings for quicker reading and consistent capitalization. Thanks [@OldUser101](https://github.com/OldUser101)!
|
||||
- Localized conflict messages in the conflict window and dialog.
|
||||
### ZoomIt
|
||||
- Updated resource file to reflect standalone v9.01 and current copyright year. Thanks [@foxmsft](https://github.com/foxmsft)!
|
||||
- Restored legacy draw/snipping behaviors and fixed recording race conditions. Thanks [@chakrik73](https://github.com/chakrik73)!
|
||||
- Added smooth image option for improved zoom quality using GDI+ for static zoom and Magnifier API for live zoom. Thanks [@markrussinovich](https://github.com/markrussinovich)!
|
||||
|
||||
### Installer
|
||||
### Documentation
|
||||
- New Microsoft Learn documentation for the Light Switch module.
|
||||
- New dev docs for the Light Switch module.
|
||||
|
||||
- Upgraded the installer to WiX 5 with silent "Files in Use" handling for smoother winget installs.
|
||||
- Switched Win10 context menu modules to runtime registration and added cleanup on uninstall to avoid stale entries.
|
||||
### Development (Area-Build & Area-Tests)
|
||||
- Allowed debug launches to continue when modules fail to load, speeding developer iteration.
|
||||
- Fixed spell checker dictionary entry (advapi) to eliminate false error.
|
||||
- Added VS Code development guide and launch configs to streamline cross-editor workflows.
|
||||
- Upgraded Windows App SDK and related dependencies to 1.8 for newer platform features.
|
||||
- Rewrote YAML comment to resolve new spell checker forbidden pattern. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Corrected solution structure by returning misplaced Common project, reducing build confusion.
|
||||
- Modernized build scripts with shared helpers and VS environment autodetection for simpler CLI builds.
|
||||
- Standardized build scripts and platform detection to improve reliability and reuse.
|
||||
- Added missing Command Palette version bump to align module release cadence.
|
||||
- Added EXECUTEDEFAULT term to dictionary to prevent regression build failures. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Introduced nightly pre-warm pipeline and configurable MSBuild cache mode to improve CI performance.
|
||||
- Resolved CI forbidden pattern spelling complaint to keep pipelines green.
|
||||
- Added AI contributor instruction set to clarify code area expectations.
|
||||
- Added accessibility IDs to settings and FancyZones toggles, stabilizing UI tests.
|
||||
- Added automatic log collection on UI test failures to speed root cause analysis.
|
||||
- Stabilized Mouse Utils tests by switching to AccessibilityId selectors.
|
||||
- Added Screen Ruler UI test coverage to validate core measurement workflows.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Adds docs for building the installer locally and testing winget installs.
|
||||
- Fixed a broken style guide link in developer documentation. Thanks [@denizmaral](https://github.com/denizmaral)!
|
||||
|
||||
### Development
|
||||
|
||||
- Excluded test and coverage DLLs from BinSkim scans to cut false positives and speed up security analysis.
|
||||
- Simplified NOTICE maintenance by removing version numbers and filtering out Microsoft/System packages.
|
||||
- Improved NuGet dependency validation to prevent package downgrades and catch issues during restore.
|
||||
- Updated UTF.Unknown to a modern version to improve compatibility without breaking changes. Thanks [@304NotModified](https://github.com/304NotModified)!
|
||||
- Refreshed package catalog in CI before installing dependencies to prevent Linux workflow failures.
|
||||
- Refactored CmdPal tests with dependency injection and added coverage for queries and settings.
|
||||
- Added unit tests to verify Close on Enter swaps Copy/Save as expected. Thanks [@mohammed-saalim](https://github.com/mohammed-saalim)!
|
||||
- Added accessibility IDs to CmdPal UI for stable UI tests.
|
||||
- Rewrote system command tests with a new test base and cleaner patterns.
|
||||
- Added unit tests for WebSearch and Shell extensions with mockable settings.
|
||||
- Added unit tests and abstractions for Apps and Bookmarks extensions.
|
||||
- Cleans up AI-generated tests; adds meaningful query tests across extensions.
|
||||
- Removed the obsolete debug dialog from Settings for a smoother developer loop.
|
||||
|
||||
## 🛣️ Roadmap
|
||||
|
||||
For [v0.95][github-next-release-work], we'll work on the items below:
|
||||
|
||||
- Continued Command Palette polish
|
||||
- Working on Shortcut Guide v2 (Thanks [@noraa-junker](https://github.com/noraa-junker)!)
|
||||
- Upgrading Keyboard Manager's editor UI
|
||||
- UI tweaking utility with day/night theme switcher
|
||||
- DSC v3 support for top utilities
|
||||
- New UI automation tests
|
||||
- Stability, bug fixes
|
||||
|
||||
## ❤️ PowerToys Community
|
||||
## 🛣️ Roadmap
|
||||
We are planning some nice new features and improvements for the next releases – a revamped Keyboard Manager UI, custom endpoint and local model support for Advanced Paste, Command Palette improvements and a brand-new Shortcut Guide experience! Stay tuned for [v0.96][github-next-release-work]!
|
||||
|
||||
## ❤️ PowerToys Community
|
||||
The PowerToys team is extremely grateful to have the [support of an amazing active community][community-link]. The work you do is incredibly important. PowerToys wouldn't be nearly what it is today without your help filing bugs, updating documentation, guiding the design, or writing features. We want to say thank you and take time to recognize your work. Your contributions and feedback improve PowerToys month after month!
|
||||
|
||||
## Contributing
|
||||
## Contributing
|
||||
This project welcomes contributions of all types. Besides coding features / bug fixes, other ways to assist include spec writing, design, documentation, and finding bugs. We are excited to work with the power user community to build a set of tools for helping you get the most out of Windows. We ask that **before you start work on a feature that you would like to contribute**, please read our [Contributor's Guide](CONTRIBUTING.md). We would be happy to work with you to figure out the best approach, provide guidance and mentorship throughout feature development, and help avoid any wasted or duplicate effort. Most contributions require you to agree to a [Contributor License Agreement (CLA)][oss-CLA] declaring that you grant us the rights to use your contribution and that you have permission to do so. For guidance on developing for PowerToys, please read the [developer docs](./doc/devdocs) for a detailed breakdown. This includes how to setup your computer to compile.
|
||||
|
||||
This project welcomes contributions of all types. Besides coding features / bug fixes, other ways to assist include spec writing, design, documentation, and finding bugs. We are excited to work with the power user community to build a set of tools for helping you get the most out of Windows.
|
||||
## Code of Conduct
|
||||
This project has adopted the [Microsoft Open Source Code of Conduct][oss-conduct-code].
|
||||
|
||||
We ask that **before you start work on a feature that you would like to contribute**, please read our [Contributor's Guide](CONTRIBUTING.md). We would be happy to work with you to figure out the best approach, provide guidance and mentorship throughout feature development, and help avoid any wasted or duplicate effort.
|
||||
## Privacy Statement
|
||||
The application logs basic diagnostic data (telemetry). For more privacy information and what we collect, see our [PowerToys Data and Privacy documentation](https://aka.ms/powertoys-data-and-privacy-documentation).
|
||||
|
||||
Most contributions require you to agree to a [Contributor License Agreement (CLA)][oss-CLA] declaring that you grant us the rights to use your contribution and that you have permission to do so.
|
||||
|
||||
For guidance on developing for PowerToys, please read the [developer docs](./doc/devdocs) for a detailed breakdown. This includes how to setup your computer to compile.
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
This project has adopted the [Microsoft Open Source Code of Conduct][oss-conduct-code].
|
||||
|
||||
## Privacy Statement
|
||||
|
||||
The application logs basic diagnostic data (telemetry). For more privacy information and what we collect, see our [PowerToys Data and Privacy documentation](https://aka.ms/powertoys-data-and-privacy-documentation).
|
||||
|
||||
[oss-CLA]: https://cla.opensource.microsoft.com
|
||||
[oss-conduct-code]: CODE_OF_CONDUCT.md
|
||||
[community-link]: COMMUNITY.md
|
||||
[github-release-link]: https://aka.ms/installPowerToys
|
||||
[microsoft-store-link]: https://aka.ms/getPowertoys
|
||||
[winget-link]: https://github.com/microsoft/winget-cli#installing-the-client
|
||||
[oss-CLA]: https://cla.opensource.microsoft.com
|
||||
[oss-conduct-code]: CODE_OF_CONDUCT.md
|
||||
[community-link]: COMMUNITY.md
|
||||
[github-release-link]: https://aka.ms/installPowerToys
|
||||
[microsoft-store-link]: https://aka.ms/getPowertoys
|
||||
[winget-link]: https://github.com/microsoft/winget-cli#installing-the-client
|
||||
[roadmap]: https://github.com/microsoft/PowerToys/wiki/Roadmap
|
||||
[privacy-link]: http://go.microsoft.com/fwlink/?LinkId=521839
|
||||
[loc-bug]: https://github.com/microsoft/PowerToys/issues/new?assignees=&labels=&template=translation_issue.md&title=
|
||||
[usingPowerToys-docs-link]: https://aka.ms/powertoys-docs
|
||||
[privacy-link]: http://go.microsoft.com/fwlink/?LinkId=521839
|
||||
[loc-bug]: https://github.com/microsoft/PowerToys/issues/new?assignees=&labels=&template=translation_issue.md&title=
|
||||
[usingPowerToys-docs-link]: https://aka.ms/powertoys-docs
|
||||
@@ -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>
|
||||
@@ -150,7 +150,7 @@ namespace LightSwitch.UITests
|
||||
var modeCombobox = testBase.Session.Find<Element>(By.AccessibilityId("ModeSelection_LightSwitch"), 5000);
|
||||
Assert.IsNotNull(modeCombobox, "Mode combobox not found.");
|
||||
|
||||
var neededTabs = 5;
|
||||
var neededTabs = 6;
|
||||
|
||||
if (modeCombobox.Text != "Manual")
|
||||
{
|
||||
@@ -167,7 +167,7 @@ namespace LightSwitch.UITests
|
||||
Assert.IsNotNull(timeline, "Timeline not found.");
|
||||
|
||||
var helpText = timeline.GetAttribute("HelpText");
|
||||
string originalStartValue = GetHelpTextValue(helpText, "Start");
|
||||
string originalEndValue = GetHelpTextValue(helpText, "End");
|
||||
|
||||
for (int i = 0; i < neededTabs; i++)
|
||||
{
|
||||
@@ -179,12 +179,12 @@ namespace LightSwitch.UITests
|
||||
testBase.Session.SendKeys(Key.Enter);
|
||||
|
||||
helpText = timeline.GetAttribute("HelpText");
|
||||
string updatedStartValue = GetHelpTextValue(helpText, "Start");
|
||||
string updatedEndValue = GetHelpTextValue(helpText, "End");
|
||||
|
||||
Assert.AreNotEqual(originalStartValue, updatedStartValue, "Timeline start time should have been updated.");
|
||||
Assert.AreNotEqual(originalEndValue, updatedEndValue, "Timeline end time should have been updated.");
|
||||
|
||||
helpText = timeline.GetAttribute("HelpText");
|
||||
string originalEndValue = GetHelpTextValue(helpText, "End");
|
||||
string originalStartValue = GetHelpTextValue(helpText, "Start");
|
||||
|
||||
testBase.Session.SendKeys(Key.Tab);
|
||||
testBase.Session.SendKeys(Key.Enter);
|
||||
@@ -192,9 +192,9 @@ namespace LightSwitch.UITests
|
||||
testBase.Session.SendKeys(Key.Enter);
|
||||
|
||||
helpText = timeline.GetAttribute("HelpText");
|
||||
string updatedEndValue = GetHelpTextValue(helpText, "End");
|
||||
string updatedStartValue = GetHelpTextValue(helpText, "Start");
|
||||
|
||||
Assert.AreNotEqual(originalEndValue, updatedEndValue, "Timeline end time should have been updated.");
|
||||
Assert.AreNotEqual(originalStartValue, updatedStartValue, "Timeline start time should have been updated.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -259,11 +259,7 @@ namespace LightSwitch.UITests
|
||||
// Click the select city button
|
||||
var setLocationButton = testBase.Session.Find<Element>(By.AccessibilityId("SetLocationButton_LightSwitch"), 5000);
|
||||
Assert.IsNotNull(setLocationButton, "Set location button not found.");
|
||||
setLocationButton.Click();
|
||||
|
||||
var syncLocationButton = testBase.Session.Find<Element>(By.AccessibilityId("SyncLocationButton_LightSwitch"), 5000);
|
||||
Assert.IsNotNull(syncLocationButton, "Sync location button not found.");
|
||||
syncLocationButton.Click(msPostAction: 8000);
|
||||
setLocationButton.Click(msPostAction: 8000);
|
||||
|
||||
var latLong = testBase.Session.Find<Element>(By.AccessibilityId("LocationResultText_LightSwitch"), 5000);
|
||||
Assert.IsFalse(string.IsNullOrWhiteSpace(latLong.Text));
|
||||
@@ -305,6 +301,7 @@ namespace LightSwitch.UITests
|
||||
string originalStartValue = GetHelpTextValue(helpText, "Start");
|
||||
|
||||
sunriseOffset.Click();
|
||||
testBase.Session.SendKeys(Key.Up);
|
||||
|
||||
helpText = timeline.GetAttribute("HelpText");
|
||||
string updatedStartValue = GetHelpTextValue(helpText, "Start");
|
||||
@@ -319,6 +316,7 @@ namespace LightSwitch.UITests
|
||||
string originalEndValue = GetHelpTextValue(helpText, "End");
|
||||
|
||||
sunsetOffset.Click();
|
||||
testBase.Session.SendKeys(Key.Up);
|
||||
|
||||
helpText = timeline.GetAttribute("HelpText");
|
||||
string updatedEndValue = GetHelpTextValue(helpText, "End");
|
||||
@@ -338,9 +336,15 @@ namespace LightSwitch.UITests
|
||||
var scrollViewer = testBase.Session.Find<Element>(By.AccessibilityId("PageScrollViewer"));
|
||||
systemCheckbox.EnsureVisible(scrollViewer);
|
||||
|
||||
// How do I handle when something is off screen?
|
||||
int neededTabs = 10;
|
||||
|
||||
if (!systemCheckbox.Selected)
|
||||
{
|
||||
for (int i = 0; i < neededTabs; i++)
|
||||
{
|
||||
testBase.Session.SendKeys(Key.Tab);
|
||||
}
|
||||
|
||||
systemCheckbox.Click();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,7 +244,36 @@ public partial class MainListPage : DynamicListPage,
|
||||
var commands = _tlcManager.TopLevelCommands;
|
||||
lock (commands)
|
||||
{
|
||||
UpdateFallbacks(SearchText, commands.ToImmutableArray(), token);
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// prefilter fallbacks
|
||||
var specialFallbacks = new List<TopLevelViewModel>(_specialFallbacks.Length);
|
||||
var commonFallbacks = new List<TopLevelViewModel>();
|
||||
|
||||
foreach (var s in commands)
|
||||
{
|
||||
if (!s.IsFallback)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_specialFallbacks.Contains(s.CommandProviderId))
|
||||
{
|
||||
specialFallbacks.Add(s);
|
||||
}
|
||||
else
|
||||
{
|
||||
commonFallbacks.Add(s);
|
||||
}
|
||||
}
|
||||
|
||||
// start update of fallbacks; update special fallbacks separately,
|
||||
// so they can finish faster
|
||||
UpdateFallbacks(SearchText, specialFallbacks, token);
|
||||
UpdateFallbacks(SearchText, commonFallbacks, token);
|
||||
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
@@ -244,9 +284,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 +293,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 +307,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)
|
||||
{
|
||||
@@ -311,15 +345,12 @@ public partial class MainListPage : DynamicListPage,
|
||||
// with a list of all our commands & apps.
|
||||
if (!newFilteredItems.Any() && !newApps.Any())
|
||||
{
|
||||
// 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
|
||||
// consider them when filtering the list.
|
||||
newFallbacks = commands.Where(s => s.IsFallback && !_specialFallbacks.Contains(s.CommandProviderId));
|
||||
newFallbacks = commonFallbacks;
|
||||
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
@@ -330,7 +361,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 +383,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 +419,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 +486,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 +562,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 +581,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; }
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.Globalization;
|
||||
using Microsoft.PowerToys.Settings.UI.Helpers;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Automation;
|
||||
using Microsoft.UI.Xaml.Automation.Peers;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Shapes;
|
||||
using Windows.Foundation;
|
||||
@@ -79,6 +80,11 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
|
||||
this.IsEnabledChanged += Timeline_IsEnabledChanged;
|
||||
}
|
||||
|
||||
protected override AutomationPeer OnCreateAutomationPeer()
|
||||
{
|
||||
return new TimelineAutomationPeer(this);
|
||||
}
|
||||
|
||||
private void Timeline_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
CheckEnabledState();
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Automation;
|
||||
using Microsoft.UI.Xaml.Automation.Peers;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Controls
|
||||
{
|
||||
public partial class TimelineAutomationPeer : FrameworkElementAutomationPeer
|
||||
{
|
||||
public TimelineAutomationPeer(Timeline owner)
|
||||
: base(owner)
|
||||
{
|
||||
}
|
||||
|
||||
protected override string GetClassNameCore() => "Timeline";
|
||||
|
||||
protected override AutomationControlType GetAutomationControlTypeCore()
|
||||
=> AutomationControlType.Custom;
|
||||
|
||||
protected override string GetAutomationIdCore()
|
||||
{
|
||||
var owner = (Timeline)Owner;
|
||||
var id = AutomationProperties.GetAutomationId(owner);
|
||||
return string.IsNullOrEmpty(id) ? base.GetAutomationIdCore() : id;
|
||||
}
|
||||
|
||||
protected override string GetNameCore()
|
||||
{
|
||||
var owner = (Timeline)Owner;
|
||||
var name = AutomationProperties.GetName(owner);
|
||||
return !string.IsNullOrEmpty(name)
|
||||
? name
|
||||
: $"Timeline from {owner.StartTime} to {owner.EndTime}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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" }
|
||||
}
|
||||
Reference in New Issue
Block a user