mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-01-02 10:26:22 +01:00
Compare commits
13 Commits
dev/mjolle
...
crutkas-pa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8038df1b62 | ||
|
|
0de60445ea | ||
|
|
47d4a65223 | ||
|
|
1b72c0b969 | ||
|
|
9160c82fc2 | ||
|
|
452e0dcf51 | ||
|
|
2c9a9e9fca | ||
|
|
09c8c1d79a | ||
|
|
95c8a83f79 | ||
|
|
2830ea919c | ||
|
|
725ad21952 | ||
|
|
ebc3a139c5 | ||
|
|
28dba2633e |
108
.github/actions/spell-check/expect.txt
vendored
108
.github/actions/spell-check/expect.txt
vendored
@@ -2,8 +2,8 @@ AAAAs
|
||||
abcdefghjkmnpqrstuvxyz
|
||||
abgr
|
||||
ABlocked
|
||||
ABOUTBOX
|
||||
ABORTIFHUNG
|
||||
ABOUTBOX
|
||||
Abug
|
||||
Acceleratorkeys
|
||||
ACCEPTFILES
|
||||
@@ -97,8 +97,8 @@ ASSOCSTR
|
||||
ASYNCWINDOWPLACEMENT
|
||||
ASYNCWINDOWPOS
|
||||
atl
|
||||
ATX
|
||||
ATRIOX
|
||||
ATX
|
||||
aumid
|
||||
authenticode
|
||||
AUTOBUDDY
|
||||
@@ -117,10 +117,10 @@ azman
|
||||
azureaiinference
|
||||
azureinference
|
||||
azureopenai
|
||||
backticks
|
||||
bbwe
|
||||
BCIE
|
||||
bck
|
||||
backticks
|
||||
BESTEFFORT
|
||||
bezelled
|
||||
bhid
|
||||
@@ -148,8 +148,8 @@ bmi
|
||||
BNumber
|
||||
BODGY
|
||||
BOklab
|
||||
Bootstrappers
|
||||
BOOTSTRAPPERINSTALLFOLDER
|
||||
Bootstrappers
|
||||
BOTTOMALIGN
|
||||
boxmodel
|
||||
BPBF
|
||||
@@ -176,17 +176,16 @@ BYPOSITION
|
||||
CALCRECT
|
||||
CALG
|
||||
callbackptr
|
||||
cabstr
|
||||
calpwstr
|
||||
caub
|
||||
Cangjie
|
||||
CANRENAME
|
||||
Carlseibert
|
||||
Canvascustomlayout
|
||||
CAPTUREBLT
|
||||
CAPTURECHANGED
|
||||
CARETBLINKING
|
||||
Carlseibert
|
||||
CAtl
|
||||
caub
|
||||
CBN
|
||||
cch
|
||||
CCHDEVICENAME
|
||||
@@ -206,11 +205,9 @@ changecursor
|
||||
CHILDACTIVATE
|
||||
CHILDWINDOW
|
||||
CHOOSEFONT
|
||||
CIBUILD
|
||||
cidl
|
||||
CIELCh
|
||||
cim
|
||||
claude
|
||||
CImage
|
||||
cla
|
||||
CLASSDC
|
||||
@@ -264,7 +261,6 @@ CONFIGW
|
||||
CONFLICTINGMODIFIERKEY
|
||||
CONFLICTINGMODIFIERSHORTCUT
|
||||
CONOUT
|
||||
coreclr
|
||||
constexpr
|
||||
contentdialog
|
||||
contentfiles
|
||||
@@ -276,6 +272,7 @@ copiedcolorrepresentation
|
||||
coppied
|
||||
copyable
|
||||
COPYPEN
|
||||
coreclr
|
||||
COREWINDOW
|
||||
Corpor
|
||||
cotaskmem
|
||||
@@ -284,18 +281,18 @@ countof
|
||||
covrun
|
||||
cpcontrols
|
||||
cph
|
||||
cppcoreguidelines
|
||||
cplusplus
|
||||
CPower
|
||||
cppcoreguidelines
|
||||
cpptools
|
||||
cppvsdbg
|
||||
cppwinrt
|
||||
createdump
|
||||
creativecommons
|
||||
CREATEPROCESS
|
||||
CREATESCHEDULEDTASK
|
||||
CREATESTRUCT
|
||||
CREATEWINDOWFAILED
|
||||
creativecommons
|
||||
CRECT
|
||||
CRH
|
||||
critsec
|
||||
@@ -331,7 +328,6 @@ CYSCREEN
|
||||
CYSMICON
|
||||
CYVIRTUALSCREEN
|
||||
Czechia
|
||||
cziplib
|
||||
Dac
|
||||
dacl
|
||||
DAffine
|
||||
@@ -355,9 +351,7 @@ Deact
|
||||
debugbreak
|
||||
decryptor
|
||||
Dedup
|
||||
dfx
|
||||
Deduplicator
|
||||
Deeplink
|
||||
DEFAULTBOOTSTRAPPERINSTALLFOLDER
|
||||
DEFAULTCOLOR
|
||||
DEFAULTFLAGS
|
||||
@@ -404,7 +398,6 @@ DISPLAYFREQUENCY
|
||||
displayname
|
||||
DISPLAYORIENTATION
|
||||
divyan
|
||||
djwsxzxb
|
||||
Dlg
|
||||
DLGFRAME
|
||||
DLGMODALFRAME
|
||||
@@ -417,7 +410,6 @@ DONTVALIDATEPATH
|
||||
dotnet
|
||||
downsampled
|
||||
downsampling
|
||||
Downsampled
|
||||
downscale
|
||||
DPICHANGED
|
||||
DPIs
|
||||
@@ -531,7 +523,6 @@ EXTRINSICPROPERTIES
|
||||
eyetracker
|
||||
FANCYZONESDRAWLAYOUTTEST
|
||||
FANCYZONESEDITOR
|
||||
FNumber
|
||||
FARPROC
|
||||
fdx
|
||||
fesf
|
||||
@@ -563,8 +554,8 @@ FIXEDSYS
|
||||
flac
|
||||
flyouts
|
||||
FMask
|
||||
foundrylocal
|
||||
fmtid
|
||||
FNumber
|
||||
FOF
|
||||
FOFX
|
||||
FOLDERID
|
||||
@@ -575,6 +566,7 @@ FORCEMINIMIZE
|
||||
FORMATDLGORD
|
||||
formatetc
|
||||
FORPARSING
|
||||
foundrylocal
|
||||
FRAMECHANGED
|
||||
frm
|
||||
FROMTOUCH
|
||||
@@ -593,13 +585,13 @@ gdi
|
||||
gdiplus
|
||||
GDIPVER
|
||||
GDISCALED
|
||||
geolocator
|
||||
GETCLIENTAREAANIMATION
|
||||
GETCURSEL
|
||||
GETDESKWALLPAPER
|
||||
GETDLGCODE
|
||||
GETDPISCALEDSIZE
|
||||
getfilesiginforedist
|
||||
geolocator
|
||||
GETHOTKEY
|
||||
GETICON
|
||||
GETLBTEXT
|
||||
@@ -610,11 +602,12 @@ GETSCREENSAVERRUNNING
|
||||
GETSECKEY
|
||||
GETSTICKYKEYS
|
||||
GETTEXTLENGTH
|
||||
GIFs
|
||||
gitmodules
|
||||
GHND
|
||||
gitmodules
|
||||
GMEM
|
||||
GNumber
|
||||
googleai
|
||||
googlegemini
|
||||
gpedit
|
||||
gpo
|
||||
GPOCA
|
||||
@@ -631,8 +624,6 @@ GValue
|
||||
gwl
|
||||
GWLP
|
||||
GWLSTYLE
|
||||
googleai
|
||||
googlegemini
|
||||
hangeul
|
||||
Hanzi
|
||||
Hardlines
|
||||
@@ -743,9 +734,7 @@ IDCANCEL
|
||||
IDD
|
||||
idk
|
||||
idl
|
||||
IIM
|
||||
idlist
|
||||
ifd
|
||||
IDOK
|
||||
IDOn
|
||||
IDR
|
||||
@@ -754,15 +743,16 @@ ietf
|
||||
IEXPLORE
|
||||
IFACEMETHOD
|
||||
IFACEMETHODIMP
|
||||
ifd
|
||||
IGNOREUNKNOWN
|
||||
IGo
|
||||
iid
|
||||
IIM
|
||||
Iindex
|
||||
Ijwhost
|
||||
ILD
|
||||
IMAGEHLP
|
||||
IMAGERESIZERCONTEXTMENU
|
||||
IPTC
|
||||
IMAGERESIZEREXT
|
||||
imageresizerinput
|
||||
imageresizersettings
|
||||
@@ -798,7 +788,6 @@ INSTALLFOLDERTOPREVIOUSINSTALLFOLDER
|
||||
INSTALLLOCATION
|
||||
INSTALLMESSAGE
|
||||
INSTALLPROPERTY
|
||||
installscopeperuser
|
||||
INSTALLSTARTMENUSHORTCUT
|
||||
INSTALLSTATE
|
||||
Inste
|
||||
@@ -811,6 +800,7 @@ invokecommand
|
||||
ipcmanager
|
||||
IPREVIEW
|
||||
ipreviewhandlervisualssetfont
|
||||
IPTC
|
||||
irow
|
||||
irprops
|
||||
isbi
|
||||
@@ -854,15 +844,14 @@ keyvault
|
||||
KILLFOCUS
|
||||
killrunner
|
||||
kmph
|
||||
ksa
|
||||
kvp
|
||||
Kybd
|
||||
LARGEICON
|
||||
lastcodeanalysissucceeded
|
||||
LASTEXITCODE
|
||||
LAYOUTRTL
|
||||
LCh
|
||||
lbl
|
||||
LCh
|
||||
lcid
|
||||
LCIDTo
|
||||
lcl
|
||||
@@ -878,10 +867,10 @@ LExit
|
||||
lhwnd
|
||||
LIBFUZZER
|
||||
LIBID
|
||||
lightswitch
|
||||
LIMITSIZE
|
||||
LIMITTEXT
|
||||
lindex
|
||||
lightswitch
|
||||
linkid
|
||||
LINKOVERLAY
|
||||
LINQTo
|
||||
@@ -892,6 +881,7 @@ LLKH
|
||||
llkhf
|
||||
LMEM
|
||||
LMENU
|
||||
lng
|
||||
LOADFROMFILE
|
||||
LOBYTE
|
||||
localappdata
|
||||
@@ -901,17 +891,14 @@ LOCATIONCHANGE
|
||||
LOCKTYPE
|
||||
LOGFONT
|
||||
LOGFONTW
|
||||
logon
|
||||
lon
|
||||
LOGMSG
|
||||
logon
|
||||
LOGPIXELSX
|
||||
LOGPIXELSY
|
||||
lng
|
||||
lon
|
||||
longdate
|
||||
LONGNAMES
|
||||
lowlevel
|
||||
lquadrant
|
||||
LOWORD
|
||||
lparam
|
||||
LPBITMAPINFOHEADER
|
||||
@@ -945,6 +932,7 @@ lpv
|
||||
LPW
|
||||
lpwcx
|
||||
lpwndpl
|
||||
lquadrant
|
||||
LReader
|
||||
LRESULT
|
||||
LSTATUS
|
||||
@@ -971,6 +959,7 @@ MAKELONG
|
||||
MAKELPARAM
|
||||
makepri
|
||||
MAKEWPARAM
|
||||
Malware
|
||||
manifestdependency
|
||||
MAPPEDTOSAMEKEY
|
||||
MAPTOSAMESHORTCUT
|
||||
@@ -993,8 +982,8 @@ MENUITEMINFO
|
||||
MENUITEMINFOW
|
||||
MERGECOPY
|
||||
MERGEPAINT
|
||||
Metadatas
|
||||
metadatamatters
|
||||
Metadatas
|
||||
metafile
|
||||
mfc
|
||||
Mgmt
|
||||
@@ -1040,9 +1029,6 @@ mousepointer
|
||||
mouseutils
|
||||
MOVESIZEEND
|
||||
MOVESIZESTART
|
||||
muxx
|
||||
muxxc
|
||||
muxxh
|
||||
MRM
|
||||
MRT
|
||||
mru
|
||||
@@ -1075,6 +1061,9 @@ MTND
|
||||
MULTIPLEUSE
|
||||
multizone
|
||||
muxc
|
||||
muxx
|
||||
muxxc
|
||||
muxxh
|
||||
MVPs
|
||||
mvvm
|
||||
MVVMTK
|
||||
@@ -1157,7 +1146,6 @@ nonstd
|
||||
NOOWNERZORDER
|
||||
NOPARENTNOTIFY
|
||||
NOPREFIX
|
||||
NPU
|
||||
NOREDIRECTIONBITMAP
|
||||
NOREDRAW
|
||||
NOREMOVE
|
||||
@@ -1186,6 +1174,7 @@ nowarn
|
||||
NOZORDER
|
||||
NPH
|
||||
npmjs
|
||||
NPU
|
||||
NResize
|
||||
NTAPI
|
||||
ntdll
|
||||
@@ -1210,16 +1199,15 @@ oldpath
|
||||
oldtheme
|
||||
oleaut
|
||||
OLECHAR
|
||||
ollama
|
||||
onebranch
|
||||
onnx
|
||||
OOBEUI
|
||||
openas
|
||||
opencode
|
||||
OPENFILENAME
|
||||
opensource
|
||||
openxmlformats
|
||||
ollama
|
||||
Olllama
|
||||
onnx
|
||||
OPTIMIZEFORINVOKE
|
||||
ORPHANEDDIALOGTITLE
|
||||
ORSCANS
|
||||
@@ -1292,6 +1280,7 @@ pguid
|
||||
phbm
|
||||
phbmp
|
||||
phicon
|
||||
Photoshop
|
||||
phwnd
|
||||
pici
|
||||
pidl
|
||||
@@ -1314,7 +1303,6 @@ pnid
|
||||
PNMLINK
|
||||
Poc
|
||||
Podcasts
|
||||
Photoshop
|
||||
POINTERID
|
||||
POINTERUPDATE
|
||||
Pokedex
|
||||
@@ -1409,10 +1397,9 @@ pwsz
|
||||
pwtd
|
||||
QDC
|
||||
qit
|
||||
QNN
|
||||
Qualcomm
|
||||
QITAB
|
||||
QITABENT
|
||||
QNN
|
||||
qoi
|
||||
Quarternary
|
||||
QUERYENDSESSION
|
||||
@@ -1422,8 +1409,8 @@ quickaccent
|
||||
QUNS
|
||||
RAII
|
||||
RAlt
|
||||
RAquadrant
|
||||
randi
|
||||
RAquadrant
|
||||
rasterization
|
||||
Rasterize
|
||||
RAWINPUTDEVICE
|
||||
@@ -1450,9 +1437,7 @@ regfile
|
||||
REGISTERCLASSFAILED
|
||||
REGISTRYHEADER
|
||||
REGISTRYPREVIEWEXT
|
||||
registryroot
|
||||
regkey
|
||||
regroot
|
||||
regsvr
|
||||
REINSTALLMODE
|
||||
releaseblog
|
||||
@@ -1505,7 +1490,6 @@ rstringalpha
|
||||
rstringdigit
|
||||
rtb
|
||||
RTLREADING
|
||||
rtm
|
||||
runas
|
||||
rundll
|
||||
rungameid
|
||||
@@ -1562,8 +1546,8 @@ SETRULES
|
||||
SETSCREENSAVEACTIVE
|
||||
SETSTICKYKEYS
|
||||
SETTEXT
|
||||
settingscard
|
||||
SETTINGCHANGE
|
||||
settingscard
|
||||
SETTINGSCHANGED
|
||||
settingsheader
|
||||
settingshotkeycontrol
|
||||
@@ -1708,6 +1692,7 @@ stringtable
|
||||
stringval
|
||||
Strm
|
||||
strret
|
||||
STRSAFE
|
||||
stscanf
|
||||
sttngs
|
||||
Stubless
|
||||
@@ -1719,7 +1704,6 @@ sublang
|
||||
SUBMODULEUPDATE
|
||||
subresource
|
||||
Superbar
|
||||
suntimes
|
||||
sut
|
||||
svchost
|
||||
SVGIn
|
||||
@@ -1753,7 +1737,6 @@ SYSTEMMODAL
|
||||
SYSTEMTIME
|
||||
TARG
|
||||
TARGETAPPHEADER
|
||||
TARGETDIR
|
||||
targetentrypoint
|
||||
TARGETHEADER
|
||||
targetver
|
||||
@@ -1783,10 +1766,10 @@ textextractor
|
||||
TEXTINCLUDE
|
||||
tfopen
|
||||
tgz
|
||||
THEMECHANGED
|
||||
themeresources
|
||||
THH
|
||||
THICKFRAME
|
||||
THEMECHANGED
|
||||
THISCOMPONENT
|
||||
throughs
|
||||
TILEDWINDOW
|
||||
@@ -1883,7 +1866,6 @@ USEINSTALLERFORTEST
|
||||
USESHOWWINDOW
|
||||
USESTDHANDLES
|
||||
USRDLL
|
||||
utm
|
||||
UType
|
||||
uuidv
|
||||
uwp
|
||||
@@ -1956,11 +1938,11 @@ Wca
|
||||
WCE
|
||||
wcex
|
||||
WClass
|
||||
WCRAPI
|
||||
wcsicmp
|
||||
wcsncpy
|
||||
wcsnicmp
|
||||
WCT
|
||||
WCRAPI
|
||||
WDA
|
||||
wdm
|
||||
wdp
|
||||
@@ -1988,6 +1970,7 @@ WINDOWPLACEMENT
|
||||
WINDOWPOSCHANGED
|
||||
WINDOWPOSCHANGING
|
||||
WINDOWSBUILDNUMBER
|
||||
windowsml
|
||||
windowssearch
|
||||
windowssettings
|
||||
WINDOWSTYLES
|
||||
@@ -2003,9 +1986,8 @@ Winhook
|
||||
WINL
|
||||
winlogon
|
||||
winmd
|
||||
WINNT
|
||||
windowsml
|
||||
winml
|
||||
WINNT
|
||||
winres
|
||||
winrt
|
||||
winsdk
|
||||
@@ -2067,20 +2049,21 @@ WTSAT
|
||||
Wubi
|
||||
WUX
|
||||
Wwanpp
|
||||
xap
|
||||
XAxis
|
||||
XButton
|
||||
xclip
|
||||
xcopy
|
||||
xap
|
||||
XDeployment
|
||||
XDimension
|
||||
xdf
|
||||
XDimension
|
||||
XDocument
|
||||
XElement
|
||||
xfd
|
||||
XFile
|
||||
XIncrement
|
||||
XLoc
|
||||
xmp
|
||||
XNamespace
|
||||
Xoshiro
|
||||
XPels
|
||||
@@ -2091,23 +2074,22 @@ xsi
|
||||
XSpeed
|
||||
XStr
|
||||
xstyler
|
||||
xmp
|
||||
XTimer
|
||||
XUP
|
||||
XVIRTUALSCREEN
|
||||
xxxxxx
|
||||
YAxis
|
||||
ycombinator
|
||||
YIncrement
|
||||
YDimension
|
||||
YIncrement
|
||||
yinle
|
||||
yinyue
|
||||
YPels
|
||||
YPos
|
||||
YResolution
|
||||
YSpeed
|
||||
YTimer
|
||||
YStr
|
||||
YTimer
|
||||
YVIRTUALSCREEN
|
||||
ZEROINIT
|
||||
zonability
|
||||
|
||||
16
README.md
16
README.md
@@ -53,17 +53,17 @@ Go to the [PowerToys GitHub releases][github-release-link], click Assets to reve
|
||||
<!-- 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.97%22
|
||||
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.96%22
|
||||
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.96.0/PowerToysUserSetup-0.96.0-x64.exe
|
||||
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.96.0/PowerToysUserSetup-0.96.0-arm64.exe
|
||||
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.96.0/PowerToysSetup-0.96.0-x64.exe
|
||||
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.96.0/PowerToysSetup-0.96.0-arm64.exe
|
||||
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.96.1/PowerToysUserSetup-0.96.1-x64.exe
|
||||
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.96.1/PowerToysUserSetup-0.96.1-arm64.exe
|
||||
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.96.1/PowerToysSetup-0.96.1-x64.exe
|
||||
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.96.1/PowerToysSetup-0.96.1-arm64.exe
|
||||
|
||||
| Description | Filename |
|
||||
|----------------|----------|
|
||||
| Per user - x64 | [PowerToysUserSetup-0.96.0-x64.exe][ptUserX64] |
|
||||
| Per user - ARM64 | [PowerToysUserSetup-0.96.0-arm64.exe][ptUserArm64] |
|
||||
| Machine wide - x64 | [PowerToysSetup-0.96.0-x64.exe][ptMachineX64] |
|
||||
| Machine wide - ARM64 | [PowerToysSetup-0.96.0-arm64.exe][ptMachineArm64] |
|
||||
| Per user - x64 | [PowerToysUserSetup-0.96.1-x64.exe][ptUserX64] |
|
||||
| Per user - ARM64 | [PowerToysUserSetup-0.96.1-arm64.exe][ptUserArm64] |
|
||||
| Machine wide - x64 | [PowerToysSetup-0.96.1-x64.exe][ptMachineX64] |
|
||||
| Machine wide - ARM64 | [PowerToysSetup-0.96.1-arm64.exe][ptMachineArm64] |
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
@@ -33,9 +33,12 @@ The **Light Switch** module lets users automatically transition between light an
|
||||
|
||||
> **Note:** Using the shortcut overrides the current schedule until the next transition event.
|
||||
|
||||
* **LightSwitchService**
|
||||
Reads settings and applies theming. Runs a check every minute to ensure the state is correct.
|
||||
|
||||
* **LightSwitchService.cpp**
|
||||
is the heart beat of the module. Controls ticking every minute and depending on user actions (manual override, settings changing, etc) triggers the state manager to perform the corresponding operation.
|
||||
|
||||
* **LightSwitchStateManager.cpp**
|
||||
handles updating the state based on the signals sent by LightSwitchService.
|
||||
|
||||
* **SettingsXAML/LightSwitch**
|
||||
Provides the settings UI for configuring schedules, syncing location, and customizing shortcuts.
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ public sealed class FoundryLocalModelProvider : ILanguageModelProvider
|
||||
|
||||
return new OpenAIClient(
|
||||
new ApiKeyCredential("none"),
|
||||
new OpenAIClientOptions { Endpoint = endpointUri })
|
||||
new OpenAIClientOptions { Endpoint = endpointUri, NetworkTimeout = TimeSpan.FromMinutes(5) })
|
||||
.GetChatClient(modelId)
|
||||
.AsIChatClient();
|
||||
}
|
||||
|
||||
@@ -215,7 +215,6 @@ public sealed class AdvancedAIKernelService : KernelServiceBase
|
||||
return new OpenAIPromptExecutionSettings
|
||||
{
|
||||
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(),
|
||||
Temperature = 0.01,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,6 +146,7 @@ public sealed class FoundryLocalPasteProvider : IPasteAIProvider
|
||||
var options = new ChatOptions
|
||||
{
|
||||
ModelId = modelReference,
|
||||
MaxOutputTokens = 2048,
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(systemPrompt))
|
||||
|
||||
@@ -157,8 +157,6 @@ namespace AdvancedPaste.Services.CustomActions
|
||||
{
|
||||
AIServiceType.OpenAI or AIServiceType.AzureOpenAI => new OpenAIPromptExecutionSettings
|
||||
{
|
||||
Temperature = 0.01,
|
||||
MaxTokens = 2000,
|
||||
FunctionChoiceBehavior = null,
|
||||
},
|
||||
_ => new PromptExecutionSettings(),
|
||||
|
||||
@@ -350,7 +350,7 @@ namespace Awake.Core
|
||||
TrayHelper.TimedIcon,
|
||||
TrayIconAction.Update);
|
||||
},
|
||||
_ => HandleTimerCompletion("timed"),
|
||||
() => HandleTimerCompletion("timed"),
|
||||
_tokenSource.Token);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,9 @@ using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.Foundation;
|
||||
@@ -15,6 +17,8 @@ namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class SettingsModel : ObservableObject
|
||||
{
|
||||
private const string DeprecatedHotkeyGoesHomeKey = "HotkeyGoesHome";
|
||||
|
||||
[JsonIgnore]
|
||||
public static readonly string FilePath;
|
||||
|
||||
@@ -30,8 +34,6 @@ public partial class SettingsModel : ObservableObject
|
||||
|
||||
public bool ShowAppDetails { get; set; }
|
||||
|
||||
public bool HotkeyGoesHome { get; set; }
|
||||
|
||||
public bool BackspaceGoesBack { get; set; }
|
||||
|
||||
public bool SingleClickActivates { get; set; }
|
||||
@@ -56,6 +58,8 @@ public partial class SettingsModel : ObservableObject
|
||||
|
||||
public WindowPosition? LastWindowPosition { get; set; }
|
||||
|
||||
public TimeSpan AutoGoHomeInterval { get; set; } = Timeout.InfiniteTimeSpan;
|
||||
|
||||
// END SETTINGS
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -98,12 +102,29 @@ public partial class SettingsModel : ObservableObject
|
||||
{
|
||||
// Read the JSON content from the file
|
||||
var jsonContent = File.ReadAllText(FilePath);
|
||||
var loaded = JsonSerializer.Deserialize<SettingsModel>(jsonContent, JsonSerializationContext.Default.SettingsModel) ?? new();
|
||||
|
||||
var loaded = JsonSerializer.Deserialize<SettingsModel>(jsonContent, JsonSerializationContext.Default.SettingsModel);
|
||||
var migratedAny = false;
|
||||
try
|
||||
{
|
||||
if (JsonNode.Parse(jsonContent) is JsonObject root)
|
||||
{
|
||||
migratedAny |= ApplyMigrations(root, loaded);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Migration check failed: {ex}");
|
||||
}
|
||||
|
||||
Debug.WriteLine(loaded is not null ? "Loaded settings file" : "Failed to parse");
|
||||
Debug.WriteLine("Loaded settings file");
|
||||
|
||||
return loaded ?? new();
|
||||
if (migratedAny)
|
||||
{
|
||||
SaveSettings(loaded);
|
||||
}
|
||||
|
||||
return loaded;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -113,6 +134,51 @@ public partial class SettingsModel : ObservableObject
|
||||
return new();
|
||||
}
|
||||
|
||||
private static bool ApplyMigrations(JsonObject root, SettingsModel model)
|
||||
{
|
||||
var migrated = false;
|
||||
|
||||
// Migration #1: HotkeyGoesHome (bool) -> AutoGoHomeInterval (TimeSpan)
|
||||
// The old 'HotkeyGoesHome' boolean indicated whether the "go home" action should happen immediately (true) or never (false).
|
||||
// The new 'AutoGoHomeInterval' uses a TimeSpan: 'TimeSpan.Zero' means immediate, 'Timeout.InfiniteTimeSpan' means never.
|
||||
migrated |= TryMigrate(
|
||||
"Migration #1: HotkeyGoesHome (bool) -> AutoGoHomeInterval (TimeSpan)",
|
||||
root,
|
||||
model,
|
||||
nameof(AutoGoHomeInterval),
|
||||
DeprecatedHotkeyGoesHomeKey,
|
||||
(settingsModel, goesHome) => settingsModel.AutoGoHomeInterval = goesHome ? TimeSpan.Zero : Timeout.InfiniteTimeSpan,
|
||||
JsonSerializationContext.Default.Boolean);
|
||||
|
||||
return migrated;
|
||||
}
|
||||
|
||||
private static bool TryMigrate<T>(string migrationName, JsonObject root, SettingsModel model, string newKey, string oldKey, Action<SettingsModel, T> apply, JsonTypeInfo<T> jsonTypeInfo)
|
||||
{
|
||||
try
|
||||
{
|
||||
// If new key already present, skip migration
|
||||
if (root.ContainsKey(newKey) && root[newKey] is not null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// If old key present, try to deserialize and apply
|
||||
if (root.TryGetPropertyValue(oldKey, out var oldNode) && oldNode is not null)
|
||||
{
|
||||
var value = oldNode.Deserialize<T>(jsonTypeInfo);
|
||||
apply(model, value!);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Error during migration {migrationName}.", ex);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void SaveSettings(SettingsModel model)
|
||||
{
|
||||
if (string.IsNullOrEmpty(FilePath))
|
||||
@@ -139,6 +205,9 @@ public partial class SettingsModel : ObservableObject
|
||||
savedSettings[item.Key] = item.Value?.DeepClone();
|
||||
}
|
||||
|
||||
// Remove deprecated keys
|
||||
savedSettings.Remove(DeprecatedHotkeyGoesHomeKey);
|
||||
|
||||
var serialized = savedSettings.ToJsonString(JsonSerializationContext.Default.Options);
|
||||
File.WriteAllText(FilePath, serialized);
|
||||
|
||||
|
||||
@@ -11,6 +11,19 @@ namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class SettingsViewModel : INotifyPropertyChanged
|
||||
{
|
||||
private static readonly List<TimeSpan> AutoGoHomeIntervals =
|
||||
[
|
||||
Timeout.InfiniteTimeSpan,
|
||||
TimeSpan.Zero,
|
||||
TimeSpan.FromSeconds(10),
|
||||
TimeSpan.FromSeconds(20),
|
||||
TimeSpan.FromSeconds(30),
|
||||
TimeSpan.FromSeconds(60),
|
||||
TimeSpan.FromSeconds(90),
|
||||
TimeSpan.FromSeconds(120),
|
||||
TimeSpan.FromSeconds(180),
|
||||
];
|
||||
|
||||
private readonly SettingsModel _settings;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
@@ -58,16 +71,6 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
||||
}
|
||||
}
|
||||
|
||||
public bool HotkeyGoesHome
|
||||
{
|
||||
get => _settings.HotkeyGoesHome;
|
||||
set
|
||||
{
|
||||
_settings.HotkeyGoesHome = value;
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public bool BackspaceGoesBack
|
||||
{
|
||||
get => _settings.BackspaceGoesBack;
|
||||
@@ -138,6 +141,25 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
||||
}
|
||||
}
|
||||
|
||||
public int AutoGoBackIntervalIndex
|
||||
{
|
||||
get
|
||||
{
|
||||
var index = AutoGoHomeIntervals.IndexOf(_settings.AutoGoHomeInterval);
|
||||
return index >= 0 ? index : 0;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
if (value >= 0 && value < AutoGoHomeIntervals.Count)
|
||||
{
|
||||
_settings.AutoGoHomeInterval = AutoGoHomeIntervals[value];
|
||||
}
|
||||
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public ObservableCollection<ProviderSettingsViewModel> CommandProviders { get; } = [];
|
||||
|
||||
public SettingsExtensionsViewModel Extensions { get; }
|
||||
|
||||
@@ -2,21 +2,52 @@
|
||||
// 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 System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.Graphics;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public sealed class WindowPosition
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets left position in device pixels.
|
||||
/// </summary>
|
||||
public int X { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets top position in device pixels.
|
||||
/// </summary>
|
||||
public int Y { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets width in device pixels.
|
||||
/// </summary>
|
||||
public int Width { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets height in device pixels.
|
||||
/// </summary>
|
||||
public int Height { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets width of the screen in device pixels where the window is located.
|
||||
/// </summary>
|
||||
public int ScreenWidth { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets height of the screen in device pixels where the window is located.
|
||||
/// </summary>
|
||||
public int ScreenHeight { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets DPI (dots per inch) of the display where the window is located.
|
||||
/// </summary>
|
||||
public int Dpi { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Converts the window position properties to a <see cref="RectInt32"/> structure representing the physical window rectangle.
|
||||
/// </summary>
|
||||
public RectInt32 ToPhysicalWindowRectangle()
|
||||
{
|
||||
return new RectInt32(X, Y, Width, Height);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Composition;
|
||||
using Microsoft.UI.Composition.SystemBackdrops;
|
||||
using Microsoft.UI.Input;
|
||||
@@ -33,6 +34,8 @@ using Windows.UI.WindowManagement;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.Graphics.Dwm;
|
||||
using Windows.Win32.Graphics.Gdi;
|
||||
using Windows.Win32.UI.HiDpi;
|
||||
using Windows.Win32.UI.Input.KeyboardAndMouse;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
using WinRT;
|
||||
@@ -48,10 +51,14 @@ public sealed partial class MainWindow : WindowEx,
|
||||
IRecipient<QuitMessage>,
|
||||
IDisposable
|
||||
{
|
||||
private const int DefaultWidth = 800;
|
||||
private const int DefaultHeight = 480;
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "Stylistically, window messages are WM_")]
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1306:Field names should begin with lower-case letter", Justification = "Stylistically, window messages are WM_")]
|
||||
private readonly uint WM_TASKBAR_RESTART;
|
||||
private readonly HWND _hwnd;
|
||||
private readonly DispatcherTimer _autoGoHomeTimer;
|
||||
private readonly WNDPROC? _hotkeyWndProc;
|
||||
private readonly WNDPROC? _originalWndProc;
|
||||
private readonly List<TopLevelHotkey> _hotkeys = [];
|
||||
@@ -62,6 +69,7 @@ public sealed partial class MainWindow : WindowEx,
|
||||
|
||||
private DesktopAcrylicController? _acrylicController;
|
||||
private SystemBackdropConfiguration? _configurationSource;
|
||||
private TimeSpan _autoGoHomeInterval = Timeout.InfiniteTimeSpan;
|
||||
|
||||
private WindowPosition _currentWindowPosition = new();
|
||||
|
||||
@@ -69,6 +77,9 @@ public sealed partial class MainWindow : WindowEx,
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_autoGoHomeTimer = new DispatcherTimer();
|
||||
_autoGoHomeTimer.Tick += OnAutoGoHomeTimerOnTick;
|
||||
|
||||
_hwnd = new HWND(WinRT.Interop.WindowNative.GetWindowHandle(this).ToInt32());
|
||||
|
||||
unsafe
|
||||
@@ -135,6 +146,15 @@ public sealed partial class MainWindow : WindowEx,
|
||||
HideWindow();
|
||||
}
|
||||
|
||||
private void OnAutoGoHomeTimerOnTick(object? s, object e)
|
||||
{
|
||||
_autoGoHomeTimer.Stop();
|
||||
|
||||
// BEAR LOADING: Focus Search must be suppressed here; otherwise it may steal focus (for example, from the system tray icon)
|
||||
// and prevent the user from opening its context menu.
|
||||
WeakReferenceMessenger.Default.Send(new GoHomeMessage(WithAnimation: false, FocusSearch: false));
|
||||
}
|
||||
|
||||
private static void LocalKeyboardListener_OnKeyPressed(object? sender, LocalKeyboardListenerKeyPressedEventArgs e)
|
||||
{
|
||||
if (e.Key == VirtualKey.GoBack)
|
||||
@@ -173,22 +193,8 @@ public sealed partial class MainWindow : WindowEx,
|
||||
return;
|
||||
}
|
||||
|
||||
AppWindow.Resize(new SizeInt32 { Width = savedPosition.Width, Height = savedPosition.Height });
|
||||
|
||||
var savedRect = new RectInt32(savedPosition.X, savedPosition.Y, savedPosition.Width, savedPosition.Height);
|
||||
var displayArea = DisplayArea.GetFromRect(savedRect, DisplayAreaFallback.Nearest);
|
||||
var workArea = displayArea.WorkArea;
|
||||
|
||||
var maxX = workArea.X + Math.Max(0, workArea.Width - savedPosition.Width);
|
||||
var maxY = workArea.Y + Math.Max(0, workArea.Height - savedPosition.Height);
|
||||
|
||||
var targetPoint = new PointInt32
|
||||
{
|
||||
X = Math.Clamp(savedPosition.X, workArea.X, maxX),
|
||||
Y = Math.Clamp(savedPosition.Y, workArea.Y, maxY),
|
||||
};
|
||||
|
||||
AppWindow.Move(targetPoint);
|
||||
var newRect = EnsureWindowIsVisible(savedPosition.ToPhysicalWindowRectangle(), new SizeInt32(savedPosition.ScreenWidth, savedPosition.ScreenHeight), savedPosition.Dpi);
|
||||
AppWindow.MoveAndResize(newRect);
|
||||
}
|
||||
|
||||
private void PositionCentered(DisplayArea displayArea)
|
||||
@@ -207,12 +213,16 @@ public sealed partial class MainWindow : WindowEx,
|
||||
|
||||
private void UpdateWindowPositionInMemory()
|
||||
{
|
||||
var displayArea = DisplayArea.GetFromWindowId(AppWindow.Id, DisplayAreaFallback.Nearest) ?? DisplayArea.Primary;
|
||||
_currentWindowPosition = new WindowPosition
|
||||
{
|
||||
X = AppWindow.Position.X,
|
||||
Y = AppWindow.Position.Y,
|
||||
Width = AppWindow.Size.Width,
|
||||
Height = AppWindow.Size.Height,
|
||||
Dpi = (int)this.GetDpiForWindow(),
|
||||
ScreenWidth = displayArea.WorkArea.Width,
|
||||
ScreenHeight = displayArea.WorkArea.Height,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -224,6 +234,9 @@ public sealed partial class MainWindow : WindowEx,
|
||||
App.Current.Services.GetService<TrayIconService>()!.SetupTrayIcon(settings.ShowSystemTrayIcon);
|
||||
|
||||
_ignoreHotKeyWhenFullScreen = settings.IgnoreShortcutWhenFullscreen;
|
||||
|
||||
_autoGoHomeInterval = settings.AutoGoHomeInterval;
|
||||
_autoGoHomeTimer.Interval = _autoGoHomeInterval;
|
||||
}
|
||||
|
||||
// We want to use DesktopAcrylicKind.Thin and custom colors as this is the default material
|
||||
@@ -283,6 +296,8 @@ public sealed partial class MainWindow : WindowEx,
|
||||
|
||||
private void ShowHwnd(IntPtr hwndValue, MonitorBehavior target)
|
||||
{
|
||||
StopAutoGoHome();
|
||||
|
||||
var hwnd = new HWND(hwndValue != 0 ? hwndValue : _hwnd);
|
||||
|
||||
// Remember, IsIconic == "minimized", which is entirely different state
|
||||
@@ -300,8 +315,8 @@ public sealed partial class MainWindow : WindowEx,
|
||||
|
||||
if (target == MonitorBehavior.ToLast)
|
||||
{
|
||||
AppWindow.Resize(new SizeInt32 { Width = _currentWindowPosition.Width, Height = _currentWindowPosition.Height });
|
||||
AppWindow.Move(new PointInt32 { X = _currentWindowPosition.X, Y = _currentWindowPosition.Y });
|
||||
var newRect = EnsureWindowIsVisible(_currentWindowPosition.ToPhysicalWindowRectangle(), new SizeInt32(_currentWindowPosition.ScreenWidth, _currentWindowPosition.ScreenHeight), _currentWindowPosition.Dpi);
|
||||
AppWindow.MoveAndResize(newRect);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -330,6 +345,114 @@ public sealed partial class MainWindow : WindowEx,
|
||||
PInvoke.SetWindowPos(hwnd, HWND.HWND_TOPMOST, 0, 0, 0, 0, SET_WINDOW_POS_FLAGS.SWP_NOMOVE | SET_WINDOW_POS_FLAGS.SWP_NOSIZE);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that the window rectangle is visible on-screen.
|
||||
/// </summary>
|
||||
/// <param name="windowRect">The window rectangle in physical pixels.</param>
|
||||
/// <param name="originalScreen">The desktop area the window was positioned on.</param>
|
||||
/// <param name="originalDpi">The window's original DPI.</param>
|
||||
/// <returns>
|
||||
/// A window rectangle in physical pixels, moved to the nearest display and resized
|
||||
/// if the DPI has changed.
|
||||
/// </returns>
|
||||
private static RectInt32 EnsureWindowIsVisible(RectInt32 windowRect, SizeInt32 originalScreen, int originalDpi)
|
||||
{
|
||||
var displayArea = DisplayArea.GetFromRect(windowRect, DisplayAreaFallback.Nearest);
|
||||
if (displayArea is null)
|
||||
{
|
||||
return windowRect;
|
||||
}
|
||||
|
||||
var workArea = displayArea.WorkArea;
|
||||
if (workArea.Width <= 0 || workArea.Height <= 0)
|
||||
{
|
||||
// Fallback, nothing reasonable to do
|
||||
return windowRect;
|
||||
}
|
||||
|
||||
var effectiveDpi = GetEffectiveDpiFromDisplayId(displayArea);
|
||||
if (originalDpi <= 0)
|
||||
{
|
||||
originalDpi = effectiveDpi; // use current DPI as baseline (no scaling adjustment needed)
|
||||
}
|
||||
|
||||
var hasInvalidSize = windowRect.Width <= 0 || windowRect.Height <= 0;
|
||||
if (hasInvalidSize)
|
||||
{
|
||||
windowRect = new RectInt32(windowRect.X, windowRect.Y, DefaultWidth, DefaultHeight);
|
||||
}
|
||||
|
||||
// If we have a DPI change, scale the window rectangle accordingly
|
||||
if (effectiveDpi != originalDpi)
|
||||
{
|
||||
var scalingFactor = effectiveDpi / (double)originalDpi;
|
||||
windowRect = new RectInt32(
|
||||
(int)Math.Round(windowRect.X * scalingFactor),
|
||||
(int)Math.Round(windowRect.Y * scalingFactor),
|
||||
(int)Math.Round(windowRect.Width * scalingFactor),
|
||||
(int)Math.Round(windowRect.Height * scalingFactor));
|
||||
}
|
||||
|
||||
var targetWidth = Math.Min(windowRect.Width, workArea.Width);
|
||||
var targetHeight = Math.Min(windowRect.Height, workArea.Height);
|
||||
|
||||
// Ensure at least some minimum visible area (e.g., 100 pixels)
|
||||
// This helps prevent the window from being entirely offscreen, regardless of display scaling.
|
||||
const int minimumVisibleSize = 100;
|
||||
var isOffscreen =
|
||||
windowRect.X + minimumVisibleSize > workArea.X + workArea.Width ||
|
||||
windowRect.X + windowRect.Width - minimumVisibleSize < workArea.X ||
|
||||
windowRect.Y + minimumVisibleSize > workArea.Y + workArea.Height ||
|
||||
windowRect.Y + windowRect.Height - minimumVisibleSize < workArea.Y;
|
||||
|
||||
// if the work area size has changed, re-center the window
|
||||
var workAreaSizeChanged =
|
||||
originalScreen.Width != workArea.Width ||
|
||||
originalScreen.Height != workArea.Height;
|
||||
|
||||
int targetX;
|
||||
int targetY;
|
||||
var recenter = isOffscreen || workAreaSizeChanged || hasInvalidSize;
|
||||
if (recenter)
|
||||
{
|
||||
targetX = workArea.X + ((workArea.Width - targetWidth) / 2);
|
||||
targetY = workArea.Y + ((workArea.Height - targetHeight) / 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
targetX = windowRect.X;
|
||||
targetY = windowRect.Y;
|
||||
}
|
||||
|
||||
return new RectInt32(targetX, targetY, targetWidth, targetHeight);
|
||||
}
|
||||
|
||||
private static int GetEffectiveDpiFromDisplayId(DisplayArea displayArea)
|
||||
{
|
||||
var effectiveDpi = 96;
|
||||
|
||||
var hMonitor = (HMONITOR)Win32Interop.GetMonitorFromDisplayId(displayArea.DisplayId);
|
||||
if (!hMonitor.IsNull)
|
||||
{
|
||||
var hr = PInvoke.GetDpiForMonitor(hMonitor, MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI, out var dpiX, out _);
|
||||
if (hr == 0)
|
||||
{
|
||||
effectiveDpi = (int)dpiX;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning($"GetDpiForMonitor failed with HRESULT: 0x{hr.Value:X8} on display {displayArea.DisplayId}");
|
||||
}
|
||||
}
|
||||
|
||||
if (effectiveDpi <= 0)
|
||||
{
|
||||
effectiveDpi = 96;
|
||||
}
|
||||
|
||||
return effectiveDpi;
|
||||
}
|
||||
|
||||
private DisplayArea GetScreen(HWND currentHwnd, MonitorBehavior target)
|
||||
{
|
||||
// Leaving a note here, in case we ever need it:
|
||||
@@ -429,6 +552,25 @@ public sealed partial class MainWindow : WindowEx,
|
||||
// If the window was not cloaked, then leave it hidden.
|
||||
// Sure, it's not ideal, but at least it's not visible.
|
||||
}
|
||||
|
||||
// Start auto-go-home timer
|
||||
RestartAutoGoHome();
|
||||
}
|
||||
|
||||
private void StopAutoGoHome()
|
||||
{
|
||||
_autoGoHomeTimer.Stop();
|
||||
}
|
||||
|
||||
private void RestartAutoGoHome()
|
||||
{
|
||||
if (_autoGoHomeInterval == Timeout.InfiniteTimeSpan)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_autoGoHomeTimer.Stop();
|
||||
_autoGoHomeTimer.Start();
|
||||
}
|
||||
|
||||
private bool Cloak()
|
||||
@@ -479,6 +621,9 @@ public sealed partial class MainWindow : WindowEx,
|
||||
Y = _currentWindowPosition.Y,
|
||||
Width = _currentWindowPosition.Width,
|
||||
Height = _currentWindowPosition.Height,
|
||||
Dpi = _currentWindowPosition.Dpi,
|
||||
ScreenWidth = _currentWindowPosition.ScreenWidth,
|
||||
ScreenHeight = _currentWindowPosition.ScreenHeight,
|
||||
};
|
||||
|
||||
SettingsModel.SaveSettings(settings);
|
||||
|
||||
@@ -345,7 +345,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
// Depending on the settings, either
|
||||
// * Go home, or
|
||||
// * Select the search text (if we should remain open on this page)
|
||||
if (settings.HotkeyGoesHome)
|
||||
if (settings.AutoGoHomeInterval == TimeSpan.Zero)
|
||||
{
|
||||
GoHome(false);
|
||||
}
|
||||
|
||||
@@ -51,8 +51,18 @@
|
||||
</controls:SettingsCard>
|
||||
</controls:SettingsExpander.Items>
|
||||
</controls:SettingsExpander>
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_GoHome_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ToggleSwitch IsOn="{x:Bind viewModel.HotkeyGoesHome, Mode=TwoWay}" />
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_AutoGoHome_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind viewModel.AutoGoBackIntervalIndex, Mode=TwoWay}">
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_AutoGoHome_Item_Never" />
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_AutoGoHome_Item_Immediately" />
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_AutoGoHome_Item_After10Seconds" />
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_AutoGoHome_Item_After20Seconds" />
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_AutoGoHome_Item_After30Seconds" />
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_AutoGoHome_Item_After60Seconds" />
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_AutoGoHome_Item_After90Seconds" />
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_AutoGoHome_Item_After120Seconds" />
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_AutoGoHome_Item_After180Seconds" />
|
||||
</ComboBox>
|
||||
</controls:SettingsCard>
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_HighlightSearch_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ToggleSwitch IsOn="{x:Bind viewModel.HighlightSearchOnActivate, Mode=TwoWay}" />
|
||||
|
||||
@@ -344,12 +344,6 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<data name="Settings_GeneralPage_IgnoreShortcutWhenFullscreen_SettingsCard.Description" xml:space="preserve">
|
||||
<value>Preventing disruption of the program running in fullscreen by unintentional activation of shortcut</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_GoHome_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Go home when activated</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_GoHome_SettingsCard.Description" xml:space="preserve">
|
||||
<value>Automatically opens the home page upon activation</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_HighlightSearch_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Highlight search on activate</value>
|
||||
</data>
|
||||
@@ -523,4 +517,37 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<data name="GlobalErrorHandler_CrashMessageBox_Caption" xml:space="preserve">
|
||||
<value>Command Palette - Fatal error</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_AutoGoHome_Item_Never.Content" xml:space="preserve">
|
||||
<value>Never</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_AutoGoHome_Item_Immediately.Content" xml:space="preserve">
|
||||
<value>Immediately</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_AutoGoHome_Item_After10Seconds.Content" xml:space="preserve">
|
||||
<value>10 seconds</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_AutoGoHome_Item_After20Seconds.Content" xml:space="preserve">
|
||||
<value>20 seconds</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_AutoGoHome_Item_After30Seconds.Content" xml:space="preserve">
|
||||
<value>30 seconds</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_AutoGoHome_Item_After60Seconds.Content" xml:space="preserve">
|
||||
<value>60 seconds</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_AutoGoHome_Item_After90Seconds.Content" xml:space="preserve">
|
||||
<value>90 seconds</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_AutoGoHome_Item_After120Seconds.Content" xml:space="preserve">
|
||||
<value>2 minutes</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_AutoGoHome_Item_After180Seconds.Content" xml:space="preserve">
|
||||
<value>3 minutes</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_AutoGoHome_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Automatically return home</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_AutoGoHome_SettingsCard.Description" xml:space="preserve">
|
||||
<value>Automatically returns to home page after a period of inactivity when Command Palette is closed</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -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.
|
||||
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.WebSearch.UnitTests;
|
||||
|
||||
public class MockBrowserInfoService : IBrowserInfoService
|
||||
{
|
||||
public BrowserInfo GetDefaultBrowser() => new() { Name = "mocked browser", Path = "C:\\mockery\\mock.exe" };
|
||||
}
|
||||
@@ -26,8 +26,9 @@ public class QueryTests : CommandPaletteUnitTestBase
|
||||
{
|
||||
// Setup
|
||||
var settings = new MockSettingsInterface();
|
||||
var browserInfoService = new MockBrowserInfoService();
|
||||
|
||||
var page = new WebSearchListPage(settings);
|
||||
var page = new WebSearchListPage(settings, browserInfoService);
|
||||
|
||||
// Act
|
||||
page.UpdateSearchText(string.Empty, query);
|
||||
@@ -55,8 +56,9 @@ public class QueryTests : CommandPaletteUnitTestBase
|
||||
};
|
||||
|
||||
var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, historyItemCount: 5);
|
||||
var browserInfoService = new MockBrowserInfoService();
|
||||
|
||||
var page = new WebSearchListPage(settings);
|
||||
var page = new WebSearchListPage(settings, browserInfoService);
|
||||
|
||||
// Act
|
||||
page.UpdateSearchText("abcdef", string.Empty);
|
||||
@@ -90,8 +92,9 @@ public class QueryTests : CommandPaletteUnitTestBase
|
||||
};
|
||||
|
||||
var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, historyItemCount: 5);
|
||||
var browserInfoService = new MockBrowserInfoService();
|
||||
|
||||
var page = new WebSearchListPage(settings);
|
||||
var page = new WebSearchListPage(settings, browserInfoService);
|
||||
|
||||
mockHistoryItems.Add(new HistoryItem("another search5", DateTime.Parse("2024-01-06 13:00:00", CultureInfo.CurrentCulture)));
|
||||
|
||||
@@ -123,8 +126,9 @@ public class QueryTests : CommandPaletteUnitTestBase
|
||||
};
|
||||
|
||||
var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, historyItemCount: 0);
|
||||
var browserInfoService = new MockBrowserInfoService();
|
||||
|
||||
var page = new WebSearchListPage(settings);
|
||||
var page = new WebSearchListPage(settings, browserInfoService);
|
||||
|
||||
// Act
|
||||
page.UpdateSearchText("abcdef", string.Empty);
|
||||
|
||||
@@ -20,7 +20,9 @@ public class SettingsManagerTests : CommandPaletteUnitTestBase
|
||||
{
|
||||
// Setup
|
||||
var settings = new MockSettingsInterface(historyItemCount: 5);
|
||||
var page = new WebSearchListPage(settings);
|
||||
var browserInfoService = new MockBrowserInfoService();
|
||||
|
||||
var page = new WebSearchListPage(settings, browserInfoService);
|
||||
|
||||
var eventRaised = false;
|
||||
|
||||
|
||||
@@ -2,32 +2,28 @@
|
||||
// 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.CmdPal.Ext.WebSearch.Helpers.Browser;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
using BrowserInfo = Microsoft.CmdPal.Ext.WebSearch.Helpers.DefaultBrowserInfo;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.WebSearch.Commands;
|
||||
|
||||
internal sealed partial class OpenURLCommand : InvokableCommand
|
||||
{
|
||||
private readonly IBrowserInfoService _browserInfoService;
|
||||
|
||||
public string Url { get; internal set; } = string.Empty;
|
||||
|
||||
internal OpenURLCommand(string url)
|
||||
internal OpenURLCommand(string url, IBrowserInfoService browserInfoService)
|
||||
{
|
||||
_browserInfoService = browserInfoService;
|
||||
Url = url;
|
||||
BrowserInfo.UpdateIfTimePassed();
|
||||
Icon = Icons.WebSearch;
|
||||
Name = string.Empty;
|
||||
}
|
||||
|
||||
public override CommandResult Invoke()
|
||||
{
|
||||
if (!ShellHelpers.OpenCommandInShell(BrowserInfo.Path, BrowserInfo.ArgumentsPattern, $"{Url}"))
|
||||
{
|
||||
// TODO GH# 138 --> actually display feedback from the extension somewhere.
|
||||
return CommandResult.KeepOpen();
|
||||
}
|
||||
|
||||
return CommandResult.Dismiss();
|
||||
// TODO GH# 138 --> actually display feedback from the extension somewhere.
|
||||
return _browserInfoService.Open(Url) ? CommandResult.Dismiss() : CommandResult.KeepOpen();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,31 +4,31 @@
|
||||
|
||||
using System;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Helpers;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Properties;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
using BrowserInfo = Microsoft.CmdPal.Ext.WebSearch.Helpers.DefaultBrowserInfo;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.WebSearch.Commands;
|
||||
|
||||
internal sealed partial class SearchWebCommand : InvokableCommand
|
||||
{
|
||||
private readonly ISettingsInterface _settingsManager;
|
||||
private readonly IBrowserInfoService _browserInfoService;
|
||||
|
||||
public string Arguments { get; internal set; } = string.Empty;
|
||||
public string Arguments { get; internal set; }
|
||||
|
||||
internal SearchWebCommand(string arguments, ISettingsInterface settingsManager)
|
||||
internal SearchWebCommand(string arguments, ISettingsInterface settingsManager, IBrowserInfoService browserInfoService)
|
||||
{
|
||||
Arguments = arguments;
|
||||
BrowserInfo.UpdateIfTimePassed();
|
||||
Icon = Icons.WebSearch;
|
||||
Name = Properties.Resources.open_in_default_browser;
|
||||
Name = Resources.open_in_default_browser;
|
||||
_settingsManager = settingsManager;
|
||||
_browserInfoService = browserInfoService;
|
||||
}
|
||||
|
||||
public override CommandResult Invoke()
|
||||
{
|
||||
if (!ShellHelpers.OpenCommandInShell(BrowserInfo.Path, BrowserInfo.ArgumentsPattern, $"? {Arguments}"))
|
||||
if (!_browserInfoService.Open($"? {Arguments}"))
|
||||
{
|
||||
// TODO GH# 138 --> actually display feedback from the extension somewhere.
|
||||
return CommandResult.KeepOpen();
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Helpers;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Properties;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using BrowserInfo = Microsoft.CmdPal.Ext.WebSearch.Helpers.DefaultBrowserInfo;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.WebSearch.Commands;
|
||||
|
||||
@@ -16,25 +16,34 @@ internal sealed partial class FallbackExecuteSearchItem : FallbackCommandItem
|
||||
private readonly SearchWebCommand _executeItem;
|
||||
private static readonly CompositeFormat PluginOpen = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open);
|
||||
private static readonly CompositeFormat SubtitleText = System.Text.CompositeFormat.Parse(Properties.Resources.web_search_fallback_subtitle);
|
||||
private string _title;
|
||||
|
||||
public FallbackExecuteSearchItem(SettingsManager settings)
|
||||
: base(new SearchWebCommand(string.Empty, settings) { Id = "com.microsoft.websearch.fallback" }, Resources.command_item_title)
|
||||
private readonly IBrowserInfoService _browserInfoService;
|
||||
|
||||
public FallbackExecuteSearchItem(ISettingsInterface settings, IBrowserInfoService browserInfoService)
|
||||
: base(new SearchWebCommand(string.Empty, settings, browserInfoService) { Id = "com.microsoft.websearch.fallback" }, Resources.command_item_title)
|
||||
{
|
||||
_executeItem = (SearchWebCommand)this.Command!;
|
||||
_executeItem = (SearchWebCommand)Command!;
|
||||
_browserInfoService = browserInfoService;
|
||||
Title = string.Empty;
|
||||
Subtitle = string.Empty;
|
||||
_executeItem.Name = string.Empty;
|
||||
_title = string.Format(CultureInfo.CurrentCulture, PluginOpen, BrowserInfo.Name ?? BrowserInfo.MSEdgeName);
|
||||
Icon = Icons.WebSearch;
|
||||
}
|
||||
|
||||
private static string UpdateBrowserName(IBrowserInfoService browserInfoService)
|
||||
{
|
||||
var browserName = browserInfoService.GetDefaultBrowser()?.Name;
|
||||
return string.IsNullOrWhiteSpace(browserName)
|
||||
? Resources.open_in_default_browser
|
||||
: string.Format(CultureInfo.CurrentCulture, PluginOpen, browserName);
|
||||
}
|
||||
|
||||
public override void UpdateQuery(string query)
|
||||
{
|
||||
_executeItem.Arguments = query;
|
||||
var isEmpty = string.IsNullOrEmpty(query);
|
||||
_executeItem.Name = isEmpty ? string.Empty : Properties.Resources.open_in_default_browser;
|
||||
Title = isEmpty ? string.Empty : _title;
|
||||
_executeItem.Name = isEmpty ? string.Empty : Resources.open_in_default_browser;
|
||||
Title = isEmpty ? string.Empty : UpdateBrowserName(_browserInfoService);
|
||||
Subtitle = string.Format(CultureInfo.CurrentCulture, SubtitleText, query);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,21 +7,26 @@ using System.Globalization;
|
||||
using System.Text;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Commands;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Helpers;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Properties;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using BrowserInfo = Microsoft.CmdPal.Ext.WebSearch.Helpers.DefaultBrowserInfo;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.WebSearch;
|
||||
|
||||
internal sealed partial class FallbackOpenURLItem : FallbackCommandItem
|
||||
{
|
||||
private readonly IBrowserInfoService _browserInfoService;
|
||||
private readonly OpenURLCommand _executeItem;
|
||||
private static readonly CompositeFormat PluginOpenURL = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open_url);
|
||||
private static readonly CompositeFormat PluginOpenUrlInBrowser = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open_url_in_browser);
|
||||
|
||||
public FallbackOpenURLItem(SettingsManager settings)
|
||||
: base(new OpenURLCommand(string.Empty), Properties.Resources.open_url_fallback_title)
|
||||
public FallbackOpenURLItem(ISettingsInterface settings, IBrowserInfoService browserInfoService)
|
||||
: base(new OpenURLCommand(string.Empty, browserInfoService), Resources.open_url_fallback_title)
|
||||
{
|
||||
_executeItem = (OpenURLCommand)this.Command!;
|
||||
ArgumentNullException.ThrowIfNull(browserInfoService);
|
||||
|
||||
_browserInfoService = browserInfoService;
|
||||
_executeItem = (OpenURLCommand)Command!;
|
||||
Title = string.Empty;
|
||||
_executeItem.Name = string.Empty;
|
||||
Subtitle = string.Empty;
|
||||
@@ -39,7 +44,7 @@ internal sealed partial class FallbackOpenURLItem : FallbackCommandItem
|
||||
return;
|
||||
}
|
||||
|
||||
var success = Uri.TryCreate(query, UriKind.Absolute, out var uri);
|
||||
var success = Uri.TryCreate(query, UriKind.Absolute, out _);
|
||||
|
||||
// if url not contain schema, add http:// by default.
|
||||
if (!success)
|
||||
@@ -48,13 +53,15 @@ internal sealed partial class FallbackOpenURLItem : FallbackCommandItem
|
||||
}
|
||||
|
||||
_executeItem.Url = query;
|
||||
_executeItem.Name = string.IsNullOrEmpty(query) ? string.Empty : Properties.Resources.open_in_default_browser;
|
||||
_executeItem.Name = string.IsNullOrEmpty(query) ? string.Empty : Resources.open_in_default_browser;
|
||||
|
||||
Title = string.Format(CultureInfo.CurrentCulture, PluginOpenURL, query);
|
||||
Subtitle = string.Format(CultureInfo.CurrentCulture, PluginOpenUrlInBrowser, BrowserInfo.Name ?? BrowserInfo.MSEdgeName);
|
||||
|
||||
var browserName = _browserInfoService.GetDefaultBrowser()?.Name;
|
||||
Subtitle = string.IsNullOrWhiteSpace(browserName) ? Resources.open_in_default_browser : string.Format(CultureInfo.CurrentCulture, PluginOpenUrlInBrowser, browserName);
|
||||
}
|
||||
|
||||
public static bool IsValidUrl(string url)
|
||||
private static bool IsValidUrl(string url)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
{
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
|
||||
|
||||
public record BrowserInfo
|
||||
{
|
||||
public required string Path { get; init; }
|
||||
|
||||
public required string Name { get; init; }
|
||||
|
||||
public string? ArgumentsPattern { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="IBrowserInfoService"/>.
|
||||
/// </summary>
|
||||
/// <seealso cref="IBrowserInfoService"/>
|
||||
internal static class BrowserInfoServiceExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Opens the specified URL in the system's default web browser.
|
||||
/// </summary>
|
||||
/// <param name="browserInfoService">The browser information service used to resolve the system's default browser.</param>
|
||||
/// <param name="url">The URL to open.</param>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> if a default browser is found and the URL launch command is issued successfully;
|
||||
/// otherwise, <see langword="false"/>.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// Returns <see langword="false"/> if the default browser cannot be determined.
|
||||
/// </remarks>
|
||||
public static bool Open(this IBrowserInfoService browserInfoService, string url)
|
||||
{
|
||||
var defaultBrowser = browserInfoService.GetDefaultBrowser();
|
||||
return defaultBrowser != null && ShellHelpers.OpenCommandInShell(defaultBrowser.Path, defaultBrowser.ArgumentsPattern, url);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser.Providers;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
|
||||
|
||||
/// <summary>
|
||||
/// Service to get information about the default browser.
|
||||
/// </summary>
|
||||
internal class DefaultBrowserInfoService : IBrowserInfoService
|
||||
{
|
||||
private static readonly IDefaultBrowserProvider[] Providers =
|
||||
[
|
||||
new ShellAssociationProvider(),
|
||||
new LegacyRegistryAssociationProvider(),
|
||||
new FallbackMsEdgeBrowserProvider(),
|
||||
];
|
||||
|
||||
private readonly Lock _updateLock = new();
|
||||
|
||||
private readonly Dictionary<Type, string> _lastLoggedErrors = [];
|
||||
|
||||
private const long UpdateTimeout = 3000;
|
||||
private long _lastUpdateTickCount = -UpdateTimeout;
|
||||
|
||||
private BrowserInfo? _defaultBrowser;
|
||||
|
||||
public BrowserInfo? GetDefaultBrowser()
|
||||
{
|
||||
try
|
||||
{
|
||||
UpdateIfTimePassed();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// exception is already logged at this point
|
||||
}
|
||||
|
||||
return _defaultBrowser;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates only if at least more than 3000ms has passed since the last update, to avoid multiple calls to <see cref="UpdateCore"/>.
|
||||
/// (because of multiple plugins calling update at the same time.)
|
||||
/// </summary>
|
||||
private void UpdateIfTimePassed()
|
||||
{
|
||||
lock (_updateLock)
|
||||
{
|
||||
var curTickCount = Environment.TickCount64;
|
||||
if (curTickCount - _lastUpdateTickCount < UpdateTimeout && _defaultBrowser != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var newDefaultBrowser = UpdateCore();
|
||||
_defaultBrowser = newDefaultBrowser;
|
||||
_lastUpdateTickCount = curTickCount;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Consider using <see cref="UpdateIfTimePassed"/> to avoid updating multiple times.
|
||||
/// (because of multiple plugins calling update at the same time.)
|
||||
/// </summary>
|
||||
private BrowserInfo UpdateCore()
|
||||
{
|
||||
foreach (var provider in Providers)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = provider.GetDefaultBrowserInfo();
|
||||
#if DEBUG
|
||||
result = result with { Name = result.Name + " (" + provider.GetType().Name + ")" };
|
||||
#endif
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// since we run this fairly often, avoid logging the same error multiple times
|
||||
var lastLoggedError = _lastLoggedErrors.GetValueOrDefault(provider.GetType());
|
||||
var error = ex.ToString();
|
||||
if (error != lastLoggedError)
|
||||
{
|
||||
_lastLoggedErrors[provider.GetType()] = error;
|
||||
Logger.LogError($"Exception when retrieving browser using provider {provider.GetType()}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Unable to determine default browser");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// 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.Ext.WebSearch.Helpers.Browser;
|
||||
|
||||
/// <summary>
|
||||
/// Provides functionality to retrieve information about the system's default web browser.
|
||||
/// </summary>
|
||||
public interface IBrowserInfoService
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets information about the system's default web browser.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
BrowserInfo? GetDefaultBrowser();
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser.Providers;
|
||||
|
||||
internal record AssociatedApp(string? Command, string? FriendlyName);
|
||||
@@ -0,0 +1,154 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using Windows.Win32;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser.Providers;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for providers that determine the default browser via application associations.
|
||||
/// </summary>
|
||||
internal abstract class AssociationProviderBase : IDefaultBrowserProvider
|
||||
{
|
||||
protected abstract AssociatedApp? FindAssociation();
|
||||
|
||||
public BrowserInfo GetDefaultBrowserInfo()
|
||||
{
|
||||
var appAssociation = FindAssociation();
|
||||
if (appAssociation is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(appAssociation), "Could not determine default browser application.");
|
||||
}
|
||||
|
||||
var commandPattern = appAssociation.Command;
|
||||
var appAndArgs = SplitAppAndArgs(commandPattern);
|
||||
|
||||
if (string.IsNullOrEmpty(appAndArgs.Path))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(appAndArgs.Path), "Default browser program path could not be determined.");
|
||||
}
|
||||
|
||||
// Packaged applications could be an URI. Example: shell:AppsFolder\Microsoft.MicrosoftEdge.Stable_8wekyb3d8bbwe!App
|
||||
if (!Path.Exists(appAndArgs.Path) && !Uri.TryCreate(appAndArgs.Path, UriKind.Absolute, out _))
|
||||
{
|
||||
throw new ArgumentException($"Command validation failed: {commandPattern}", nameof(commandPattern));
|
||||
}
|
||||
|
||||
return new BrowserInfo
|
||||
{
|
||||
Path = appAndArgs.Path,
|
||||
Name = appAssociation.FriendlyName ?? Path.GetFileNameWithoutExtension(appAndArgs.Path),
|
||||
ArgumentsPattern = appAndArgs.Arguments,
|
||||
};
|
||||
}
|
||||
|
||||
private static (string? Path, string? Arguments) SplitAppAndArgs(string? commandPattern)
|
||||
{
|
||||
if (string.IsNullOrEmpty(commandPattern))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(commandPattern), "Default browser program command is not specified.");
|
||||
}
|
||||
|
||||
commandPattern = GetIndirectString(commandPattern);
|
||||
|
||||
// HACK: for firefox installed through Microsoft store
|
||||
// When installed through Microsoft Firefox the commandPattern does not have
|
||||
// quotes for the path. As the Program Files does have a space
|
||||
// the extracted path would be invalid, here we add the quotes to fix it
|
||||
const string FirefoxExecutableName = "firefox.exe";
|
||||
if (commandPattern.Contains(FirefoxExecutableName) && commandPattern.Contains(@"\WindowsApps\") &&
|
||||
!commandPattern.StartsWith('\"'))
|
||||
{
|
||||
var pathEndIndex = commandPattern.IndexOf(FirefoxExecutableName, StringComparison.Ordinal) +
|
||||
FirefoxExecutableName.Length;
|
||||
commandPattern = commandPattern.Insert(pathEndIndex, "\"");
|
||||
commandPattern = commandPattern.Insert(0, "\"");
|
||||
}
|
||||
|
||||
if (commandPattern.StartsWith('\"'))
|
||||
{
|
||||
var endQuoteIndex = commandPattern.IndexOf('\"', 1);
|
||||
if (endQuoteIndex != -1)
|
||||
{
|
||||
return (commandPattern[1..endQuoteIndex], commandPattern[(endQuoteIndex + 1)..].Trim());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var spaceIndex = commandPattern.IndexOf(' ');
|
||||
if (spaceIndex != -1)
|
||||
{
|
||||
return (commandPattern[..spaceIndex], commandPattern[(spaceIndex + 1)..].Trim());
|
||||
}
|
||||
}
|
||||
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
protected static string GetIndirectString(string str)
|
||||
{
|
||||
if (string.IsNullOrEmpty(str) || str[0] != '@')
|
||||
{
|
||||
return str;
|
||||
}
|
||||
|
||||
const int initialCapacity = 128;
|
||||
const int maxCapacity = 8192; // Reasonable upper limit
|
||||
int hresult;
|
||||
|
||||
unsafe
|
||||
{
|
||||
// Try with stack allocation first for common cases
|
||||
var stackBuffer = stackalloc char[initialCapacity];
|
||||
|
||||
fixed (char* pszSource = str)
|
||||
{
|
||||
hresult = PInvoke.SHLoadIndirectString(
|
||||
pszSource,
|
||||
stackBuffer,
|
||||
initialCapacity,
|
||||
null);
|
||||
|
||||
// S_OK (0) means success
|
||||
if (hresult == 0)
|
||||
{
|
||||
return new string(stackBuffer);
|
||||
}
|
||||
|
||||
// STRSAFE_E_INSUFFICIENT_BUFFER (0x8007007A) means buffer too small
|
||||
// Try with progressively larger heap buffers
|
||||
if (unchecked((uint)hresult) == 0x8007007A)
|
||||
{
|
||||
for (var capacity = initialCapacity * 2; capacity <= maxCapacity; capacity *= 2)
|
||||
{
|
||||
var heapBuffer = new char[capacity];
|
||||
fixed (char* pBuffer = heapBuffer)
|
||||
{
|
||||
hresult = PInvoke.SHLoadIndirectString(
|
||||
pszSource,
|
||||
pBuffer,
|
||||
(uint)capacity,
|
||||
null);
|
||||
|
||||
if (hresult == 0)
|
||||
{
|
||||
return new string(pBuffer);
|
||||
}
|
||||
|
||||
if (unchecked((uint)hresult) != 0x8007007A)
|
||||
{
|
||||
break; // Different error, stop retrying
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"Could not load indirect string. HRESULT: 0x{unchecked((uint)hresult):X8}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser.Providers;
|
||||
|
||||
/// <summary>
|
||||
/// Provides a fallback implementation of the default browser provider that returns information for Microsoft Edge.
|
||||
/// </summary>
|
||||
/// <remarks>This class is used when no other default browser provider is available. It supplies the path,
|
||||
/// arguments pattern, and name for Microsoft Edge as the default browser information.</remarks>
|
||||
internal sealed class FallbackMsEdgeBrowserProvider : IDefaultBrowserProvider
|
||||
{
|
||||
private const string MsEdgeArgumentsPattern = "--single-argument %1";
|
||||
|
||||
private const string MsEdgeName = "Microsoft Edge";
|
||||
|
||||
private static string MsEdgePath => Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86),
|
||||
@"Microsoft\Edge\Application\msedge.exe");
|
||||
|
||||
public BrowserInfo GetDefaultBrowserInfo() => new()
|
||||
{
|
||||
Path = MsEdgePath,
|
||||
ArgumentsPattern = MsEdgeArgumentsPattern,
|
||||
Name = MsEdgeName,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser.Providers;
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves information about the default browser.
|
||||
/// </summary>
|
||||
internal interface IDefaultBrowserProvider
|
||||
{
|
||||
BrowserInfo GetDefaultBrowserInfo();
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser.Providers;
|
||||
|
||||
/// <summary>
|
||||
/// Provides the default web browser by reading registry keys. This is a legacy method and may not work on all systems.
|
||||
/// </summary>
|
||||
internal sealed class LegacyRegistryAssociationProvider : AssociationProviderBase
|
||||
{
|
||||
protected override AssociatedApp? FindAssociation()
|
||||
{
|
||||
var progId = GetRegistryValue(
|
||||
@"HKEY_CURRENT_USER\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoiceLatest\ProgId",
|
||||
"ProgId")
|
||||
?? GetRegistryValue(
|
||||
@"HKEY_CURRENT_USER\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice",
|
||||
"ProgId");
|
||||
var appName = GetRegistryValue($@"HKEY_CLASSES_ROOT\{progId}\Application", "ApplicationName")
|
||||
?? GetRegistryValue($@"HKEY_CLASSES_ROOT\{progId}", "FriendlyTypeName");
|
||||
|
||||
if (appName is not null)
|
||||
{
|
||||
appName = GetIndirectString(appName);
|
||||
appName = appName
|
||||
.Replace("URL", null, StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("HTML", null, StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("Document", null, StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("Web", null, StringComparison.OrdinalIgnoreCase)
|
||||
.TrimEnd();
|
||||
}
|
||||
|
||||
var commandPattern = GetRegistryValue($@"HKEY_CLASSES_ROOT\{progId}\shell\open\command", null);
|
||||
|
||||
return commandPattern is null ? null : new AssociatedApp(commandPattern, appName);
|
||||
|
||||
static string? GetRegistryValue(string registryLocation, string? valueName)
|
||||
{
|
||||
return Registry.GetValue(registryLocation, valueName, null) as string;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// 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.Ext.WebSearch.Helpers.Browser.Providers;
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the default web browser using the system shell functions.
|
||||
/// </summary>
|
||||
internal sealed class ShellAssociationProvider : AssociationProviderBase
|
||||
{
|
||||
private static readonly string[] Protocols = ["https", "http"];
|
||||
|
||||
protected override AssociatedApp FindAssociation()
|
||||
{
|
||||
foreach (var protocol in Protocols)
|
||||
{
|
||||
var command = AssocQueryStringSafe(NativeMethods.AssocStr.Command, protocol);
|
||||
if (string.IsNullOrWhiteSpace(command))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var appName = AssocQueryStringSafe(NativeMethods.AssocStr.FriendlyAppName, protocol);
|
||||
|
||||
return new AssociatedApp(command, appName);
|
||||
}
|
||||
|
||||
return new AssociatedApp(null, null);
|
||||
}
|
||||
|
||||
private static unsafe string? AssocQueryStringSafe(NativeMethods.AssocStr what, string protocol)
|
||||
{
|
||||
uint cch = 0;
|
||||
|
||||
// First call: get required length (incl. null)
|
||||
_ = NativeMethods.AssocQueryStringW(NativeMethods.AssocF.IsProtocol, what, protocol, null, null, ref cch);
|
||||
if (cch == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Small buffers on stack; large on heap
|
||||
var span = cch <= 512 ? stackalloc char[(int)cch] : new char[(int)cch];
|
||||
|
||||
fixed (char* p = span)
|
||||
{
|
||||
var hr = NativeMethods.AssocQueryStringW(NativeMethods.AssocF.IsProtocol, what, protocol, null, p, ref cch);
|
||||
if (hr != 0 || cch == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// cch includes the null terminator; slice it off
|
||||
var len = (int)cch - 1;
|
||||
if (len < 0)
|
||||
{
|
||||
len = 0;
|
||||
}
|
||||
|
||||
return new string(span[..len]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,215 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using ManagedCommon;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Contains information (e.g. path to executable, name...) about the default browser.
|
||||
/// </summary>
|
||||
public static class DefaultBrowserInfo
|
||||
{
|
||||
private static readonly Lock _updateLock = new();
|
||||
|
||||
/// <summary>Gets the path to the MS Edge browser executable.</summary>
|
||||
public static string MSEdgePath => System.IO.Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86),
|
||||
@"Microsoft\Edge\Application\msedge.exe");
|
||||
|
||||
/// <summary>Gets the command line pattern of the MS Edge.</summary>
|
||||
public const string MSEdgeArgumentsPattern = "--single-argument %1";
|
||||
|
||||
public const string MSEdgeName = "Microsoft Edge";
|
||||
|
||||
/// <summary>Gets the path to default browser's executable.</summary>
|
||||
public static string? Path { get; private set; }
|
||||
|
||||
/// <summary>Gets <see cref="Path"/> since the icon is embedded in the executable.</summary>
|
||||
public static string? IconPath => Path;
|
||||
|
||||
/// <summary>Gets the user-friendly name of the default browser.</summary>
|
||||
public static string? Name { get; private set; }
|
||||
|
||||
/// <summary>Gets the command line pattern of the default browser.</summary>
|
||||
public static string? ArgumentsPattern { get; private set; }
|
||||
|
||||
public static bool IsDefaultBrowserSet => !string.IsNullOrEmpty(Path);
|
||||
|
||||
public const long UpdateTimeout = 300;
|
||||
|
||||
private static long _lastUpdateTickCount = -UpdateTimeout;
|
||||
|
||||
private static bool _updatedOnce;
|
||||
private static bool _errorLogged;
|
||||
|
||||
/// <summary>
|
||||
/// Updates only if at least more than 300ms has passed since the last update, to avoid multiple calls to <see cref="Update"/>.
|
||||
/// (because of multiple plugins calling update at the same time.)
|
||||
/// </summary>
|
||||
public static void UpdateIfTimePassed()
|
||||
{
|
||||
var curTickCount = Environment.TickCount64;
|
||||
if (curTickCount - _lastUpdateTickCount >= UpdateTimeout)
|
||||
{
|
||||
_lastUpdateTickCount = curTickCount;
|
||||
Update();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Consider using <see cref="UpdateIfTimePassed"/> to avoid updating multiple times.
|
||||
/// (because of multiple plugins calling update at the same time.)
|
||||
/// </summary>
|
||||
public static void Update()
|
||||
{
|
||||
lock (_updateLock)
|
||||
{
|
||||
if (!_updatedOnce)
|
||||
{
|
||||
// Log.Info("I've tried updating the chosen Web Browser info at least once.", typeof(DefaultBrowserInfo));
|
||||
_updatedOnce = true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var progId = GetRegistryValue(
|
||||
@"HKEY_CURRENT_USER\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoiceLatest\ProgId",
|
||||
"ProgId")
|
||||
?? GetRegistryValue(
|
||||
@"HKEY_CURRENT_USER\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice",
|
||||
"ProgId");
|
||||
var appName = GetRegistryValue($@"HKEY_CLASSES_ROOT\{progId}\Application", "ApplicationName")
|
||||
?? GetRegistryValue($@"HKEY_CLASSES_ROOT\{progId}", "FriendlyTypeName");
|
||||
|
||||
if (appName is not null)
|
||||
{
|
||||
// Handle indirect strings:
|
||||
if (appName.StartsWith('@'))
|
||||
{
|
||||
appName = GetIndirectString(appName);
|
||||
}
|
||||
|
||||
appName = appName
|
||||
.Replace("URL", null, StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("HTML", null, StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("Document", null, StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("Web", null, StringComparison.OrdinalIgnoreCase)
|
||||
.TrimEnd();
|
||||
}
|
||||
|
||||
Name = appName;
|
||||
|
||||
var commandPattern = GetRegistryValue($@"HKEY_CLASSES_ROOT\{progId}\shell\open\command", null);
|
||||
|
||||
if (string.IsNullOrEmpty(commandPattern))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(commandPattern),
|
||||
"Default browser program command is not specified.");
|
||||
}
|
||||
|
||||
if (commandPattern.StartsWith('@'))
|
||||
{
|
||||
commandPattern = GetIndirectString(commandPattern);
|
||||
}
|
||||
|
||||
// HACK: for firefox installed through Microsoft store
|
||||
// When installed through Microsoft Firefox the commandPattern does not have
|
||||
// quotes for the path. As the Program Files does have a space
|
||||
// the extracted path would be invalid, here we add the quotes to fix it
|
||||
const string FirefoxExecutableName = "firefox.exe";
|
||||
if (commandPattern.Contains(FirefoxExecutableName) && commandPattern.Contains(@"\WindowsApps\") && (!commandPattern.StartsWith('\"')))
|
||||
{
|
||||
var pathEndIndex = commandPattern.IndexOf(FirefoxExecutableName, StringComparison.Ordinal) + FirefoxExecutableName.Length;
|
||||
commandPattern = commandPattern.Insert(pathEndIndex, "\"");
|
||||
commandPattern = commandPattern.Insert(0, "\"");
|
||||
}
|
||||
|
||||
if (commandPattern.StartsWith('\"'))
|
||||
{
|
||||
var endQuoteIndex = commandPattern.IndexOf('\"', 1);
|
||||
if (endQuoteIndex != -1)
|
||||
{
|
||||
Path = commandPattern.Substring(1, endQuoteIndex - 1);
|
||||
ArgumentsPattern = commandPattern.Substring(endQuoteIndex + 1).Trim();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var spaceIndex = commandPattern.IndexOf(' ');
|
||||
if (spaceIndex != -1)
|
||||
{
|
||||
Path = commandPattern.Substring(0, spaceIndex);
|
||||
ArgumentsPattern = commandPattern.Substring(spaceIndex + 1).Trim();
|
||||
}
|
||||
}
|
||||
|
||||
// Packaged applications could be an URI. Example: shell:AppsFolder\Microsoft.MicrosoftEdge.Stable_8wekyb3d8bbwe!App
|
||||
if (!System.IO.Path.Exists(Path) && !Uri.TryCreate(Path, UriKind.Absolute, out _))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"Command validation failed: {commandPattern}",
|
||||
nameof(commandPattern));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(Path))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(Path),
|
||||
"Default browser program path could not be determined.");
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Fallback to MS Edge
|
||||
Path = MSEdgePath;
|
||||
Name = MSEdgeName;
|
||||
ArgumentsPattern = MSEdgeArgumentsPattern;
|
||||
|
||||
if (!_errorLogged)
|
||||
{
|
||||
// Log.Exception("Exception when retrieving browser path/name. Path and Name are set to use Microsoft Edge.", e, typeof(DefaultBrowserInfo));
|
||||
Logger.LogError("Exception when retrieving browser path/name. Path and Name are set to use Microsoft Edge.");
|
||||
_errorLogged = true;
|
||||
}
|
||||
}
|
||||
|
||||
string? GetRegistryValue(string registryLocation, string? valueName)
|
||||
{
|
||||
return Microsoft.Win32.Registry.GetValue(registryLocation, valueName, null) as string;
|
||||
}
|
||||
|
||||
string GetIndirectString(string str)
|
||||
{
|
||||
var stringBuilder = new StringBuilder(128);
|
||||
unsafe
|
||||
{
|
||||
var buffer = stackalloc char[128];
|
||||
var capacity = 128;
|
||||
var firstChar = str[0];
|
||||
var strPtr = &firstChar;
|
||||
|
||||
// S_OK == 0
|
||||
fixed (char* pszSourceLocal = str)
|
||||
{
|
||||
if (global::Windows.Win32.PInvoke.SHLoadIndirectString(
|
||||
pszSourceLocal,
|
||||
buffer,
|
||||
(uint)capacity,
|
||||
default) == 0)
|
||||
{
|
||||
return new string(buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new ArgumentNullException(nameof(str), "Could not load indirect string.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers;
|
||||
|
||||
internal static partial class NativeMethods
|
||||
{
|
||||
[LibraryImport("shlwapi.dll", StringMarshalling = StringMarshalling.Utf16, SetLastError = false)]
|
||||
internal static unsafe partial int AssocQueryStringW(
|
||||
AssocF flags,
|
||||
AssocStr str,
|
||||
string pszAssoc,
|
||||
string? pszExtra,
|
||||
char* pszOut,
|
||||
ref uint pcchOut);
|
||||
|
||||
[Flags]
|
||||
public enum AssocF : uint
|
||||
{
|
||||
None = 0,
|
||||
IsProtocol = 0x00001000,
|
||||
}
|
||||
|
||||
public enum AssocStr
|
||||
{
|
||||
Command = 1,
|
||||
Executable,
|
||||
FriendlyDocName,
|
||||
FriendlyAppName,
|
||||
NoOpen,
|
||||
ShellNewValue,
|
||||
DDECommand,
|
||||
DDEIfExec,
|
||||
DDEApplication,
|
||||
DDETopic,
|
||||
InfoTip,
|
||||
QuickTip,
|
||||
TileInfo,
|
||||
ContentType,
|
||||
DefaultIcon,
|
||||
ShellExtension,
|
||||
DropTarget,
|
||||
DelegateExecute,
|
||||
SupportedUriProtocols,
|
||||
ProgId,
|
||||
AppId,
|
||||
AppPublisher,
|
||||
AppIconReference, // sometimes present, but DefaultIcon is most common
|
||||
}
|
||||
}
|
||||
@@ -9,23 +9,24 @@ using System.Text;
|
||||
using System.Threading;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Commands;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Helpers;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Properties;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using BrowserInfo = Microsoft.CmdPal.Ext.WebSearch.Helpers.DefaultBrowserInfo;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.WebSearch.Pages;
|
||||
|
||||
internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable
|
||||
{
|
||||
private readonly ISettingsInterface _settingsManager;
|
||||
private readonly IBrowserInfoService _browserInfoService;
|
||||
private readonly Lock _sync = new();
|
||||
private static readonly CompositeFormat PluginInBrowserName = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_in_browser_name);
|
||||
private static readonly CompositeFormat PluginOpen = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open);
|
||||
private IListItem[] _allItems = [];
|
||||
private List<ListItem> _historyItems = [];
|
||||
|
||||
public WebSearchListPage(ISettingsInterface settingsManager)
|
||||
public WebSearchListPage(ISettingsInterface settingsManager, IBrowserInfoService browserInfoService)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(settingsManager);
|
||||
|
||||
@@ -35,6 +36,7 @@ internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable
|
||||
Id = "com.microsoft.cmdpal.websearch";
|
||||
|
||||
_settingsManager = settingsManager;
|
||||
_browserInfoService = browserInfoService;
|
||||
_settingsManager.HistoryChanged += SettingsManagerOnHistoryChanged;
|
||||
|
||||
// It just looks viewer to have string twice on the page, and default placeholder is good enough
|
||||
@@ -43,8 +45,8 @@ internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable
|
||||
EmptyContent = new CommandItem(new NoOpCommand())
|
||||
{
|
||||
Icon = Icon,
|
||||
Title = Properties.Resources.plugin_description,
|
||||
Subtitle = string.Format(CultureInfo.CurrentCulture, PluginInBrowserName, BrowserInfo.Name ?? BrowserInfo.MSEdgeName),
|
||||
Title = Resources.plugin_description,
|
||||
Subtitle = string.Format(CultureInfo.CurrentCulture, PluginInBrowserName, browserInfoService.GetDefaultBrowser()?.Name ?? Resources.default_browser),
|
||||
};
|
||||
|
||||
UpdateHistory();
|
||||
@@ -67,7 +69,7 @@ internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable
|
||||
for (var index = items.Count - 1; index >= 0; index--)
|
||||
{
|
||||
var historyItem = items[index];
|
||||
history.Add(new ListItem(new SearchWebCommand(historyItem.SearchString, _settingsManager))
|
||||
history.Add(new ListItem(new SearchWebCommand(historyItem.SearchString, _settingsManager, _browserInfoService))
|
||||
{
|
||||
Icon = Icons.History,
|
||||
Title = historyItem.SearchString,
|
||||
@@ -82,7 +84,7 @@ internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
private static IListItem[] Query(string query, List<ListItem> historySnapshot, ISettingsInterface settingsManager)
|
||||
private static IListItem[] Query(string query, List<ListItem> historySnapshot, ISettingsInterface settingsManager, IBrowserInfoService browserInfoService)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(query);
|
||||
|
||||
@@ -95,10 +97,10 @@ internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable
|
||||
if (!string.IsNullOrEmpty(query))
|
||||
{
|
||||
var searchTerm = query;
|
||||
var result = new ListItem(new SearchWebCommand(searchTerm, settingsManager))
|
||||
var result = new ListItem(new SearchWebCommand(searchTerm, settingsManager, browserInfoService))
|
||||
{
|
||||
Title = searchTerm,
|
||||
Subtitle = string.Format(CultureInfo.CurrentCulture, PluginOpen, BrowserInfo.Name ?? BrowserInfo.MSEdgeName),
|
||||
Subtitle = string.Format(CultureInfo.CurrentCulture, PluginOpen, browserInfoService.GetDefaultBrowser()?.Name ?? Resources.default_browser),
|
||||
Icon = Icons.Search,
|
||||
};
|
||||
results.Add(result);
|
||||
@@ -117,7 +119,7 @@ internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable
|
||||
historySnapshot = _historyItems;
|
||||
}
|
||||
|
||||
var items = Query(search ?? string.Empty, historySnapshot, _settingsManager);
|
||||
var items = Query(search ?? string.Empty, historySnapshot, _settingsManager, _browserInfoService);
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace Microsoft.CmdPal.Ext.WebSearch.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()]
|
||||
public class Resources {
|
||||
@@ -69,6 +69,15 @@ namespace Microsoft.CmdPal.Ext.WebSearch.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to default browser.
|
||||
/// </summary>
|
||||
public static string default_browser {
|
||||
get {
|
||||
return ResourceManager.GetString("default_browser", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Web Search.
|
||||
/// </summary>
|
||||
|
||||
@@ -184,4 +184,7 @@
|
||||
<data name="open_url_fallback_title" xml:space="preserve">
|
||||
<value>Open URL</value>
|
||||
</data>
|
||||
<data name="default_browser" xml:space="preserve">
|
||||
<value>default browser</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -5,6 +5,7 @@
|
||||
using System;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Commands;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Helpers;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Properties;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
@@ -19,6 +20,7 @@ public sealed partial class WebSearchCommandsProvider : CommandProvider
|
||||
private readonly WebSearchTopLevelCommandItem _webSearchTopLevelItem;
|
||||
private readonly ICommandItem[] _topLevelItems;
|
||||
private readonly IFallbackCommandItem[] _fallbackCommands;
|
||||
private readonly IBrowserInfoService _browserInfoService = new DefaultBrowserInfoService();
|
||||
|
||||
public WebSearchCommandsProvider()
|
||||
{
|
||||
@@ -27,10 +29,10 @@ public sealed partial class WebSearchCommandsProvider : CommandProvider
|
||||
Icon = Icons.WebSearch;
|
||||
Settings = _settingsManager.Settings;
|
||||
|
||||
_fallbackItem = new FallbackExecuteSearchItem(_settingsManager);
|
||||
_openUrlFallbackItem = new FallbackOpenURLItem(_settingsManager);
|
||||
_fallbackItem = new FallbackExecuteSearchItem(_settingsManager, _browserInfoService);
|
||||
_openUrlFallbackItem = new FallbackOpenURLItem(_settingsManager, _browserInfoService);
|
||||
|
||||
_webSearchTopLevelItem = new WebSearchTopLevelCommandItem(_settingsManager)
|
||||
_webSearchTopLevelItem = new WebSearchTopLevelCommandItem(_settingsManager, _browserInfoService)
|
||||
{
|
||||
MoreCommands =
|
||||
[
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
using System;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Commands;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Helpers;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Pages;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Properties;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
@@ -15,13 +16,15 @@ namespace Microsoft.CmdPal.Ext.WebSearch;
|
||||
public partial class WebSearchTopLevelCommandItem : CommandItem, IFallbackHandler, IDisposable
|
||||
{
|
||||
private readonly SettingsManager _settingsManager;
|
||||
private readonly IBrowserInfoService _browserInfoService;
|
||||
|
||||
public WebSearchTopLevelCommandItem(SettingsManager settingsManager)
|
||||
: base(new WebSearchListPage(settingsManager))
|
||||
public WebSearchTopLevelCommandItem(SettingsManager settingsManager, IBrowserInfoService browserInfoService)
|
||||
: base(new WebSearchListPage(settingsManager, browserInfoService))
|
||||
{
|
||||
Icon = Icons.WebSearch;
|
||||
SetDefaultTitle();
|
||||
_settingsManager = settingsManager;
|
||||
_browserInfoService = browserInfoService;
|
||||
}
|
||||
|
||||
private void SetDefaultTitle() => Title = Resources.command_item_title;
|
||||
@@ -37,12 +40,12 @@ public partial class WebSearchTopLevelCommandItem : CommandItem, IFallbackHandle
|
||||
if (string.IsNullOrEmpty(query))
|
||||
{
|
||||
SetDefaultTitle();
|
||||
ReplaceCommand(new WebSearchListPage(_settingsManager));
|
||||
ReplaceCommand(new WebSearchListPage(_settingsManager, _browserInfoService));
|
||||
}
|
||||
else
|
||||
{
|
||||
Title = query;
|
||||
ReplaceCommand(new SearchWebCommand(query, _settingsManager));
|
||||
ReplaceCommand(new SearchWebCommand(query, _settingsManager, _browserInfoService));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,13 +24,13 @@
|
||||
<ApplicationIcon>Resources\ImageResizer.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- <PropertyGroup>
|
||||
<ApplicationManifest>ImageResizerUI.dev.manifest</ApplicationManifest>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(CIBuild)'=='true'">
|
||||
<ApplicationManifest>ImageResizerUI.prod.manifest</ApplicationManifest>
|
||||
</PropertyGroup>
|
||||
</PropertyGroup> -->
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="Properties\Resources.resx">
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
|
||||
Example:
|
||||
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
@@ -26,36 +26,36 @@
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
@@ -3410,7 +3410,7 @@ Activate by holding the key for the character you want to add an accent to, then
|
||||
<value>An AI powered tool to put your clipboard content into any format you need, focused towards developer workflows.</value>
|
||||
</data>
|
||||
<data name="AdvancedPaste_EnableAISettingsCardDescription.Text" xml:space="preserve">
|
||||
<value>Transform your clipboard content with the power of AI. An cloud or local endpoint is required.</value>
|
||||
<value>Transform your clipboard content with the power of AI. A cloud or local endpoint is required.</value>
|
||||
</data>
|
||||
<data name="AdvancedPaste_EnableAISettingsCardDescriptionLearnMore.Content" xml:space="preserve">
|
||||
<value>Learn more</value>
|
||||
@@ -5164,7 +5164,7 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m
|
||||
<comment>Command Palette is a product name, do not loc</comment>
|
||||
</data>
|
||||
<data name="LearnMore_CmdPal.Text" xml:space="preserve">
|
||||
<value>Learn more</value>
|
||||
<value>Learn more about Command Palette</value>
|
||||
</data>
|
||||
<data name="Shell_CmdPal.Content" xml:space="preserve">
|
||||
<value>Command Palette</value>
|
||||
@@ -5781,4 +5781,4 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m
|
||||
<value>A modern UI built with Fluent Design</value>
|
||||
<comment>Fluent Design is a product name, do not loc</comment>
|
||||
</data>
|
||||
</root>
|
||||
</root>
|
||||
|
||||
37
tools/module_loader/ModuleLoader.manifest
Normal file
37
tools/module_loader/ModuleLoader.manifest
Normal file
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<assemblyIdentity
|
||||
version="1.0.0.0"
|
||||
processorArchitecture="*"
|
||||
name="Microsoft.PowerToys.ModuleLoader"
|
||||
type="win32"
|
||||
/>
|
||||
<description>PowerToys Module Loader - Standalone module testing utility</description>
|
||||
|
||||
<!-- Per-Monitor DPI Awareness V2 -->
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/PM</dpiAware>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
|
||||
<!-- Request administrator execution level if needed -->
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||
<security>
|
||||
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
|
||||
<!-- Windows 10+ compatibility -->
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- Windows 10 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||
<!-- Windows 11 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9b}"/>
|
||||
</application>
|
||||
</compatibility>
|
||||
</assembly>
|
||||
205
tools/module_loader/ModuleLoader.vcxproj
Normal file
205
tools/module_loader/ModuleLoader.vcxproj
Normal file
@@ -0,0 +1,205 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Debug|ARM64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>ARM64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|ARM64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>ARM64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>17.0</VCProjectVersion>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
<ProjectGuid>{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}</ProjectGuid>
|
||||
<RootNamespace>ModuleLoader</RootNamespace>
|
||||
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
|
||||
<ProjectName>ModuleLoader</ProjectName>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<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|x64'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<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|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|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|ARM64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\</OutDir>
|
||||
<IntDir>$(Platform)\$(Configuration)\</IntDir>
|
||||
<TargetName>ModuleLoader</TargetName>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">
|
||||
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\</OutDir>
|
||||
<IntDir>$(Platform)\$(Configuration)\</IntDir>
|
||||
<TargetName>ModuleLoader</TargetName>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\</OutDir>
|
||||
<IntDir>$(Platform)\$(Configuration)\</IntDir>
|
||||
<TargetName>ModuleLoader</TargetName>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
|
||||
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\</OutDir>
|
||||
<IntDir>$(Platform)\$(Configuration)\</IntDir>
|
||||
<TargetName>ModuleLoader</TargetName>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level4</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<LanguageStandard>stdcpp20</LanguageStandard>
|
||||
<AdditionalIncludeDirectories>$(ProjectDir)src;$(SolutionDir)src\modules\interface;$(SolutionDir)src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<PrecompiledHeader>NotUsing</PrecompiledHeader>
|
||||
<TreatWarningAsError>false</TreatWarningAsError>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<AdditionalDependencies>kernel32.lib;user32.lib;Shcore.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalManifestDependencies>type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'</AdditionalManifestDependencies>
|
||||
</Link>
|
||||
<Manifest>
|
||||
<AdditionalManifestFiles>$(ProjectDir)ModuleLoader.manifest</AdditionalManifestFiles>
|
||||
</Manifest>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level4</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<LanguageStandard>stdcpp20</LanguageStandard>
|
||||
<AdditionalIncludeDirectories>$(ProjectDir)src;$(SolutionDir)src\modules\interface;$(SolutionDir)src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<PrecompiledHeader>NotUsing</PrecompiledHeader>
|
||||
<TreatWarningAsError>false</TreatWarningAsError>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<AdditionalDependencies>kernel32.lib;user32.lib;Shcore.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalManifestDependencies>type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'</AdditionalManifestDependencies>
|
||||
</Link>
|
||||
<Manifest>
|
||||
<AdditionalManifestFiles>$(ProjectDir)ModuleLoader.manifest</AdditionalManifestFiles>
|
||||
</Manifest>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level4</WarningLevel>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<LanguageStandard>stdcpp20</LanguageStandard>
|
||||
<AdditionalIncludeDirectories>$(ProjectDir)src;$(SolutionDir)src\modules\interface;$(SolutionDir)src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<PrecompiledHeader>NotUsing</PrecompiledHeader>
|
||||
<TreatWarningAsError>false</TreatWarningAsError>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<AdditionalDependencies>kernel32.lib;user32.lib;Shcore.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalManifestDependencies>type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'</AdditionalManifestDependencies>
|
||||
</Link>
|
||||
<Manifest>
|
||||
<AdditionalManifestFiles>$(ProjectDir)ModuleLoader.manifest</AdditionalManifestFiles>
|
||||
</Manifest>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level4</WarningLevel>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<LanguageStandard>stdcpp20</LanguageStandard>
|
||||
<AdditionalIncludeDirectories>$(ProjectDir)src;$(SolutionDir)src\modules\interface;$(SolutionDir)src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<PrecompiledHeader>NotUsing</PrecompiledHeader>
|
||||
<TreatWarningAsError>false</TreatWarningAsError>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<AdditionalDependencies>kernel32.lib;user32.lib;Shcore.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalManifestDependencies>type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'</AdditionalManifestDependencies>
|
||||
</Link>
|
||||
<Manifest>
|
||||
<AdditionalManifestFiles>$(ProjectDir)ModuleLoader.manifest</AdditionalManifestFiles>
|
||||
</Manifest>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="src\main.cpp" />
|
||||
<ClCompile Include="src\ModuleLoader.cpp" />
|
||||
<ClCompile Include="src\SettingsLoader.cpp" />
|
||||
<ClCompile Include="src\HotkeyManager.cpp" />
|
||||
<ClCompile Include="src\ConsoleHost.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="src\ModuleLoader.h" />
|
||||
<ClInclude Include="src\SettingsLoader.h" />
|
||||
<ClInclude Include="src\HotkeyManager.h" />
|
||||
<ClInclude Include="src\ConsoleHost.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="README.md" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
</ImportGroup>
|
||||
</Project>
|
||||
51
tools/module_loader/ModuleLoader.vcxproj.filters
Normal file
51
tools/module_loader/ModuleLoader.vcxproj.filters
Normal file
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup>
|
||||
<Filter Include="Source Files">
|
||||
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
|
||||
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="Header Files">
|
||||
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
|
||||
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="Resource Files">
|
||||
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
|
||||
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
|
||||
</Filter>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="src\main.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\ModuleLoader.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\SettingsLoader.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\HotkeyManager.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\ConsoleHost.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="src\ModuleLoader.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\SettingsLoader.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\HotkeyManager.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\ConsoleHost.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="README.md" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
483
tools/module_loader/SHARING.md
Normal file
483
tools/module_loader/SHARING.md
Normal file
@@ -0,0 +1,483 @@
|
||||
# Sharing ModuleLoader and Modules
|
||||
|
||||
This guide explains how to share the ModuleLoader tool and PowerToy modules with others for testing purposes.
|
||||
|
||||
## Overview
|
||||
|
||||
The ModuleLoader is designed to be a **portable, standalone testing tool** that can be shared with module developers and testers. It has minimal dependencies and can work with any compatible PowerToy module DLL.
|
||||
|
||||
---
|
||||
|
||||
## What You Need to Share
|
||||
|
||||
### For Testing a Module (e.g., CursorWrap)
|
||||
|
||||
#### **Minimum Package** (Recommended for Quick Testing)
|
||||
|
||||
1. **ModuleLoader.exe** - The standalone loader application
|
||||
- Location: `x64\Debug\ModuleLoader.exe` or `x64\Release\ModuleLoader.exe`
|
||||
- No additional DLLs required (uses only Windows system libraries)
|
||||
|
||||
2. **The Module DLL** - The PowerToy module to test
|
||||
- Example: `CursorWrap.dll` from `x64\Debug\` or `x64\Release\`
|
||||
- Location varies by module (see module-specific locations below)
|
||||
|
||||
3. **settings.json** - Module configuration (place in same folder as the DLL)
|
||||
- **NEW**: Settings can be placed alongside the module DLL for portable testing
|
||||
- Location: Same directory as the module DLL (e.g., `settings.json` next to `CursorWrap.dll`)
|
||||
- Falls back to: `%LOCALAPPDATA%\Microsoft\PowerToys\<ModuleName>\settings.json` if not found locally
|
||||
|
||||
#### **Complete Standalone Package** (For Users Without PowerToys Installed)
|
||||
|
||||
1. **ModuleLoader.exe**
|
||||
2. **Module DLL**
|
||||
3. **Sample settings.json** - Pre-configured settings file
|
||||
4. **Installation instructions** - See "Standalone Package Setup" section below
|
||||
|
||||
---
|
||||
|
||||
### Debug Builds
|
||||
If you build the module in Debug configuration:
|
||||
- The module will output debug messages via `OutputDebugString()`
|
||||
- View these with [DebugView](https://learn.microsoft.com/sysinternals/downloads/debugview) or Visual Studio Output window
|
||||
- Example: CursorWrap outputs detailed topology and cursor wrapping debug info
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Module-Specific File Locations
|
||||
|
||||
### CursorWrap
|
||||
```
|
||||
Files to share:
|
||||
- x64\Debug\CursorWrap.dll (or Release)
|
||||
- %LOCALAPPDATA%\Microsoft\PowerToys\CursorWrap\settings.json
|
||||
|
||||
Size: ~100KB
|
||||
```
|
||||
|
||||
### MouseHighlighter
|
||||
```
|
||||
Files to share:
|
||||
- x64\Debug\MouseHighlighter.dll (or Release)
|
||||
- %LOCALAPPDATA%\Microsoft\PowerToys\MouseHighlighter\settings.json
|
||||
|
||||
Size: ~150KB
|
||||
```
|
||||
|
||||
### FindMyMouse
|
||||
```
|
||||
Files to share:
|
||||
- x64\Debug\FindMyMouse.dll (or Release)
|
||||
- %LOCALAPPDATA%\Microsoft\PowerToys\FindMyMouse\settings.json
|
||||
|
||||
Size: ~120KB
|
||||
```
|
||||
|
||||
### MousePointerCrosshairs
|
||||
```
|
||||
Files to share:
|
||||
- x64\Debug\MousePointerCrosshairs.dll (or Release)
|
||||
- %LOCALAPPDATA%\Microsoft\PowerToys\MousePointerCrosshairs\settings.json
|
||||
|
||||
Size: ~140KB
|
||||
```
|
||||
|
||||
### MouseJump
|
||||
```
|
||||
Files to share:
|
||||
- x64\Debug\MouseJump.dll (or Release)
|
||||
- %LOCALAPPDATA%\Microsoft\PowerToys\MouseJump\settings.json
|
||||
|
||||
Note: MouseJump is a UI-based module and may not work fully with ModuleLoader
|
||||
Size: ~200KB
|
||||
```
|
||||
|
||||
### AlwaysOnTop
|
||||
```
|
||||
Files to share:
|
||||
- x64\Debug\AlwaysOnTop.dll (or Release)
|
||||
- %LOCALAPPDATA%\Microsoft\PowerToys\AlwaysOnTop\settings.json
|
||||
|
||||
Size: ~100KB
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dependency Analysis
|
||||
|
||||
### ModuleLoader.exe Dependencies
|
||||
**Windows System Libraries Only** (automatically available on all Windows systems):
|
||||
- `KERNEL32.dll` - Core Windows API
|
||||
- `USER32.dll` - User interface functions
|
||||
- `SHELL32.dll` - Shell functions
|
||||
- `ole32.dll` - COM library
|
||||
|
||||
**No PowerToys dependencies required!** The ModuleLoader is completely standalone.
|
||||
|
||||
### Module DLL Dependencies (Typical)
|
||||
Most PowerToy modules depend on:
|
||||
- Windows system DLLs (automatically available)
|
||||
- PowerToys common libraries (if any, they're typically statically linked)
|
||||
- **Module settings** - Must be present in `%LOCALAPPDATA%\Microsoft\PowerToys\<ModuleName>\`
|
||||
|
||||
**Important**: Modules are generally **self-contained** and statically link most dependencies. You typically only need the module DLL itself.
|
||||
|
||||
---
|
||||
|
||||
## Creating a Standalone Package
|
||||
|
||||
### Step 1: Prepare the Files
|
||||
|
||||
Create a folder structure like this:
|
||||
```
|
||||
ModuleLoaderPackage\
|
||||
??? ModuleLoader.exe
|
||||
??? CursorWrap.dll (or other module)
|
||||
??? settings.json (module settings - placed locally!)
|
||||
```
|
||||
|
||||
**NEW Simplified Structure**: You can now place `settings.json` directly alongside the module DLL! The ModuleLoader will check this location first before looking in the standard PowerToys settings directories.
|
||||
|
||||
### Step 2: Extract Settings from Your Machine
|
||||
|
||||
```powershell
|
||||
# Copy settings from your development machine
|
||||
$moduleName = "CursorWrap" # Change as needed
|
||||
$settingsPath = "$env:LOCALAPPDATA\Microsoft\PowerToys\$moduleName\settings.json"
|
||||
Copy-Item $settingsPath ".\settings\$moduleName\settings.json"
|
||||
```
|
||||
|
||||
### Step 3: Create Installation Instructions (README.txt)
|
||||
|
||||
```text
|
||||
PowerToys Module Testing Package
|
||||
=================================
|
||||
|
||||
This package contains the ModuleLoader tool for testing PowerToy modules.
|
||||
|
||||
Contents:
|
||||
- ModuleLoader.exe : Standalone module loader
|
||||
- modules\*.dll : PowerToy module(s) to test
|
||||
- settings\*\*.json : Module configuration files
|
||||
|
||||
Setup (First Time):
|
||||
-------------------
|
||||
1. Create settings directory:
|
||||
%LOCALAPPDATA%\Microsoft\PowerToys\
|
||||
|
||||
2. Copy settings:
|
||||
Copy the entire "settings\<ModuleName>" folder to:
|
||||
%LOCALAPPDATA%\Microsoft\PowerToys\
|
||||
|
||||
Example for CursorWrap:
|
||||
Copy "settings\CursorWrap" to:
|
||||
%LOCALAPPDATA%\Microsoft\PowerToys\CursorWrap\
|
||||
|
||||
Usage:
|
||||
------
|
||||
ModuleLoader.exe modules\CursorWrap.dll
|
||||
|
||||
The tool will:
|
||||
- Load the module DLL
|
||||
- Read settings from %LOCALAPPDATA%\Microsoft\PowerToys\<ModuleName>\
|
||||
- Register hotkeys
|
||||
- Enable the module
|
||||
|
||||
Press Ctrl+C to exit.
|
||||
Press the module's hotkey to toggle functionality.
|
||||
|
||||
Requirements:
|
||||
-------------
|
||||
- Windows 10 1803 or later
|
||||
- No PowerToys installation required!
|
||||
|
||||
Troubleshooting:
|
||||
----------------
|
||||
If you see "Settings file not found":
|
||||
1. Make sure you copied the settings folder correctly
|
||||
2. Check that the path is:
|
||||
%LOCALAPPDATA%\Microsoft\PowerToys\<ModuleName>\settings.json
|
||||
3. You can also run PowerToys once to generate default settings
|
||||
|
||||
Debug Logs:
|
||||
-----------
|
||||
Module logs are written to:
|
||||
%LOCALAPPDATA%\Microsoft\PowerToys\<ModuleName>\Logs\
|
||||
|
||||
For debug builds, use DebugView to see real-time output.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Distribution Methods
|
||||
|
||||
### Method 1: ZIP Archive
|
||||
```powershell
|
||||
# Create a complete package
|
||||
$moduleName = "CursorWrap"
|
||||
$packageName = "ModuleLoader-$moduleName-Package"
|
||||
|
||||
# Collect files
|
||||
New-Item $packageName -ItemType Directory
|
||||
Copy-Item "x64\Debug\ModuleLoader.exe" "$packageName\"
|
||||
New-Item "$packageName\modules" -ItemType Directory
|
||||
Copy-Item "x64\Debug\$moduleName.dll" "$packageName\modules\"
|
||||
New-Item "$packageName\settings\$moduleName" -ItemType Directory -Force
|
||||
Copy-Item "$env:LOCALAPPDATA\Microsoft\PowerToys\$moduleName\settings.json" "$packageName\settings\$moduleName\"
|
||||
|
||||
# Create README
|
||||
@"
|
||||
See README in the tools\module_loader folder for instructions
|
||||
"@ | Out-File "$packageName\README.txt"
|
||||
|
||||
# Zip it
|
||||
Compress-Archive -Path $packageName -DestinationPath "$packageName.zip"
|
||||
```
|
||||
|
||||
### Method 2: Direct Share (Advanced Users)
|
||||
For developers who already have PowerToys installed:
|
||||
```powershell
|
||||
# Just share the executables
|
||||
Copy-Item "x64\Debug\ModuleLoader.exe" "\\ShareLocation\"
|
||||
Copy-Item "x64\Debug\CursorWrap.dll" "\\ShareLocation\"
|
||||
```
|
||||
|
||||
They can run: `ModuleLoader.exe CursorWrap.dll`
|
||||
(Settings will be loaded from their existing PowerToys installation)
|
||||
|
||||
---
|
||||
|
||||
## Platform-Specific Notes
|
||||
|
||||
### x64 vs ARM64
|
||||
|
||||
**Important**: Match architectures!
|
||||
- `x64\Debug\ModuleLoader.exe` ? Only works with `x64` module DLLs
|
||||
- `ARM64\Debug\ModuleLoader.exe` ? Only works with `ARM64` module DLLs
|
||||
|
||||
**Distribution Tip**: Provide both architectures if targeting multiple platforms:
|
||||
```
|
||||
ModuleLoaderPackage\
|
||||
??? x64\
|
||||
? ??? ModuleLoader.exe
|
||||
? ??? modules\CursorWrap.dll
|
||||
??? ARM64\
|
||||
? ??? ModuleLoader.exe
|
||||
? ??? modules\CursorWrap.dll
|
||||
??? settings\...
|
||||
```
|
||||
|
||||
### Debug vs Release
|
||||
|
||||
**Debug builds**:
|
||||
- Larger file size
|
||||
- Include debug symbols
|
||||
- Verbose logging via `OutputDebugString()`
|
||||
- Recommended for testing/development
|
||||
|
||||
**Release builds**:
|
||||
- Smaller file size
|
||||
- Optimized performance
|
||||
- Minimal logging
|
||||
- Recommended for end-user testing
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
Before sharing a module package:
|
||||
|
||||
- [ ] ModuleLoader.exe is included
|
||||
- [ ] Module DLL is included (matching architecture)
|
||||
- [ ] Sample settings.json is included
|
||||
- [ ] README/instructions are included
|
||||
- [ ] Tested on a clean machine (no PowerToys installed)
|
||||
- [ ] Verified hotkeys work
|
||||
- [ ] Verified Ctrl+C exits cleanly
|
||||
- [ ] Confirmed settings path in documentation
|
||||
|
||||
---
|
||||
|
||||
## Advanced: Portable Package Script
|
||||
|
||||
Here's a complete PowerShell script to create a fully portable package:
|
||||
|
||||
```powershell
|
||||
param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$ModuleName,
|
||||
|
||||
[ValidateSet("Debug", "Release")]
|
||||
[string]$Configuration = "Debug",
|
||||
|
||||
[ValidateSet("x64", "ARM64")]
|
||||
[string]$Platform = "x64"
|
||||
)
|
||||
|
||||
$packageName = "ModuleLoader-$ModuleName-$Platform-$Configuration"
|
||||
$packagePath = ".\$packageName"
|
||||
|
||||
Write-Host "Creating portable package: $packageName" -ForegroundColor Green
|
||||
|
||||
# Create structure
|
||||
New-Item $packagePath -ItemType Directory -Force | Out-Null
|
||||
New-Item "$packagePath\modules" -ItemType Directory -Force | Out-Null
|
||||
New-Item "$packagePath\settings\$ModuleName" -ItemType Directory -Force | Out-Null
|
||||
|
||||
# Copy ModuleLoader
|
||||
$loaderPath = "$Platform\$Configuration\ModuleLoader.exe"
|
||||
if (Test-Path $loaderPath) {
|
||||
Copy-Item $loaderPath "$packagePath\"
|
||||
Write-Host "? Copied ModuleLoader.exe" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "? ModuleLoader.exe not found at $loaderPath" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Copy Module DLL
|
||||
$modulePath = "$Platform\$Configuration\$ModuleName.dll"
|
||||
if (Test-Path $modulePath) {
|
||||
Copy-Item $modulePath "$packagePath\modules\"
|
||||
Write-Host "? Copied $ModuleName.dll" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "? $ModuleName.dll not found at $modulePath" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Copy Settings
|
||||
$settingsPath = "$env:LOCALAPPDATA\Microsoft\PowerToys\$ModuleName\settings.json"
|
||||
if (Test-Path $settingsPath) {
|
||||
Copy-Item $settingsPath "$packagePath\settings\$ModuleName\"
|
||||
Write-Host "? Copied settings.json" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "? Settings not found at $settingsPath - creating placeholder" -ForegroundColor Yellow
|
||||
@"
|
||||
{
|
||||
"name": "$ModuleName",
|
||||
"version": "1.0"
|
||||
}
|
||||
"@ | Out-File "$packagePath\settings\$ModuleName\settings.json"
|
||||
}
|
||||
|
||||
# Create README
|
||||
@"
|
||||
PowerToys $ModuleName Testing Package
|
||||
======================================
|
||||
|
||||
Configuration: $Configuration
|
||||
Platform: $Platform
|
||||
|
||||
Setup Instructions:
|
||||
-------------------
|
||||
1. Copy the 'settings\$ModuleName' folder to:
|
||||
%LOCALAPPDATA%\Microsoft\PowerToys\
|
||||
|
||||
2. Run:
|
||||
ModuleLoader.exe modules\$ModuleName.dll
|
||||
|
||||
3. Press Ctrl+C to exit
|
||||
|
||||
Logs are written to:
|
||||
%LOCALAPPDATA%\Microsoft\PowerToys\$ModuleName\Logs\
|
||||
|
||||
For more information, see:
|
||||
https://github.com/microsoft/PowerToys/tree/main/tools/module_loader
|
||||
"@ | Out-File "$packagePath\README.txt"
|
||||
|
||||
# Create ZIP
|
||||
$zipPath = "$packageName.zip"
|
||||
Compress-Archive -Path $packagePath -DestinationPath $zipPath -Force
|
||||
Write-Host "? Created $zipPath" -ForegroundColor Green
|
||||
|
||||
# Show summary
|
||||
Write-Host "`nPackage Contents:" -ForegroundColor Cyan
|
||||
Get-ChildItem $packagePath -Recurse | ForEach-Object {
|
||||
Write-Host " $($_.FullName.Replace($packagePath, ''))"
|
||||
}
|
||||
|
||||
Write-Host "`nPackage ready: $zipPath" -ForegroundColor Green
|
||||
Write-Host "Size: $([math]::Round((Get-Item $zipPath).Length / 1KB, 2)) KB"
|
||||
```
|
||||
|
||||
**Usage**:
|
||||
```powershell
|
||||
.\CreateModulePackage.ps1 -ModuleName "CursorWrap" -Configuration Release -Platform x64
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## FAQ
|
||||
|
||||
### Q: Can I share just ModuleLoader.exe and the module DLL?
|
||||
**A**: Yes, but the recipient must have PowerToys installed (or manually create the settings file).
|
||||
|
||||
### Q: Does the tester need PowerToys installed?
|
||||
**A**: No, if you provide the complete package with settings. ModuleLoader is fully standalone.
|
||||
|
||||
### Q: What if settings.json doesn't exist?
|
||||
**A**: ModuleLoader will show an error. Either:
|
||||
1. Run PowerToys once with the module enabled to generate settings
|
||||
2. Manually create a minimal settings.json file
|
||||
3. Include a sample settings.json in your package
|
||||
|
||||
### Q: Can I test modules on a virtual machine?
|
||||
**A**: Yes! This is a great use case. Just copy the package to the VM - no PowerToys installation needed.
|
||||
|
||||
### Q: Do I need to include PDB files?
|
||||
**A**: Only for debugging. For normal testing, just the EXE and DLL are sufficient.
|
||||
|
||||
### Q: Can I distribute this to end users?
|
||||
**A**: ModuleLoader is a **development/testing tool**, not intended for end-user distribution. For production use, direct users to install PowerToys.
|
||||
|
||||
---
|
||||
|
||||
## Security Considerations
|
||||
|
||||
When sharing module DLLs:
|
||||
|
||||
1. **Verify Source**: Only share modules you built from trusted source code
|
||||
2. **Scan for Malware**: Run antivirus scans on the package before sharing
|
||||
3. **HTTPS Only**: Use secure channels (HTTPS, OneDrive, SharePoint) for distribution
|
||||
4. **Hash Verification**: Consider providing SHA256 hashes for file integrity:
|
||||
```powershell
|
||||
Get-FileHash ModuleLoader.exe -Algorithm SHA256
|
||||
Get-FileHash modules\CursorWrap.dll -Algorithm SHA256
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example Package (CursorWrap)
|
||||
|
||||
Here's what a complete CursorWrap testing package looks like:
|
||||
|
||||
```
|
||||
ModuleLoader-CursorWrap-x64-Debug.zip (220 KB)
|
||||
?
|
||||
??? ModuleLoader-CursorWrap-x64-Debug\
|
||||
??? ModuleLoader.exe (160 KB)
|
||||
??? README.txt (2 KB)
|
||||
??? modules\
|
||||
? ??? CursorWrap.dll (55 KB)
|
||||
??? settings\
|
||||
??? CursorWrap\
|
||||
??? settings.json (3 KB)
|
||||
```
|
||||
|
||||
**Total package size**: ~220 KB (compressed)
|
||||
|
||||
---
|
||||
|
||||
## Support
|
||||
|
||||
For issues with ModuleLoader, see:
|
||||
- [ModuleLoader README](./README.md)
|
||||
- [PowerToys Documentation](https://aka.ms/PowerToysOverview)
|
||||
- [PowerToys GitHub Issues](https://github.com/microsoft/PowerToys/issues)
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
ModuleLoader is part of PowerToys and is licensed under the MIT License.
|
||||
See the LICENSE file in the PowerToys repository root for details.
|
||||
80
tools/module_loader/src/ConsoleHost.cpp
Normal file
80
tools/module_loader/src/ConsoleHost.cpp
Normal file
@@ -0,0 +1,80 @@
|
||||
// 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.
|
||||
|
||||
#include "ConsoleHost.h"
|
||||
#include <iostream>
|
||||
|
||||
bool ConsoleHost::s_exitRequested = false;
|
||||
|
||||
ConsoleHost::ConsoleHost(ModuleLoader& moduleLoader, HotkeyManager& hotkeyManager)
|
||||
: m_moduleLoader(moduleLoader)
|
||||
, m_hotkeyManager(hotkeyManager)
|
||||
{
|
||||
}
|
||||
|
||||
ConsoleHost::~ConsoleHost()
|
||||
{
|
||||
}
|
||||
|
||||
BOOL WINAPI ConsoleHost::ConsoleCtrlHandler(DWORD ctrlType)
|
||||
{
|
||||
switch (ctrlType)
|
||||
{
|
||||
case CTRL_C_EVENT:
|
||||
case CTRL_BREAK_EVENT:
|
||||
case CTRL_CLOSE_EVENT:
|
||||
std::wcout << L"\nCtrl+C received, shutting down...\n";
|
||||
s_exitRequested = true;
|
||||
|
||||
// Post a quit message to break the message loop
|
||||
PostQuitMessage(0);
|
||||
return TRUE;
|
||||
|
||||
default:
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
void ConsoleHost::Run()
|
||||
{
|
||||
// Install console control handler
|
||||
if (!SetConsoleCtrlHandler(ConsoleCtrlHandler, TRUE))
|
||||
{
|
||||
std::wcerr << L"Warning: Failed to set console control handler\n";
|
||||
}
|
||||
|
||||
s_exitRequested = false;
|
||||
|
||||
// Message loop
|
||||
MSG msg;
|
||||
while (!s_exitRequested)
|
||||
{
|
||||
// Wait for a message with a timeout so we can check s_exitRequested
|
||||
DWORD result = MsgWaitForMultipleObjects(0, nullptr, FALSE, 100, QS_ALLINPUT);
|
||||
|
||||
if (result == WAIT_OBJECT_0)
|
||||
{
|
||||
// Process all pending messages
|
||||
while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE))
|
||||
{
|
||||
if (msg.message == WM_QUIT)
|
||||
{
|
||||
s_exitRequested = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (msg.message == WM_HOTKEY)
|
||||
{
|
||||
m_hotkeyManager.HandleHotkey(static_cast<int>(msg.wParam), m_moduleLoader);
|
||||
}
|
||||
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessage(&msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove console control handler
|
||||
SetConsoleCtrlHandler(ConsoleCtrlHandler, FALSE);
|
||||
}
|
||||
38
tools/module_loader/src/ConsoleHost.h
Normal file
38
tools/module_loader/src/ConsoleHost.h
Normal file
@@ -0,0 +1,38 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Windows.h>
|
||||
#include "ModuleLoader.h"
|
||||
#include "HotkeyManager.h"
|
||||
|
||||
/// <summary>
|
||||
/// Console host that runs the message loop and handles Ctrl+C
|
||||
/// </summary>
|
||||
class ConsoleHost
|
||||
{
|
||||
public:
|
||||
ConsoleHost(ModuleLoader& moduleLoader, HotkeyManager& hotkeyManager);
|
||||
~ConsoleHost();
|
||||
|
||||
// Prevent copying
|
||||
ConsoleHost(const ConsoleHost&) = delete;
|
||||
ConsoleHost& operator=(const ConsoleHost&) = delete;
|
||||
|
||||
/// <summary>
|
||||
/// Run the message loop until Ctrl+C is pressed
|
||||
/// </summary>
|
||||
void Run();
|
||||
|
||||
private:
|
||||
ModuleLoader& m_moduleLoader;
|
||||
HotkeyManager& m_hotkeyManager;
|
||||
static bool s_exitRequested;
|
||||
|
||||
/// <summary>
|
||||
/// Console control handler (for Ctrl+C)
|
||||
/// </summary>
|
||||
static BOOL WINAPI ConsoleCtrlHandler(DWORD ctrlType);
|
||||
};
|
||||
279
tools/module_loader/src/HotkeyManager.cpp
Normal file
279
tools/module_loader/src/HotkeyManager.cpp
Normal file
@@ -0,0 +1,279 @@
|
||||
// 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.
|
||||
|
||||
#include "HotkeyManager.h"
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
|
||||
HotkeyManager::HotkeyManager()
|
||||
: m_nextHotkeyId(1) // Start from 1
|
||||
, m_hotkeyExRegistered(false)
|
||||
, m_hotkeyExId(0)
|
||||
{
|
||||
}
|
||||
|
||||
HotkeyManager::~HotkeyManager()
|
||||
{
|
||||
UnregisterAll();
|
||||
}
|
||||
|
||||
UINT HotkeyManager::ConvertModifiers(bool win, bool ctrl, bool alt, bool shift) const
|
||||
{
|
||||
UINT modifiers = MOD_NOREPEAT; // Prevent repeat events
|
||||
if (win) modifiers |= MOD_WIN;
|
||||
if (ctrl) modifiers |= MOD_CONTROL;
|
||||
if (alt) modifiers |= MOD_ALT;
|
||||
if (shift) modifiers |= MOD_SHIFT;
|
||||
return modifiers;
|
||||
}
|
||||
|
||||
bool HotkeyManager::RegisterModuleHotkeys(ModuleLoader& moduleLoader)
|
||||
{
|
||||
if (!moduleLoader.IsLoaded())
|
||||
{
|
||||
std::wcerr << L"Error: Module not loaded\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool anyRegistered = false;
|
||||
|
||||
// First, try the newer GetHotkeyEx() API
|
||||
auto hotkeyEx = moduleLoader.GetHotkeyEx();
|
||||
if (hotkeyEx.has_value())
|
||||
{
|
||||
std::wcout << L"Module has HotkeyEx activation hotkey\n";
|
||||
|
||||
UINT modifiers = hotkeyEx->modifiersMask | MOD_NOREPEAT;
|
||||
UINT vkCode = hotkeyEx->vkCode;
|
||||
|
||||
if (vkCode != 0)
|
||||
{
|
||||
int hotkeyId = m_nextHotkeyId++;
|
||||
|
||||
std::wcout << L" Registering HotkeyEx: ";
|
||||
std::wcout << ModifiersToString(modifiers) << L"+" << VKeyToString(vkCode);
|
||||
|
||||
if (RegisterHotKey(nullptr, hotkeyId, modifiers, vkCode))
|
||||
{
|
||||
m_hotkeyExRegistered = true;
|
||||
m_hotkeyExId = hotkeyId;
|
||||
|
||||
std::wcout << L" - OK (Activation/Toggle)\n";
|
||||
anyRegistered = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
DWORD error = GetLastError();
|
||||
std::wcout << L" - FAILED (Error: " << error << L")\n";
|
||||
|
||||
if (error == ERROR_HOTKEY_ALREADY_REGISTERED)
|
||||
{
|
||||
std::wcout << L" (Hotkey is already registered by another application)\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also check the legacy get_hotkeys() API
|
||||
size_t hotkeyCount = moduleLoader.GetHotkeys(nullptr, 0);
|
||||
if (hotkeyCount > 0)
|
||||
{
|
||||
std::wcout << L"Module reports " << hotkeyCount << L" legacy hotkey(s)\n";
|
||||
|
||||
// Allocate buffer and get the hotkeys
|
||||
std::vector<PowertoyModuleIface::Hotkey> hotkeys(hotkeyCount);
|
||||
size_t actualCount = moduleLoader.GetHotkeys(hotkeys.data(), hotkeyCount);
|
||||
|
||||
// Register each hotkey
|
||||
for (size_t i = 0; i < actualCount; i++)
|
||||
{
|
||||
const auto& hotkey = hotkeys[i];
|
||||
|
||||
UINT modifiers = ConvertModifiers(hotkey.win, hotkey.ctrl, hotkey.alt, hotkey.shift);
|
||||
UINT vkCode = hotkey.key;
|
||||
|
||||
if (vkCode == 0)
|
||||
{
|
||||
std::wcout << L" Skipping hotkey " << i << L" (no key code)\n";
|
||||
continue;
|
||||
}
|
||||
|
||||
int hotkeyId = m_nextHotkeyId++;
|
||||
|
||||
std::wcout << L" Registering hotkey " << i << L": ";
|
||||
std::wcout << ModifiersToString(modifiers) << L"+" << VKeyToString(vkCode);
|
||||
|
||||
if (RegisterHotKey(nullptr, hotkeyId, modifiers, vkCode))
|
||||
{
|
||||
HotkeyInfo info;
|
||||
info.id = hotkeyId;
|
||||
info.moduleHotkeyId = i;
|
||||
info.modifiers = modifiers;
|
||||
info.vkCode = vkCode;
|
||||
info.description = ModifiersToString(modifiers) + L"+" + VKeyToString(vkCode);
|
||||
|
||||
m_registeredHotkeys.push_back(info);
|
||||
std::wcout << L" - OK\n";
|
||||
anyRegistered = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
DWORD error = GetLastError();
|
||||
std::wcout << L" - FAILED (Error: " << error << L")\n";
|
||||
|
||||
if (error == ERROR_HOTKEY_ALREADY_REGISTERED)
|
||||
{
|
||||
std::wcout << L" (Hotkey is already registered by another application)\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!anyRegistered && hotkeyCount == 0 && !hotkeyEx.has_value())
|
||||
{
|
||||
std::wcout << L"Module has no hotkeys\n";
|
||||
}
|
||||
|
||||
return anyRegistered;
|
||||
}
|
||||
|
||||
void HotkeyManager::UnregisterAll()
|
||||
{
|
||||
for (const auto& hotkey : m_registeredHotkeys)
|
||||
{
|
||||
UnregisterHotKey(nullptr, hotkey.id);
|
||||
}
|
||||
m_registeredHotkeys.clear();
|
||||
|
||||
if (m_hotkeyExRegistered)
|
||||
{
|
||||
UnregisterHotKey(nullptr, m_hotkeyExId);
|
||||
m_hotkeyExRegistered = false;
|
||||
m_hotkeyExId = 0;
|
||||
}
|
||||
}
|
||||
|
||||
bool HotkeyManager::HandleHotkey(int hotkeyId, ModuleLoader& moduleLoader)
|
||||
{
|
||||
// Check if it's the HotkeyEx activation hotkey
|
||||
if (m_hotkeyExRegistered && hotkeyId == m_hotkeyExId)
|
||||
{
|
||||
std::wcout << L"\nActivation hotkey triggered (HotkeyEx)\n";
|
||||
|
||||
moduleLoader.OnHotkeyEx();
|
||||
|
||||
std::wcout << L"Module toggled via activation hotkey\n";
|
||||
std::wcout << L"Module enabled: " << (moduleLoader.IsEnabled() ? L"Yes" : L"No") << L"\n\n";
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check legacy hotkeys
|
||||
for (const auto& hotkey : m_registeredHotkeys)
|
||||
{
|
||||
if (hotkey.id == hotkeyId)
|
||||
{
|
||||
std::wcout << L"\nHotkey triggered: " << hotkey.description << L"\n";
|
||||
|
||||
bool result = moduleLoader.OnHotkey(hotkey.moduleHotkeyId);
|
||||
|
||||
std::wcout << L"Module handled hotkey: " << (result ? L"Swallowed" : L"Not swallowed") << L"\n";
|
||||
std::wcout << L"Module enabled: " << (moduleLoader.IsEnabled() ? L"Yes" : L"No") << L"\n\n";
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void HotkeyManager::PrintHotkeys() const
|
||||
{
|
||||
for (const auto& hotkey : m_registeredHotkeys)
|
||||
{
|
||||
std::wcout << L" " << hotkey.description << L"\n";
|
||||
}
|
||||
}
|
||||
|
||||
std::wstring HotkeyManager::ModifiersToString(UINT modifiers) const
|
||||
{
|
||||
std::wstringstream ss;
|
||||
bool first = true;
|
||||
|
||||
if (modifiers & MOD_WIN)
|
||||
{
|
||||
if (!first) ss << L"+";
|
||||
ss << L"Win";
|
||||
first = false;
|
||||
}
|
||||
if (modifiers & MOD_CONTROL)
|
||||
{
|
||||
if (!first) ss << L"+";
|
||||
ss << L"Ctrl";
|
||||
first = false;
|
||||
}
|
||||
if (modifiers & MOD_ALT)
|
||||
{
|
||||
if (!first) ss << L"+";
|
||||
ss << L"Alt";
|
||||
first = false;
|
||||
}
|
||||
if (modifiers & MOD_SHIFT)
|
||||
{
|
||||
if (!first) ss << L"+";
|
||||
ss << L"Shift";
|
||||
first = false;
|
||||
}
|
||||
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::wstring HotkeyManager::VKeyToString(UINT vkCode) const
|
||||
{
|
||||
// Handle special keys
|
||||
switch (vkCode)
|
||||
{
|
||||
case VK_SPACE: return L"Space";
|
||||
case VK_RETURN: return L"Enter";
|
||||
case VK_ESCAPE: return L"Esc";
|
||||
case VK_TAB: return L"Tab";
|
||||
case VK_BACK: return L"Backspace";
|
||||
case VK_DELETE: return L"Del";
|
||||
case VK_INSERT: return L"Ins";
|
||||
case VK_HOME: return L"Home";
|
||||
case VK_END: return L"End";
|
||||
case VK_PRIOR: return L"PgUp";
|
||||
case VK_NEXT: return L"PgDn";
|
||||
case VK_LEFT: return L"Left";
|
||||
case VK_RIGHT: return L"Right";
|
||||
case VK_UP: return L"Up";
|
||||
case VK_DOWN: return L"Down";
|
||||
case VK_F1: return L"F1";
|
||||
case VK_F2: return L"F2";
|
||||
case VK_F3: return L"F3";
|
||||
case VK_F4: return L"F4";
|
||||
case VK_F5: return L"F5";
|
||||
case VK_F6: return L"F6";
|
||||
case VK_F7: return L"F7";
|
||||
case VK_F8: return L"F8";
|
||||
case VK_F9: return L"F9";
|
||||
case VK_F10: return L"F10";
|
||||
case VK_F11: return L"F11";
|
||||
case VK_F12: return L"F12";
|
||||
}
|
||||
|
||||
// For alphanumeric keys, use MapVirtualKey
|
||||
wchar_t keyName[256];
|
||||
UINT scanCode = MapVirtualKeyW(vkCode, MAPVK_VK_TO_VSC);
|
||||
|
||||
if (GetKeyNameTextW(scanCode << 16, keyName, 256) > 0)
|
||||
{
|
||||
return keyName;
|
||||
}
|
||||
|
||||
// Fallback to hex code
|
||||
std::wstringstream ss;
|
||||
ss << L"0x" << std::hex << vkCode;
|
||||
return ss.str();
|
||||
}
|
||||
86
tools/module_loader/src/HotkeyManager.h
Normal file
86
tools/module_loader/src/HotkeyManager.h
Normal file
@@ -0,0 +1,86 @@
|
||||
// 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.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Windows.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include "ModuleLoader.h"
|
||||
|
||||
/// <summary>
|
||||
/// Manages hotkey registration using RegisterHotKey API
|
||||
/// </summary>
|
||||
class HotkeyManager
|
||||
{
|
||||
public:
|
||||
HotkeyManager();
|
||||
~HotkeyManager();
|
||||
|
||||
// Prevent copying
|
||||
HotkeyManager(const HotkeyManager&) = delete;
|
||||
HotkeyManager& operator=(const HotkeyManager&) = delete;
|
||||
|
||||
/// <summary>
|
||||
/// Register all hotkeys from a module
|
||||
/// </summary>
|
||||
/// <param name="moduleLoader">Module to get hotkeys from</param>
|
||||
/// <returns>True if at least one hotkey was registered</returns>
|
||||
bool RegisterModuleHotkeys(ModuleLoader& moduleLoader);
|
||||
|
||||
/// <summary>
|
||||
/// Unregister all hotkeys
|
||||
/// </summary>
|
||||
void UnregisterAll();
|
||||
|
||||
/// <summary>
|
||||
/// Handle a WM_HOTKEY message
|
||||
/// </summary>
|
||||
/// <param name="hotkeyId">ID from the WM_HOTKEY message</param>
|
||||
/// <param name="moduleLoader">Module to trigger the hotkey on</param>
|
||||
/// <returns>True if the hotkey was handled</returns>
|
||||
bool HandleHotkey(int hotkeyId, ModuleLoader& moduleLoader);
|
||||
|
||||
/// <summary>
|
||||
/// Get the number of registered hotkeys
|
||||
/// </summary>
|
||||
/// <returns>Number of registered hotkeys</returns>
|
||||
size_t GetRegisteredCount() const { return m_registeredHotkeys.size() + (m_hotkeyExRegistered ? 1 : 0); }
|
||||
|
||||
/// <summary>
|
||||
/// Print registered hotkeys to console
|
||||
/// </summary>
|
||||
void PrintHotkeys() const;
|
||||
|
||||
private:
|
||||
struct HotkeyInfo
|
||||
{
|
||||
int id = 0;
|
||||
size_t moduleHotkeyId = 0;
|
||||
UINT modifiers = 0;
|
||||
UINT vkCode = 0;
|
||||
std::wstring description;
|
||||
};
|
||||
|
||||
std::vector<HotkeyInfo> m_registeredHotkeys;
|
||||
int m_nextHotkeyId;
|
||||
bool m_hotkeyExRegistered;
|
||||
int m_hotkeyExId;
|
||||
|
||||
/// <summary>
|
||||
/// Convert modifier bools to RegisterHotKey modifiers
|
||||
/// </summary>
|
||||
UINT ConvertModifiers(bool win, bool ctrl, bool alt, bool shift) const;
|
||||
|
||||
/// <summary>
|
||||
/// Get a string representation of modifiers
|
||||
/// </summary>
|
||||
std::wstring ModifiersToString(UINT modifiers) const;
|
||||
|
||||
/// <summary>
|
||||
/// Get a string representation of a virtual key code
|
||||
/// </summary>
|
||||
std::wstring VKeyToString(UINT vkCode) const;
|
||||
};
|
||||
183
tools/module_loader/src/ModuleLoader.cpp
Normal file
183
tools/module_loader/src/ModuleLoader.cpp
Normal file
@@ -0,0 +1,183 @@
|
||||
// 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.
|
||||
|
||||
#include "ModuleLoader.h"
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
|
||||
ModuleLoader::ModuleLoader()
|
||||
: m_hModule(nullptr)
|
||||
, m_module(nullptr)
|
||||
{
|
||||
}
|
||||
|
||||
ModuleLoader::~ModuleLoader()
|
||||
{
|
||||
if (m_module)
|
||||
{
|
||||
try
|
||||
{
|
||||
m_module->destroy();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// Ignore exceptions during cleanup
|
||||
}
|
||||
m_module = nullptr;
|
||||
}
|
||||
|
||||
if (m_hModule)
|
||||
{
|
||||
FreeLibrary(m_hModule);
|
||||
m_hModule = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool ModuleLoader::Load(const std::wstring& dllPath)
|
||||
{
|
||||
if (m_hModule || m_module)
|
||||
{
|
||||
std::wcerr << L"Error: Module already loaded\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
m_dllPath = dllPath;
|
||||
|
||||
// Load the DLL
|
||||
m_hModule = LoadLibraryW(dllPath.c_str());
|
||||
if (!m_hModule)
|
||||
{
|
||||
DWORD error = GetLastError();
|
||||
std::wcerr << L"Error: Failed to load DLL. Error code: " << error << L"\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the powertoy_create function
|
||||
using powertoy_create_func = PowertoyModuleIface* (*)();
|
||||
auto create_func = reinterpret_cast<powertoy_create_func>(
|
||||
GetProcAddress(m_hModule, "powertoy_create"));
|
||||
|
||||
if (!create_func)
|
||||
{
|
||||
std::wcerr << L"Error: DLL does not export 'powertoy_create' function\n";
|
||||
FreeLibrary(m_hModule);
|
||||
m_hModule = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create the module instance
|
||||
m_module = create_func();
|
||||
if (!m_module)
|
||||
{
|
||||
std::wcerr << L"Error: powertoy_create() returned nullptr\n";
|
||||
FreeLibrary(m_hModule);
|
||||
m_hModule = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::wcout << L"Module instance created successfully\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
void ModuleLoader::Enable()
|
||||
{
|
||||
if (!m_module)
|
||||
{
|
||||
throw std::runtime_error("Module not loaded");
|
||||
}
|
||||
|
||||
m_module->enable();
|
||||
}
|
||||
|
||||
void ModuleLoader::Disable()
|
||||
{
|
||||
if (!m_module)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
m_module->disable();
|
||||
}
|
||||
|
||||
bool ModuleLoader::IsEnabled() const
|
||||
{
|
||||
if (!m_module)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return m_module->is_enabled();
|
||||
}
|
||||
|
||||
void ModuleLoader::SetConfig(const std::wstring& configJson)
|
||||
{
|
||||
if (!m_module)
|
||||
{
|
||||
throw std::runtime_error("Module not loaded");
|
||||
}
|
||||
|
||||
m_module->set_config(configJson.c_str());
|
||||
}
|
||||
|
||||
std::wstring ModuleLoader::GetModuleName() const
|
||||
{
|
||||
if (!m_module)
|
||||
{
|
||||
return L"<not loaded>";
|
||||
}
|
||||
|
||||
const wchar_t* name = m_module->get_name();
|
||||
return name ? name : L"<unknown>";
|
||||
}
|
||||
|
||||
std::wstring ModuleLoader::GetModuleKey() const
|
||||
{
|
||||
if (!m_module)
|
||||
{
|
||||
return L"<not loaded>";
|
||||
}
|
||||
|
||||
const wchar_t* key = m_module->get_key();
|
||||
return key ? key : L"<unknown>";
|
||||
}
|
||||
|
||||
size_t ModuleLoader::GetHotkeys(PowertoyModuleIface::Hotkey* buffer, size_t bufferSize)
|
||||
{
|
||||
if (!m_module)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return m_module->get_hotkeys(buffer, bufferSize);
|
||||
}
|
||||
|
||||
bool ModuleLoader::OnHotkey(size_t hotkeyId)
|
||||
{
|
||||
if (!m_module)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return m_module->on_hotkey(hotkeyId);
|
||||
}
|
||||
|
||||
std::optional<PowertoyModuleIface::HotkeyEx> ModuleLoader::GetHotkeyEx()
|
||||
{
|
||||
if (!m_module)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return m_module->GetHotkeyEx();
|
||||
}
|
||||
|
||||
void ModuleLoader::OnHotkeyEx()
|
||||
{
|
||||
if (!m_module)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
m_module->OnHotkeyEx();
|
||||
}
|
||||
102
tools/module_loader/src/ModuleLoader.h
Normal file
102
tools/module_loader/src/ModuleLoader.h
Normal file
@@ -0,0 +1,102 @@
|
||||
// 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.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Windows.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <powertoy_module_interface.h>
|
||||
|
||||
/// <summary>
|
||||
/// Wrapper class for loading and managing a PowerToy module DLL
|
||||
/// </summary>
|
||||
class ModuleLoader
|
||||
{
|
||||
public:
|
||||
ModuleLoader();
|
||||
~ModuleLoader();
|
||||
|
||||
// Prevent copying
|
||||
ModuleLoader(const ModuleLoader&) = delete;
|
||||
ModuleLoader& operator=(const ModuleLoader&) = delete;
|
||||
|
||||
/// <summary>
|
||||
/// Load a PowerToy module DLL
|
||||
/// </summary>
|
||||
/// <param name="dllPath">Path to the module DLL</param>
|
||||
/// <returns>True if successful, false otherwise</returns>
|
||||
bool Load(const std::wstring& dllPath);
|
||||
|
||||
/// <summary>
|
||||
/// Enable the loaded module
|
||||
/// </summary>
|
||||
void Enable();
|
||||
|
||||
/// <summary>
|
||||
/// Disable the loaded module
|
||||
/// </summary>
|
||||
void Disable();
|
||||
|
||||
/// <summary>
|
||||
/// Check if the module is enabled
|
||||
/// </summary>
|
||||
/// <returns>True if enabled, false otherwise</returns>
|
||||
bool IsEnabled() const;
|
||||
|
||||
/// <summary>
|
||||
/// Set configuration for the module
|
||||
/// </summary>
|
||||
/// <param name="configJson">JSON configuration string</param>
|
||||
void SetConfig(const std::wstring& configJson);
|
||||
|
||||
/// <summary>
|
||||
/// Get the module's localized name
|
||||
/// </summary>
|
||||
/// <returns>Module name</returns>
|
||||
std::wstring GetModuleName() const;
|
||||
|
||||
/// <summary>
|
||||
/// Get the module's non-localized key
|
||||
/// </summary>
|
||||
/// <returns>Module key</returns>
|
||||
std::wstring GetModuleKey() const;
|
||||
|
||||
/// <summary>
|
||||
/// Get the module's hotkeys
|
||||
/// </summary>
|
||||
/// <param name="buffer">Buffer to store hotkeys</param>
|
||||
/// <param name="bufferSize">Size of the buffer</param>
|
||||
/// <returns>Number of hotkeys returned</returns>
|
||||
size_t GetHotkeys(PowertoyModuleIface::Hotkey* buffer, size_t bufferSize);
|
||||
|
||||
/// <summary>
|
||||
/// Trigger a hotkey callback on the module
|
||||
/// </summary>
|
||||
/// <param name="hotkeyId">ID of the hotkey to trigger</param>
|
||||
/// <returns>True if the key press should be swallowed</returns>
|
||||
bool OnHotkey(size_t hotkeyId);
|
||||
|
||||
/// <summary>
|
||||
/// Check if the module is loaded
|
||||
/// </summary>
|
||||
/// <returns>True if loaded, false otherwise</returns>
|
||||
bool IsLoaded() const { return m_module != nullptr; }
|
||||
|
||||
/// <summary>
|
||||
/// Get the module's activation hotkey (newer HotkeyEx API)
|
||||
/// </summary>
|
||||
/// <returns>Optional HotkeyEx struct</returns>
|
||||
std::optional<PowertoyModuleIface::HotkeyEx> GetHotkeyEx();
|
||||
|
||||
/// <summary>
|
||||
/// Trigger the newer-style hotkey callback on the module
|
||||
/// </summary>
|
||||
void OnHotkeyEx();
|
||||
|
||||
private:
|
||||
HMODULE m_hModule;
|
||||
PowertoyModuleIface* m_module;
|
||||
std::wstring m_dllPath;
|
||||
};
|
||||
182
tools/module_loader/src/SettingsLoader.cpp
Normal file
182
tools/module_loader/src/SettingsLoader.cpp
Normal file
@@ -0,0 +1,182 @@
|
||||
// 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.
|
||||
|
||||
#include "SettingsLoader.h"
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <filesystem>
|
||||
#include <Shlobj.h>
|
||||
|
||||
SettingsLoader::SettingsLoader()
|
||||
{
|
||||
}
|
||||
|
||||
SettingsLoader::~SettingsLoader()
|
||||
{
|
||||
}
|
||||
|
||||
std::wstring SettingsLoader::GetPowerToysSettingsRoot() const
|
||||
{
|
||||
// Get %LOCALAPPDATA%
|
||||
PWSTR localAppDataPath = nullptr;
|
||||
HRESULT hr = SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, nullptr, &localAppDataPath);
|
||||
|
||||
if (FAILED(hr) || !localAppDataPath)
|
||||
{
|
||||
std::wcerr << L"Error: Failed to get LOCALAPPDATA path\n";
|
||||
return L"";
|
||||
}
|
||||
|
||||
std::wstring result(localAppDataPath);
|
||||
CoTaskMemFree(localAppDataPath);
|
||||
|
||||
// Append PowerToys directory
|
||||
result += L"\\Microsoft\\PowerToys";
|
||||
return result;
|
||||
}
|
||||
|
||||
std::wstring SettingsLoader::GetSettingsPath(const std::wstring& moduleName) const
|
||||
{
|
||||
std::wstring root = GetPowerToysSettingsRoot();
|
||||
if (root.empty())
|
||||
{
|
||||
return L"";
|
||||
}
|
||||
|
||||
// Construct path: %LOCALAPPDATA%\Microsoft\PowerToys\<ModuleName>\settings.json
|
||||
std::wstring settingsPath = root + L"\\" + moduleName + L"\\settings.json";
|
||||
return settingsPath;
|
||||
}
|
||||
|
||||
std::wstring SettingsLoader::ReadFileContents(const std::wstring& filePath) const
|
||||
{
|
||||
std::wifstream file(filePath, std::ios::binary);
|
||||
if (!file.is_open())
|
||||
{
|
||||
std::wcerr << L"Error: Could not open file: " << filePath << L"\n";
|
||||
return L"";
|
||||
}
|
||||
|
||||
// Read the entire file
|
||||
std::wstringstream buffer;
|
||||
buffer << file.rdbuf();
|
||||
|
||||
return buffer.str();
|
||||
}
|
||||
|
||||
std::wstring SettingsLoader::LoadSettings(const std::wstring& moduleName, const std::wstring& moduleDllPath)
|
||||
{
|
||||
const std::wstring powerToysPrefix = L"PowerToys.";
|
||||
|
||||
// Build list of possible module name variations to try
|
||||
std::vector<std::wstring> moduleNameVariants;
|
||||
|
||||
// Try exact name first
|
||||
moduleNameVariants.push_back(moduleName);
|
||||
|
||||
// If doesn't start with "PowerToys.", try adding it
|
||||
if (moduleName.find(powerToysPrefix) != 0)
|
||||
{
|
||||
moduleNameVariants.push_back(powerToysPrefix + moduleName);
|
||||
}
|
||||
// If starts with "PowerToys.", try without it
|
||||
else
|
||||
{
|
||||
moduleNameVariants.push_back(moduleName.substr(powerToysPrefix.length()));
|
||||
}
|
||||
|
||||
// FIRST: Try same directory as the module DLL
|
||||
if (!moduleDllPath.empty())
|
||||
{
|
||||
std::filesystem::path dllPath(moduleDllPath);
|
||||
std::filesystem::path dllDirectory = dllPath.parent_path();
|
||||
|
||||
std::wstring localSettingsPath = (dllDirectory / L"settings.json").wstring();
|
||||
std::wcout << L"Trying settings path (module directory): " << localSettingsPath << L"\n";
|
||||
|
||||
if (std::filesystem::exists(localSettingsPath))
|
||||
{
|
||||
std::wstring contents = ReadFileContents(localSettingsPath);
|
||||
if (!contents.empty())
|
||||
{
|
||||
std::wcout << L"Settings file loaded from module directory (" << contents.size() << L" characters)\n";
|
||||
return contents;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SECOND: Try standard PowerToys settings locations
|
||||
for (const auto& variant : moduleNameVariants)
|
||||
{
|
||||
std::wstring settingsPath = GetSettingsPath(variant);
|
||||
|
||||
std::wcout << L"Trying settings path: " << settingsPath << L"\n";
|
||||
|
||||
// Check if file exists (case-sensitive path)
|
||||
if (std::filesystem::exists(settingsPath))
|
||||
{
|
||||
std::wstring contents = ReadFileContents(settingsPath);
|
||||
if (!contents.empty())
|
||||
{
|
||||
std::wcout << L"Settings file loaded (" << contents.size() << L" characters)\n";
|
||||
return contents;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Try case-insensitive search in the parent directory
|
||||
std::wstring root = GetPowerToysSettingsRoot();
|
||||
if (!root.empty() && std::filesystem::exists(root))
|
||||
{
|
||||
try
|
||||
{
|
||||
// Search for a directory that matches case-insensitively
|
||||
for (const auto& entry : std::filesystem::directory_iterator(root))
|
||||
{
|
||||
if (entry.is_directory())
|
||||
{
|
||||
std::wstring dirName = entry.path().filename().wstring();
|
||||
|
||||
// Case-insensitive comparison
|
||||
if (_wcsicmp(dirName.c_str(), variant.c_str()) == 0)
|
||||
{
|
||||
std::wstring actualSettingsPath = entry.path().wstring() + L"\\settings.json";
|
||||
std::wcout << L"Found case-insensitive match: " << actualSettingsPath << L"\n";
|
||||
|
||||
if (std::filesystem::exists(actualSettingsPath))
|
||||
{
|
||||
std::wstring contents = ReadFileContents(actualSettingsPath);
|
||||
if (!contents.empty())
|
||||
{
|
||||
std::wcout << L"Settings file loaded (" << contents.size() << L" characters)\n";
|
||||
return contents;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (const std::filesystem::filesystem_error& e)
|
||||
{
|
||||
std::wcerr << L"Error searching directory: " << e.what() << L"\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::wcerr << L"Error: Settings file not found in any expected location:\n";
|
||||
if (!moduleDllPath.empty())
|
||||
{
|
||||
std::filesystem::path dllPath(moduleDllPath);
|
||||
std::filesystem::path dllDirectory = dllPath.parent_path();
|
||||
std::wcerr << L" - " << (dllDirectory / L"settings.json").wstring() << L" (module directory)\n";
|
||||
}
|
||||
for (const auto& variant : moduleNameVariants)
|
||||
{
|
||||
std::wcerr << L" - " << GetSettingsPath(variant) << L"\n";
|
||||
}
|
||||
|
||||
return L"";
|
||||
}
|
||||
47
tools/module_loader/src/SettingsLoader.h
Normal file
47
tools/module_loader/src/SettingsLoader.h
Normal file
@@ -0,0 +1,47 @@
|
||||
// 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.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Windows.h>
|
||||
#include <string>
|
||||
|
||||
/// <summary>
|
||||
/// Utility class for discovering and loading PowerToy module settings
|
||||
/// </summary>
|
||||
class SettingsLoader
|
||||
{
|
||||
public:
|
||||
SettingsLoader();
|
||||
~SettingsLoader();
|
||||
|
||||
/// <summary>
|
||||
/// Load settings for a PowerToy module
|
||||
/// </summary>
|
||||
/// <param name="moduleName">Name of the module (e.g., "CursorWrap")</param>
|
||||
/// <param name="moduleDllPath">Full path to the module DLL (for checking local settings.json)</param>
|
||||
/// <returns>JSON settings string, or empty string if not found</returns>
|
||||
std::wstring LoadSettings(const std::wstring& moduleName, const std::wstring& moduleDllPath);
|
||||
|
||||
/// <summary>
|
||||
/// Get the settings file path for a module
|
||||
/// </summary>
|
||||
/// <param name="moduleName">Name of the module</param>
|
||||
/// <returns>Full path to the settings.json file</returns>
|
||||
std::wstring GetSettingsPath(const std::wstring& moduleName) const;
|
||||
|
||||
private:
|
||||
/// <summary>
|
||||
/// Get the PowerToys root settings directory
|
||||
/// </summary>
|
||||
/// <returns>Path to %LOCALAPPDATA%\Microsoft\PowerToys</returns>
|
||||
std::wstring GetPowerToysSettingsRoot() const;
|
||||
|
||||
/// <summary>
|
||||
/// Read a text file into a string
|
||||
/// </summary>
|
||||
/// <param name="filePath">Path to the file</param>
|
||||
/// <returns>File contents as a string</returns>
|
||||
std::wstring ReadFileContents(const std::wstring& filePath) const;
|
||||
};
|
||||
244
tools/module_loader/src/main.cpp
Normal file
244
tools/module_loader/src/main.cpp
Normal file
@@ -0,0 +1,244 @@
|
||||
// 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.
|
||||
|
||||
#include <Windows.h>
|
||||
#include <Tlhelp32.h>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <filesystem>
|
||||
#include "ModuleLoader.h"
|
||||
#include "SettingsLoader.h"
|
||||
#include "HotkeyManager.h"
|
||||
#include "ConsoleHost.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
void PrintUsage()
|
||||
{
|
||||
std::wcout << L"PowerToys Module Loader - Standalone utility for loading and testing PowerToy modules\n\n";
|
||||
std::wcout << L"Usage: ModuleLoader.exe <module_dll_path>\n\n";
|
||||
std::wcout << L"Arguments:\n";
|
||||
std::wcout << L" module_dll_path Path to the PowerToy module DLL (e.g., CursorWrap.dll)\n\n";
|
||||
std::wcout << L"Behavior:\n";
|
||||
std::wcout << L" - Automatically discovers settings from %%LOCALAPPDATA%%\\Microsoft\\PowerToys\\<ModuleName>\\settings.json\n";
|
||||
std::wcout << L" - Loads and enables the module\n";
|
||||
std::wcout << L" - Registers module hotkeys\n";
|
||||
std::wcout << L" - Runs until Ctrl+C is pressed\n\n";
|
||||
std::wcout << L"Examples:\n";
|
||||
std::wcout << L" ModuleLoader.exe x64\\Debug\\modules\\CursorWrap.dll\n";
|
||||
std::wcout << L" ModuleLoader.exe \"C:\\Program Files\\PowerToys\\modules\\MouseHighlighter.dll\"\n\n";
|
||||
std::wcout << L"Notes:\n";
|
||||
std::wcout << L" - Only non-UI modules are supported\n";
|
||||
std::wcout << L" - Module must have a valid settings.json file\n";
|
||||
std::wcout << L" - Debug output is written to module's log directory\n";
|
||||
}
|
||||
|
||||
std::wstring ExtractModuleName(const std::wstring& dllPath)
|
||||
{
|
||||
std::filesystem::path path(dllPath);
|
||||
std::wstring filename = path.stem().wstring();
|
||||
|
||||
// Remove "PowerToys." prefix if present (case-insensitive)
|
||||
const std::wstring powerToysPrefix = L"PowerToys.";
|
||||
if (filename.length() >= powerToysPrefix.length())
|
||||
{
|
||||
// Check if filename starts with "PowerToys." (case-insensitive)
|
||||
if (_wcsnicmp(filename.c_str(), powerToysPrefix.c_str(), powerToysPrefix.length()) == 0)
|
||||
{
|
||||
filename = filename.substr(powerToysPrefix.length());
|
||||
}
|
||||
}
|
||||
|
||||
// Common PowerToys module naming patterns
|
||||
// Remove common suffixes if present
|
||||
const std::wstring suffixes[] = { L"Module", L"ModuleInterface", L"Interface" };
|
||||
for (const auto& suffix : suffixes)
|
||||
{
|
||||
if (filename.size() > suffix.size())
|
||||
{
|
||||
size_t pos = filename.rfind(suffix);
|
||||
if (pos != std::wstring::npos && pos + suffix.size() == filename.size())
|
||||
{
|
||||
filename = filename.substr(0, pos);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filename;
|
||||
}
|
||||
}
|
||||
|
||||
int wmain(int argc, wchar_t* argv[])
|
||||
{
|
||||
std::wcout << L"PowerToys Module Loader v1.0\n";
|
||||
std::wcout << L"=============================\n\n";
|
||||
|
||||
// Check if PowerToys.exe is running
|
||||
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
||||
if (hSnapshot != INVALID_HANDLE_VALUE)
|
||||
{
|
||||
PROCESSENTRY32W pe32;
|
||||
pe32.dwSize = sizeof(PROCESSENTRY32W);
|
||||
|
||||
bool powerToysRunning = false;
|
||||
if (Process32FirstW(hSnapshot, &pe32))
|
||||
{
|
||||
do
|
||||
{
|
||||
if (_wcsicmp(pe32.szExeFile, L"PowerToys.exe") == 0)
|
||||
{
|
||||
powerToysRunning = true;
|
||||
break;
|
||||
}
|
||||
} while (Process32NextW(hSnapshot, &pe32));
|
||||
}
|
||||
CloseHandle(hSnapshot);
|
||||
|
||||
if (powerToysRunning)
|
||||
{
|
||||
// Display warning with VT100 colors
|
||||
// Yellow background (43m), black text (30m), bold (1m)
|
||||
std::wcout << L"\033[1;43;30m WARNING \033[0m PowerToys.exe is currently running!\n\n";
|
||||
|
||||
// Red text for important message
|
||||
std::wcout << L"\033[1;31m";
|
||||
std::wcout << L"Running ModuleLoader while PowerToys is active may cause conflicts:\n";
|
||||
std::wcout << L" - Duplicate hotkey registrations\n";
|
||||
std::wcout << L" - Conflicting module instances\n";
|
||||
std::wcout << L" - Unexpected behavior\n";
|
||||
std::wcout << L"\033[0m\n"; // Reset color
|
||||
|
||||
// Cyan text for recommendation
|
||||
std::wcout << L"\033[1;36m";
|
||||
std::wcout << L"RECOMMENDATION: Exit PowerToys before continuing.\n";
|
||||
std::wcout << L"\033[0m\n"; // Reset color
|
||||
|
||||
// Yellow text for prompt
|
||||
std::wcout << L"\033[1;33m";
|
||||
std::wcout << L"Do you want to continue anyway? (y/N): ";
|
||||
std::wcout << L"\033[0m"; // Reset color
|
||||
|
||||
wchar_t response = L'\0';
|
||||
std::wcin >> response;
|
||||
|
||||
if (response != L'y' && response != L'Y')
|
||||
{
|
||||
std::wcout << L"\nExiting. Please close PowerToys and try again.\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::wcout << L"\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Parse command-line arguments
|
||||
if (argc < 2)
|
||||
{
|
||||
std::wcerr << L"Error: Missing required argument <module_dll_path>\n\n";
|
||||
PrintUsage();
|
||||
return 1;
|
||||
}
|
||||
|
||||
const std::wstring dllPath = argv[1];
|
||||
|
||||
// Validate DLL exists
|
||||
if (!std::filesystem::exists(dllPath))
|
||||
{
|
||||
std::wcerr << L"Error: Module DLL not found: " << dllPath << L"\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::wcout << L"Loading module: " << dllPath << L"\n";
|
||||
|
||||
// Extract module name from DLL path
|
||||
std::wstring moduleName = ExtractModuleName(dllPath);
|
||||
std::wcout << L"Detected module name: " << moduleName << L"\n\n";
|
||||
|
||||
try
|
||||
{
|
||||
// Load settings for the module
|
||||
std::wcout << L"Loading settings...\n";
|
||||
SettingsLoader settingsLoader;
|
||||
std::wstring settingsJson = settingsLoader.LoadSettings(moduleName, dllPath);
|
||||
|
||||
if (settingsJson.empty())
|
||||
{
|
||||
std::wcerr << L"Error: Could not load settings for module '" << moduleName << L"'\n";
|
||||
std::wcerr << L"Expected location: %LOCALAPPDATA%\\Microsoft\\PowerToys\\" << moduleName << L"\\settings.json\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::wcout << L"Settings loaded successfully.\n\n";
|
||||
|
||||
// Load the module DLL
|
||||
std::wcout << L"Loading module DLL...\n";
|
||||
ModuleLoader moduleLoader;
|
||||
if (!moduleLoader.Load(dllPath))
|
||||
{
|
||||
std::wcerr << L"Error: Failed to load module DLL\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::wcout << L"Module DLL loaded successfully.\n";
|
||||
std::wcout << L"Module key: " << moduleLoader.GetModuleKey() << L"\n";
|
||||
std::wcout << L"Module name: " << moduleLoader.GetModuleName() << L"\n\n";
|
||||
|
||||
// Apply settings to the module
|
||||
std::wcout << L"Applying settings to module...\n";
|
||||
moduleLoader.SetConfig(settingsJson);
|
||||
std::wcout << L"Settings applied.\n\n";
|
||||
|
||||
// Register hotkeys
|
||||
std::wcout << L"Registering module hotkeys...\n";
|
||||
HotkeyManager hotkeyManager;
|
||||
if (!hotkeyManager.RegisterModuleHotkeys(moduleLoader))
|
||||
{
|
||||
std::wcerr << L"Warning: Failed to register some hotkeys\n";
|
||||
}
|
||||
std::wcout << L"Hotkeys registered: " << hotkeyManager.GetRegisteredCount() << L"\n\n";
|
||||
|
||||
// Enable the module
|
||||
std::wcout << L"Enabling module...\n";
|
||||
moduleLoader.Enable();
|
||||
std::wcout << L"Module enabled.\n\n";
|
||||
|
||||
// Display status
|
||||
std::wcout << L"=============================\n";
|
||||
std::wcout << L"Module is now running!\n";
|
||||
std::wcout << L"=============================\n\n";
|
||||
std::wcout << L"Module Status:\n";
|
||||
std::wcout << L" - Name: " << moduleLoader.GetModuleName() << L"\n";
|
||||
std::wcout << L" - Key: " << moduleLoader.GetModuleKey() << L"\n";
|
||||
std::wcout << L" - Enabled: " << (moduleLoader.IsEnabled() ? L"Yes" : L"No") << L"\n";
|
||||
std::wcout << L" - Hotkeys: " << hotkeyManager.GetRegisteredCount() << L" registered\n\n";
|
||||
|
||||
if (hotkeyManager.GetRegisteredCount() > 0)
|
||||
{
|
||||
std::wcout << L"Registered Hotkeys:\n";
|
||||
hotkeyManager.PrintHotkeys();
|
||||
std::wcout << L"\n";
|
||||
}
|
||||
|
||||
std::wcout << L"Press Ctrl+C to exit.\n";
|
||||
std::wcout << L"You can press the module's hotkey to toggle its functionality.\n\n";
|
||||
|
||||
// Run the message loop
|
||||
ConsoleHost consoleHost(moduleLoader, hotkeyManager);
|
||||
consoleHost.Run();
|
||||
|
||||
// Cleanup
|
||||
std::wcout << L"\nShutting down...\n";
|
||||
moduleLoader.Disable();
|
||||
hotkeyManager.UnregisterAll();
|
||||
|
||||
std::wcout << L"Module unloaded successfully.\n";
|
||||
return 0;
|
||||
}
|
||||
catch (const std::exception& ex)
|
||||
{
|
||||
std::wcerr << L"Fatal error: " << ex.what() << L"\n";
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user