mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-29 16:36:40 +01:00
Compare commits
19 Commits
leilzh/fix
...
update-tel
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0852be68f | ||
|
|
f510be4c53 | ||
|
|
4d3c223402 | ||
|
|
1ba5a258e9 | ||
|
|
8aea589b01 | ||
|
|
afd9d4cc3c | ||
|
|
bc0a760aff | ||
|
|
06afe09973 | ||
|
|
0de60445ea | ||
|
|
47d4a65223 | ||
|
|
1b72c0b969 | ||
|
|
9160c82fc2 | ||
|
|
452e0dcf51 | ||
|
|
2c9a9e9fca | ||
|
|
09c8c1d79a | ||
|
|
95c8a83f79 | ||
|
|
2830ea919c | ||
|
|
725ad21952 | ||
|
|
ebc3a139c5 |
111
.github/actions/spell-check/expect.txt
vendored
111
.github/actions/spell-check/expect.txt
vendored
@@ -2,8 +2,8 @@ AAAAs
|
||||
abcdefghjkmnpqrstuvxyz
|
||||
abgr
|
||||
ABlocked
|
||||
ABOUTBOX
|
||||
ABORTIFHUNG
|
||||
ABOUTBOX
|
||||
Abug
|
||||
Acceleratorkeys
|
||||
ACCEPTFILES
|
||||
@@ -56,6 +56,7 @@ ANull
|
||||
AOC
|
||||
aocfnapldcnfbofgmbbllojgocaelgdd
|
||||
AOklab
|
||||
aot
|
||||
APARTMENTTHREADED
|
||||
APeriod
|
||||
apicontract
|
||||
@@ -97,8 +98,8 @@ ASSOCSTR
|
||||
ASYNCWINDOWPLACEMENT
|
||||
ASYNCWINDOWPOS
|
||||
atl
|
||||
ATX
|
||||
ATRIOX
|
||||
ATX
|
||||
aumid
|
||||
authenticode
|
||||
AUTOBUDDY
|
||||
@@ -117,10 +118,10 @@ azman
|
||||
azureaiinference
|
||||
azureinference
|
||||
azureopenai
|
||||
backticks
|
||||
bbwe
|
||||
BCIE
|
||||
bck
|
||||
backticks
|
||||
BESTEFFORT
|
||||
bezelled
|
||||
bhid
|
||||
@@ -148,8 +149,8 @@ bmi
|
||||
BNumber
|
||||
BODGY
|
||||
BOklab
|
||||
Bootstrappers
|
||||
BOOTSTRAPPERINSTALLFOLDER
|
||||
Bootstrappers
|
||||
BOTTOMALIGN
|
||||
boxmodel
|
||||
BPBF
|
||||
@@ -176,17 +177,16 @@ BYPOSITION
|
||||
CALCRECT
|
||||
CALG
|
||||
callbackptr
|
||||
cabstr
|
||||
calpwstr
|
||||
caub
|
||||
Cangjie
|
||||
CANRENAME
|
||||
Carlseibert
|
||||
Canvascustomlayout
|
||||
CAPTUREBLT
|
||||
CAPTURECHANGED
|
||||
CARETBLINKING
|
||||
Carlseibert
|
||||
CAtl
|
||||
caub
|
||||
CBN
|
||||
cch
|
||||
CCHDEVICENAME
|
||||
@@ -206,11 +206,9 @@ changecursor
|
||||
CHILDACTIVATE
|
||||
CHILDWINDOW
|
||||
CHOOSEFONT
|
||||
CIBUILD
|
||||
cidl
|
||||
CIELCh
|
||||
cim
|
||||
claude
|
||||
CImage
|
||||
cla
|
||||
CLASSDC
|
||||
@@ -264,7 +262,6 @@ CONFIGW
|
||||
CONFLICTINGMODIFIERKEY
|
||||
CONFLICTINGMODIFIERSHORTCUT
|
||||
CONOUT
|
||||
coreclr
|
||||
constexpr
|
||||
contentdialog
|
||||
contentfiles
|
||||
@@ -276,6 +273,7 @@ copiedcolorrepresentation
|
||||
coppied
|
||||
copyable
|
||||
COPYPEN
|
||||
coreclr
|
||||
COREWINDOW
|
||||
Corpor
|
||||
cotaskmem
|
||||
@@ -284,18 +282,18 @@ countof
|
||||
covrun
|
||||
cpcontrols
|
||||
cph
|
||||
cppcoreguidelines
|
||||
cplusplus
|
||||
CPower
|
||||
cppcoreguidelines
|
||||
cpptools
|
||||
cppvsdbg
|
||||
cppwinrt
|
||||
createdump
|
||||
creativecommons
|
||||
CREATEPROCESS
|
||||
CREATESCHEDULEDTASK
|
||||
CREATESTRUCT
|
||||
CREATEWINDOWFAILED
|
||||
creativecommons
|
||||
CRECT
|
||||
CRH
|
||||
critsec
|
||||
@@ -331,7 +329,6 @@ CYSCREEN
|
||||
CYSMICON
|
||||
CYVIRTUALSCREEN
|
||||
Czechia
|
||||
cziplib
|
||||
Dac
|
||||
dacl
|
||||
DAffine
|
||||
@@ -355,9 +352,7 @@ Deact
|
||||
debugbreak
|
||||
decryptor
|
||||
Dedup
|
||||
dfx
|
||||
Deduplicator
|
||||
Deeplink
|
||||
DEFAULTBOOTSTRAPPERINSTALLFOLDER
|
||||
DEFAULTCOLOR
|
||||
DEFAULTFLAGS
|
||||
@@ -404,7 +399,6 @@ DISPLAYFREQUENCY
|
||||
displayname
|
||||
DISPLAYORIENTATION
|
||||
divyan
|
||||
djwsxzxb
|
||||
Dlg
|
||||
DLGFRAME
|
||||
DLGMODALFRAME
|
||||
@@ -417,7 +411,6 @@ DONTVALIDATEPATH
|
||||
dotnet
|
||||
downsampled
|
||||
downsampling
|
||||
Downsampled
|
||||
downscale
|
||||
DPICHANGED
|
||||
DPIs
|
||||
@@ -531,7 +524,6 @@ EXTRINSICPROPERTIES
|
||||
eyetracker
|
||||
FANCYZONESDRAWLAYOUTTEST
|
||||
FANCYZONESEDITOR
|
||||
FNumber
|
||||
FARPROC
|
||||
fdx
|
||||
fesf
|
||||
@@ -563,8 +555,8 @@ FIXEDSYS
|
||||
flac
|
||||
flyouts
|
||||
FMask
|
||||
foundrylocal
|
||||
fmtid
|
||||
FNumber
|
||||
FOF
|
||||
FOFX
|
||||
FOLDERID
|
||||
@@ -575,6 +567,7 @@ FORCEMINIMIZE
|
||||
FORMATDLGORD
|
||||
formatetc
|
||||
FORPARSING
|
||||
foundrylocal
|
||||
FRAMECHANGED
|
||||
frm
|
||||
FROMTOUCH
|
||||
@@ -593,13 +586,13 @@ gdi
|
||||
gdiplus
|
||||
GDIPVER
|
||||
GDISCALED
|
||||
geolocator
|
||||
GETCLIENTAREAANIMATION
|
||||
GETCURSEL
|
||||
GETDESKWALLPAPER
|
||||
GETDLGCODE
|
||||
GETDPISCALEDSIZE
|
||||
getfilesiginforedist
|
||||
geolocator
|
||||
GETHOTKEY
|
||||
GETICON
|
||||
GETLBTEXT
|
||||
@@ -610,11 +603,12 @@ GETSCREENSAVERRUNNING
|
||||
GETSECKEY
|
||||
GETSTICKYKEYS
|
||||
GETTEXTLENGTH
|
||||
GIFs
|
||||
gitmodules
|
||||
GHND
|
||||
gitmodules
|
||||
GMEM
|
||||
GNumber
|
||||
googleai
|
||||
googlegemini
|
||||
gpedit
|
||||
gpo
|
||||
GPOCA
|
||||
@@ -631,8 +625,6 @@ GValue
|
||||
gwl
|
||||
GWLP
|
||||
GWLSTYLE
|
||||
googleai
|
||||
googlegemini
|
||||
hangeul
|
||||
Hanzi
|
||||
Hardlines
|
||||
@@ -743,9 +735,7 @@ IDCANCEL
|
||||
IDD
|
||||
idk
|
||||
idl
|
||||
IIM
|
||||
idlist
|
||||
ifd
|
||||
IDOK
|
||||
IDOn
|
||||
IDR
|
||||
@@ -754,15 +744,16 @@ ietf
|
||||
IEXPLORE
|
||||
IFACEMETHOD
|
||||
IFACEMETHODIMP
|
||||
ifd
|
||||
IGNOREUNKNOWN
|
||||
IGo
|
||||
iid
|
||||
IIM
|
||||
Iindex
|
||||
Ijwhost
|
||||
ILD
|
||||
IMAGEHLP
|
||||
IMAGERESIZERCONTEXTMENU
|
||||
IPTC
|
||||
IMAGERESIZEREXT
|
||||
imageresizerinput
|
||||
imageresizersettings
|
||||
@@ -798,7 +789,6 @@ INSTALLFOLDERTOPREVIOUSINSTALLFOLDER
|
||||
INSTALLLOCATION
|
||||
INSTALLMESSAGE
|
||||
INSTALLPROPERTY
|
||||
installscopeperuser
|
||||
INSTALLSTARTMENUSHORTCUT
|
||||
INSTALLSTATE
|
||||
Inste
|
||||
@@ -811,6 +801,7 @@ invokecommand
|
||||
ipcmanager
|
||||
IPREVIEW
|
||||
ipreviewhandlervisualssetfont
|
||||
IPTC
|
||||
irow
|
||||
irprops
|
||||
isbi
|
||||
@@ -854,15 +845,14 @@ keyvault
|
||||
KILLFOCUS
|
||||
killrunner
|
||||
kmph
|
||||
ksa
|
||||
kvp
|
||||
Kybd
|
||||
LARGEICON
|
||||
lastcodeanalysissucceeded
|
||||
LASTEXITCODE
|
||||
LAYOUTRTL
|
||||
LCh
|
||||
lbl
|
||||
LCh
|
||||
lcid
|
||||
LCIDTo
|
||||
lcl
|
||||
@@ -878,10 +868,10 @@ LExit
|
||||
lhwnd
|
||||
LIBFUZZER
|
||||
LIBID
|
||||
lightswitch
|
||||
LIMITSIZE
|
||||
LIMITTEXT
|
||||
lindex
|
||||
lightswitch
|
||||
linkid
|
||||
LINKOVERLAY
|
||||
LINQTo
|
||||
@@ -892,6 +882,7 @@ LLKH
|
||||
llkhf
|
||||
LMEM
|
||||
LMENU
|
||||
lng
|
||||
LOADFROMFILE
|
||||
LOBYTE
|
||||
localappdata
|
||||
@@ -901,17 +892,14 @@ LOCATIONCHANGE
|
||||
LOCKTYPE
|
||||
LOGFONT
|
||||
LOGFONTW
|
||||
logon
|
||||
lon
|
||||
LOGMSG
|
||||
logon
|
||||
LOGPIXELSX
|
||||
LOGPIXELSY
|
||||
lng
|
||||
lon
|
||||
longdate
|
||||
LONGNAMES
|
||||
lowlevel
|
||||
lquadrant
|
||||
LOWORD
|
||||
lparam
|
||||
LPBITMAPINFOHEADER
|
||||
@@ -945,6 +933,7 @@ lpv
|
||||
LPW
|
||||
lpwcx
|
||||
lpwndpl
|
||||
lquadrant
|
||||
LReader
|
||||
LRESULT
|
||||
LSTATUS
|
||||
@@ -971,6 +960,7 @@ MAKELONG
|
||||
MAKELPARAM
|
||||
makepri
|
||||
MAKEWPARAM
|
||||
Malware
|
||||
manifestdependency
|
||||
MAPPEDTOSAMEKEY
|
||||
MAPTOSAMESHORTCUT
|
||||
@@ -993,8 +983,8 @@ MENUITEMINFO
|
||||
MENUITEMINFOW
|
||||
MERGECOPY
|
||||
MERGEPAINT
|
||||
Metadatas
|
||||
metadatamatters
|
||||
Metadatas
|
||||
metafile
|
||||
mfc
|
||||
Mgmt
|
||||
@@ -1040,9 +1030,6 @@ mousepointer
|
||||
mouseutils
|
||||
MOVESIZEEND
|
||||
MOVESIZESTART
|
||||
muxx
|
||||
muxxc
|
||||
muxxh
|
||||
MRM
|
||||
MRT
|
||||
mru
|
||||
@@ -1071,10 +1058,14 @@ msrc
|
||||
msstore
|
||||
msvcp
|
||||
MT
|
||||
mstsc
|
||||
MTND
|
||||
MULTIPLEUSE
|
||||
multizone
|
||||
muxc
|
||||
muxx
|
||||
muxxc
|
||||
muxxh
|
||||
MVPs
|
||||
mvvm
|
||||
MVVMTK
|
||||
@@ -1157,7 +1148,6 @@ nonstd
|
||||
NOOWNERZORDER
|
||||
NOPARENTNOTIFY
|
||||
NOPREFIX
|
||||
NPU
|
||||
NOREDIRECTIONBITMAP
|
||||
NOREDRAW
|
||||
NOREMOVE
|
||||
@@ -1186,6 +1176,7 @@ nowarn
|
||||
NOZORDER
|
||||
NPH
|
||||
npmjs
|
||||
NPU
|
||||
NResize
|
||||
NTAPI
|
||||
ntdll
|
||||
@@ -1210,15 +1201,17 @@ oldpath
|
||||
oldtheme
|
||||
oleaut
|
||||
OLECHAR
|
||||
ollama
|
||||
onebranch
|
||||
onnx
|
||||
OOBEUI
|
||||
openas
|
||||
opencode
|
||||
OPENFILENAME
|
||||
openrdp
|
||||
opensource
|
||||
openxmlformats
|
||||
ollama
|
||||
Olllama
|
||||
onnx
|
||||
OPTIMIZEFORINVOKE
|
||||
ORPHANEDDIALOGTITLE
|
||||
@@ -1292,6 +1285,7 @@ pguid
|
||||
phbm
|
||||
phbmp
|
||||
phicon
|
||||
Photoshop
|
||||
phwnd
|
||||
pici
|
||||
pidl
|
||||
@@ -1314,7 +1308,6 @@ pnid
|
||||
PNMLINK
|
||||
Poc
|
||||
Podcasts
|
||||
Photoshop
|
||||
POINTERID
|
||||
POINTERUPDATE
|
||||
Pokedex
|
||||
@@ -1409,10 +1402,9 @@ pwsz
|
||||
pwtd
|
||||
QDC
|
||||
qit
|
||||
QNN
|
||||
Qualcomm
|
||||
QITAB
|
||||
QITABENT
|
||||
QNN
|
||||
qoi
|
||||
Quarternary
|
||||
QUERYENDSESSION
|
||||
@@ -1422,8 +1414,8 @@ quickaccent
|
||||
QUNS
|
||||
RAII
|
||||
RAlt
|
||||
RAquadrant
|
||||
randi
|
||||
RAquadrant
|
||||
rasterization
|
||||
Rasterize
|
||||
RAWINPUTDEVICE
|
||||
@@ -1433,6 +1425,8 @@ RAWPATH
|
||||
rbhid
|
||||
rclsid
|
||||
RCZOOMIT
|
||||
remotedesktop
|
||||
rdp
|
||||
RDW
|
||||
READMODE
|
||||
READOBJECTS
|
||||
@@ -1450,9 +1444,7 @@ regfile
|
||||
REGISTERCLASSFAILED
|
||||
REGISTRYHEADER
|
||||
REGISTRYPREVIEWEXT
|
||||
registryroot
|
||||
regkey
|
||||
regroot
|
||||
regsvr
|
||||
REINSTALLMODE
|
||||
releaseblog
|
||||
@@ -1505,7 +1497,6 @@ rstringalpha
|
||||
rstringdigit
|
||||
rtb
|
||||
RTLREADING
|
||||
rtm
|
||||
runas
|
||||
rundll
|
||||
rungameid
|
||||
@@ -1562,8 +1553,8 @@ SETRULES
|
||||
SETSCREENSAVEACTIVE
|
||||
SETSTICKYKEYS
|
||||
SETTEXT
|
||||
settingscard
|
||||
SETTINGCHANGE
|
||||
settingscard
|
||||
SETTINGSCHANGED
|
||||
settingsheader
|
||||
settingshotkeycontrol
|
||||
@@ -1708,6 +1699,7 @@ stringtable
|
||||
stringval
|
||||
Strm
|
||||
strret
|
||||
STRSAFE
|
||||
stscanf
|
||||
sttngs
|
||||
Stubless
|
||||
@@ -1719,7 +1711,6 @@ sublang
|
||||
SUBMODULEUPDATE
|
||||
subresource
|
||||
Superbar
|
||||
suntimes
|
||||
sut
|
||||
svchost
|
||||
SVGIn
|
||||
@@ -1753,7 +1744,6 @@ SYSTEMMODAL
|
||||
SYSTEMTIME
|
||||
TARG
|
||||
TARGETAPPHEADER
|
||||
TARGETDIR
|
||||
targetentrypoint
|
||||
TARGETHEADER
|
||||
targetver
|
||||
@@ -1783,10 +1773,10 @@ textextractor
|
||||
TEXTINCLUDE
|
||||
tfopen
|
||||
tgz
|
||||
THEMECHANGED
|
||||
themeresources
|
||||
THH
|
||||
THICKFRAME
|
||||
THEMECHANGED
|
||||
THISCOMPONENT
|
||||
throughs
|
||||
TILEDWINDOW
|
||||
@@ -1883,7 +1873,6 @@ USEINSTALLERFORTEST
|
||||
USESHOWWINDOW
|
||||
USESTDHANDLES
|
||||
USRDLL
|
||||
utm
|
||||
UType
|
||||
uuidv
|
||||
uwp
|
||||
@@ -1956,11 +1945,11 @@ Wca
|
||||
WCE
|
||||
wcex
|
||||
WClass
|
||||
WCRAPI
|
||||
wcsicmp
|
||||
wcsncpy
|
||||
wcsnicmp
|
||||
WCT
|
||||
WCRAPI
|
||||
WDA
|
||||
wdm
|
||||
wdp
|
||||
@@ -1988,6 +1977,7 @@ WINDOWPLACEMENT
|
||||
WINDOWPOSCHANGED
|
||||
WINDOWPOSCHANGING
|
||||
WINDOWSBUILDNUMBER
|
||||
windowsml
|
||||
windowssearch
|
||||
windowssettings
|
||||
WINDOWSTYLES
|
||||
@@ -2003,9 +1993,8 @@ Winhook
|
||||
WINL
|
||||
winlogon
|
||||
winmd
|
||||
WINNT
|
||||
windowsml
|
||||
winml
|
||||
WINNT
|
||||
winres
|
||||
winrt
|
||||
winsdk
|
||||
@@ -2067,20 +2056,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 +2081,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
|
||||
|
||||
2
.github/workflows/dependency-review.yml
vendored
2
.github/workflows/dependency-review.yml
vendored
@@ -21,6 +21,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v4
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
issue: ${{ fromJson(github.event.inputs.issue_numbers) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Run GenAI Issue Deduplicator
|
||||
uses: pelikhan/action-genai-issue-dedup@v0
|
||||
|
||||
@@ -243,6 +243,10 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
<th>Event Name</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.CmdNotFound_EnableCmdNotFound</td>
|
||||
<td>Triggered when Command Not Found is enabled or disabled.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.CmdNotFoundInstallEvent</td>
|
||||
<td>Triggered when a Command Not Found is installed.</td>
|
||||
@@ -257,6 +261,62 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### Command Palette
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.CmdPal_BeginInvoke</td>
|
||||
<td>Triggered when the Command Palette is launched by the user.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.CmdPal_ColdLaunch</td>
|
||||
<td>Occurs when Command Palette starts for the first time (cold start).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.CmdPal_OpenPage</td>
|
||||
<td>Triggered when a page is opened within the Command Palette, tracking navigation depth.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.CmdPal_OpenUri</td>
|
||||
<td>Occurs when a URI is opened through the Command Palette, including whether it's a web URL.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.CmdPal_ReactivateInstance</td>
|
||||
<td>Triggered when an existing Command Palette instance is reactivated.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.CmdPal_RunCommand</td>
|
||||
<td>Logs when a command is executed through the Command Palette, including admin elevation status.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.CmdPal_RunQuery</td>
|
||||
<td>Triggered when a search query is performed, including result count and duration.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.CmdPalDismissedOnEsc</td>
|
||||
<td>Occurs when the Command Palette is dismissed by pressing the Escape key.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.CmdPalDismissedOnLostFocus</td>
|
||||
<td>Triggered when the Command Palette is dismissed due to losing focus.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.CmdPalHotkeySummoned</td>
|
||||
<td>Logs when the Command Palette is summoned via hotkey, distinguishing between global and context-specific hotkeys.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.CmdPalInvokeResult</td>
|
||||
<td>Records the result type of a Command Palette invocation.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.CmdPalProcessStarted</td>
|
||||
<td>Triggered when the Command Palette process is started.</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### Crop And Lock
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
@@ -735,6 +795,10 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
<th>Event Name</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.NewPlus_ChangedTemplateLocation</td>
|
||||
<td>Triggered when the template folder location is changed.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.NewPlus_EventCopyTemplate</td>
|
||||
<td>Triggered when an item from New+ is created (copied to the current directory).</td>
|
||||
@@ -743,6 +807,10 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
<td>Microsoft.PowerToys.NewPlus_EventCopyTemplateResult</td>
|
||||
<td>Logs the success of item creation (copying).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.NewPlus_EventOpenTemplates</td>
|
||||
<td>Triggered when the templates folder is opened.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.NewPlus_EventShowTemplateItems</td>
|
||||
<td>Triggered when the New+ context menu flyout is displayed.</td>
|
||||
@@ -928,12 +996,8 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.ShortcutGuide_EnableGuide</td>
|
||||
<td>Triggered when Shortcut Guide is enabled.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.ShortcutGuide_HideGuide</td>
|
||||
<td>Occurs when Shortcut Guide is hidden from view.</td>
|
||||
<td>Microsoft.PowerToys.ShortcutGuide_GuideSession</td>
|
||||
<td>Logs a Shortcut Guide session including duration and how it was closed.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.ShortcutGuide_Settings</td>
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
This is present due to a bug in CsWinRT where WPF projects cause the analyzer to fail.
|
||||
-->
|
||||
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
|
||||
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.4948" />
|
||||
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.6901" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.8.250907003" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK.AI" Version="1.8.37" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK.Runtime" Version="1.8.250907003" />
|
||||
|
||||
@@ -834,6 +834,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LanguageModelProvider", "sr
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.UI.ViewModels.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.UI.ViewModels.UnitTests\Microsoft.CmdPal.UI.ViewModels.UnitTests.csproj", "{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.RemoteDesktop", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.RemoteDesktop\Microsoft.CmdPal.Ext.RemoteDesktop.csproj", "{2B3FB837-23DE-629F-82C6-42304E7083C9}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests\Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests.csproj", "{DB34808A-FF91-D06E-A426-AFB5A8BD583B}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|ARM64 = Debug|ARM64
|
||||
@@ -3036,6 +3040,22 @@ Global
|
||||
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Release|x64.ActiveCfg = Release|x64
|
||||
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Release|x64.Build.0 = Release|x64
|
||||
{2B3FB837-23DE-629F-82C6-42304E7083C9}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{2B3FB837-23DE-629F-82C6-42304E7083C9}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{2B3FB837-23DE-629F-82C6-42304E7083C9}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{2B3FB837-23DE-629F-82C6-42304E7083C9}.Debug|x64.Build.0 = Debug|x64
|
||||
{2B3FB837-23DE-629F-82C6-42304E7083C9}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||
{2B3FB837-23DE-629F-82C6-42304E7083C9}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{2B3FB837-23DE-629F-82C6-42304E7083C9}.Release|x64.ActiveCfg = Release|x64
|
||||
{2B3FB837-23DE-629F-82C6-42304E7083C9}.Release|x64.Build.0 = Release|x64
|
||||
{DB34808A-FF91-D06E-A426-AFB5A8BD583B}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{DB34808A-FF91-D06E-A426-AFB5A8BD583B}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{DB34808A-FF91-D06E-A426-AFB5A8BD583B}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{DB34808A-FF91-D06E-A426-AFB5A8BD583B}.Debug|x64.Build.0 = Debug|x64
|
||||
{DB34808A-FF91-D06E-A426-AFB5A8BD583B}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||
{DB34808A-FF91-D06E-A426-AFB5A8BD583B}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{DB34808A-FF91-D06E-A426-AFB5A8BD583B}.Release|x64.ActiveCfg = Release|x64
|
||||
{DB34808A-FF91-D06E-A426-AFB5A8BD583B}.Release|x64.Build.0 = Release|x64
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -3367,6 +3387,8 @@ Global
|
||||
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
|
||||
{45354F4F-1414-45CE-B600-51CD1209FD19} = {1AFB6476-670D-4E80-A464-657E01DFF482}
|
||||
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
|
||||
{2B3FB837-23DE-629F-82C6-42304E7083C9} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2}
|
||||
{DB34808A-FF91-D06E-A426-AFB5A8BD583B} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}
|
||||
|
||||
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>
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -31,6 +31,11 @@ namespace ManagedCommon
|
||||
/// </summary>
|
||||
public static string CurrentVersionLogDirectoryPath { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path to the current log file.
|
||||
/// </summary>
|
||||
public static string CurrentLogFile { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path to the log directory for the app.
|
||||
/// </summary>
|
||||
@@ -55,7 +60,9 @@ namespace ManagedCommon
|
||||
AppLogDirectoryPath = basePath;
|
||||
CurrentVersionLogDirectoryPath = versionedPath;
|
||||
|
||||
var logFilePath = Path.Combine(versionedPath, "Log_" + DateTime.Now.ToString(@"yyyy-MM-dd", CultureInfo.InvariantCulture) + ".log");
|
||||
var logFile = "Log_" + DateTime.Now.ToString(@"yyyy-MM-dd", CultureInfo.InvariantCulture) + ".log";
|
||||
var logFilePath = Path.Combine(versionedPath, logFile);
|
||||
CurrentLogFile = logFilePath;
|
||||
|
||||
Trace.Listeners.Add(new TextWriterTraceListener(logFilePath));
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,15 +11,15 @@ public class GalleryGridPropertiesViewModel : IGridPropertiesViewModel
|
||||
{
|
||||
private readonly ExtensionObject<IGalleryGridLayout> _model;
|
||||
|
||||
public bool ShowTitle { get; private set; }
|
||||
|
||||
public bool ShowSubtitle { get; private set; }
|
||||
|
||||
public GalleryGridPropertiesViewModel(IGalleryGridLayout galleryGridLayout)
|
||||
{
|
||||
_model = new(galleryGridLayout);
|
||||
}
|
||||
|
||||
public bool ShowTitle { get; set; }
|
||||
|
||||
public bool ShowSubtitle { get; set; }
|
||||
|
||||
public void InitializeProperties()
|
||||
{
|
||||
var model = _model.Unsafe;
|
||||
|
||||
@@ -6,5 +6,9 @@ namespace Microsoft.CmdPal.Core.ViewModels;
|
||||
|
||||
public interface IGridPropertiesViewModel
|
||||
{
|
||||
bool ShowTitle { get; }
|
||||
|
||||
bool ShowSubtitle { get; }
|
||||
|
||||
void InitializeProperties();
|
||||
}
|
||||
|
||||
@@ -10,10 +10,9 @@ using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels;
|
||||
|
||||
public partial class ListItemViewModel(IListItem model, WeakReference<IPageContext> context)
|
||||
: CommandItemViewModel(new(model), context)
|
||||
public partial class ListItemViewModel : CommandItemViewModel
|
||||
{
|
||||
public new ExtensionObject<IListItem> Model { get; } = new(model);
|
||||
public new ExtensionObject<IListItem> Model { get; }
|
||||
|
||||
public List<TagViewModel>? Tags { get; set; }
|
||||
|
||||
@@ -32,6 +31,40 @@ public partial class ListItemViewModel(IListItem model, WeakReference<IPageConte
|
||||
|
||||
public string AccessibleName { get; private set; } = string.Empty;
|
||||
|
||||
public bool ShowTitle { get; private set; }
|
||||
|
||||
public bool ShowSubtitle { get; private set; }
|
||||
|
||||
public bool LayoutShowsTitle
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref field, value))
|
||||
{
|
||||
UpdateShowsTitle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool LayoutShowsSubtitle
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref field, value))
|
||||
{
|
||||
UpdateShowsSubtitle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ListItemViewModel(IListItem model, WeakReference<IPageContext> context)
|
||||
: base(new(model), context)
|
||||
{
|
||||
Model = new ExtensionObject<IListItem>(model);
|
||||
}
|
||||
|
||||
public override void InitializeProperties()
|
||||
{
|
||||
if (IsInitialized)
|
||||
@@ -93,16 +126,18 @@ public partial class ListItemViewModel(IListItem model, WeakReference<IPageConte
|
||||
|
||||
switch (propertyName)
|
||||
{
|
||||
case nameof(Tags):
|
||||
case nameof(model.Tags):
|
||||
UpdateTags(model.Tags);
|
||||
break;
|
||||
case nameof(TextToSuggest):
|
||||
this.TextToSuggest = model.TextToSuggest ?? string.Empty;
|
||||
case nameof(model.TextToSuggest):
|
||||
TextToSuggest = model.TextToSuggest ?? string.Empty;
|
||||
UpdateProperty(nameof(TextToSuggest));
|
||||
break;
|
||||
case nameof(Section):
|
||||
this.Section = model.Section ?? string.Empty;
|
||||
case nameof(model.Section):
|
||||
Section = model.Section ?? string.Empty;
|
||||
UpdateProperty(nameof(Section));
|
||||
break;
|
||||
case nameof(Details):
|
||||
case nameof(model.Details):
|
||||
var extensionDetails = model.Details;
|
||||
Details = extensionDetails is not null ? new(extensionDetails, PageContext) : null;
|
||||
Details?.InitializeProperties();
|
||||
@@ -110,16 +145,24 @@ public partial class ListItemViewModel(IListItem model, WeakReference<IPageConte
|
||||
UpdateProperty(nameof(HasDetails));
|
||||
UpdateShowDetailsCommand();
|
||||
break;
|
||||
case nameof(MoreCommands):
|
||||
case nameof(model.MoreCommands):
|
||||
UpdateProperty(nameof(MoreCommands));
|
||||
AddShowDetailsCommands();
|
||||
break;
|
||||
case nameof(Title):
|
||||
case nameof(Subtitle):
|
||||
case nameof(model.Title):
|
||||
UpdateProperty(nameof(Title));
|
||||
UpdateShowsTitle();
|
||||
UpdateAccessibleName();
|
||||
break;
|
||||
case nameof(model.Subtitle):
|
||||
UpdateProperty(nameof(Subtitle));
|
||||
UpdateShowsSubtitle();
|
||||
UpdateAccessibleName();
|
||||
break;
|
||||
default:
|
||||
UpdateProperty(propertyName);
|
||||
break;
|
||||
}
|
||||
|
||||
UpdateProperty(propertyName);
|
||||
}
|
||||
|
||||
// TODO: Do we want filters to match descriptions and other properties? Tags, etc... Yes?
|
||||
@@ -206,11 +249,32 @@ public partial class ListItemViewModel(IListItem model, WeakReference<IPageConte
|
||||
// many COM exception issues.
|
||||
Tags = [.. newTags];
|
||||
|
||||
UpdateProperty(nameof(Tags));
|
||||
UpdateProperty(nameof(HasTags));
|
||||
// We're already in UI thread, so just raise the events
|
||||
OnPropertyChanged(nameof(Tags));
|
||||
OnPropertyChanged(nameof(HasTags));
|
||||
});
|
||||
}
|
||||
|
||||
private void UpdateShowsTitle()
|
||||
{
|
||||
var oldShowTitle = ShowTitle;
|
||||
ShowTitle = LayoutShowsTitle;
|
||||
if (oldShowTitle != ShowTitle)
|
||||
{
|
||||
UpdateProperty(nameof(ShowTitle));
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateShowsSubtitle()
|
||||
{
|
||||
var oldShowSubtitle = ShowSubtitle;
|
||||
ShowSubtitle = LayoutShowsSubtitle && !string.IsNullOrWhiteSpace(Subtitle);
|
||||
if (oldShowSubtitle != ShowSubtitle)
|
||||
{
|
||||
UpdateProperty(nameof(ShowSubtitle));
|
||||
}
|
||||
}
|
||||
|
||||
protected override void UnsafeCleanup()
|
||||
{
|
||||
base.UnsafeCleanup();
|
||||
|
||||
@@ -24,8 +24,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
|
||||
// Observable from MVVM Toolkit will auto create public properties that use INotifyPropertyChange change
|
||||
// https://learn.microsoft.com/dotnet/communitytoolkit/mvvm/observablegroupedcollections for grouping support
|
||||
[ObservableProperty]
|
||||
public partial ObservableCollection<ListItemViewModel> FilteredItems { get; set; } = [];
|
||||
public ObservableCollection<ListItemViewModel> FilteredItems { get; } = [];
|
||||
|
||||
public FiltersViewModel? Filters { get; set; }
|
||||
|
||||
@@ -224,6 +223,8 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
// TODO we can probably further optimize this by also keeping a
|
||||
// HashSet of every ExtensionObject we currently have, and only
|
||||
// building new viewmodels for the ones we haven't already built.
|
||||
var showsTitle = GridProperties?.ShowTitle ?? true;
|
||||
var showsSubtitle = GridProperties?.ShowSubtitle ?? true;
|
||||
foreach (var item in newItems)
|
||||
{
|
||||
// Check for cancellation during item processing
|
||||
@@ -237,6 +238,8 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
// If an item fails to load, silently ignore it.
|
||||
if (viewModel.SafeFastInit())
|
||||
{
|
||||
viewModel.LayoutShowsTitle = showsTitle;
|
||||
viewModel.LayoutShowsSubtitle = showsSubtitle;
|
||||
newViewModels.Add(viewModel);
|
||||
}
|
||||
}
|
||||
@@ -583,6 +586,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
GridProperties = LoadGridPropertiesViewModel(model.GridProperties);
|
||||
GridProperties?.InitializeProperties();
|
||||
UpdateProperty(nameof(GridProperties));
|
||||
ApplyLayoutToItems();
|
||||
|
||||
ShowDetails = model.ShowDetails;
|
||||
UpdateProperty(nameof(ShowDetails));
|
||||
@@ -608,22 +612,15 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
model.ItemsChanged += Model_ItemsChanged;
|
||||
}
|
||||
|
||||
private IGridPropertiesViewModel? LoadGridPropertiesViewModel(IGridProperties? gridProperties)
|
||||
private static IGridPropertiesViewModel? LoadGridPropertiesViewModel(IGridProperties? gridProperties)
|
||||
{
|
||||
if (gridProperties is IMediumGridLayout mediumGridLayout)
|
||||
return gridProperties switch
|
||||
{
|
||||
return new MediumGridPropertiesViewModel(mediumGridLayout);
|
||||
}
|
||||
else if (gridProperties is IGalleryGridLayout galleryGridLayout)
|
||||
{
|
||||
return new GalleryGridPropertiesViewModel(galleryGridLayout);
|
||||
}
|
||||
else if (gridProperties is ISmallGridLayout smallGridLayout)
|
||||
{
|
||||
return new SmallGridPropertiesViewModel(smallGridLayout);
|
||||
}
|
||||
|
||||
return null;
|
||||
IMediumGridLayout mediumGridLayout => new MediumGridPropertiesViewModel(mediumGridLayout),
|
||||
IGalleryGridLayout galleryGridLayout => new GalleryGridPropertiesViewModel(galleryGridLayout),
|
||||
ISmallGridLayout smallGridLayout => new SmallGridPropertiesViewModel(smallGridLayout),
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
public void LoadMoreIfNeeded()
|
||||
@@ -685,6 +682,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
GridProperties = LoadGridPropertiesViewModel(model.GridProperties);
|
||||
GridProperties?.InitializeProperties();
|
||||
UpdateProperty(nameof(IsGridView));
|
||||
ApplyLayoutToItems();
|
||||
break;
|
||||
case nameof(ShowDetails):
|
||||
ShowDetails = model.ShowDetails;
|
||||
@@ -730,6 +728,21 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
});
|
||||
}
|
||||
|
||||
private void ApplyLayoutToItems()
|
||||
{
|
||||
lock (_listLock)
|
||||
{
|
||||
var showsTitle = GridProperties?.ShowTitle ?? true;
|
||||
var showsSubtitle = GridProperties?.ShowSubtitle ?? true;
|
||||
|
||||
foreach (var item in Items)
|
||||
{
|
||||
item.LayoutShowsTitle = showsTitle;
|
||||
item.LayoutShowsSubtitle = showsSubtitle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
|
||||
@@ -11,13 +11,15 @@ public class MediumGridPropertiesViewModel : IGridPropertiesViewModel
|
||||
{
|
||||
private readonly ExtensionObject<IMediumGridLayout> _model;
|
||||
|
||||
public bool ShowTitle { get; private set; }
|
||||
|
||||
public bool ShowSubtitle => false;
|
||||
|
||||
public MediumGridPropertiesViewModel(IMediumGridLayout mediumGridLayout)
|
||||
{
|
||||
_model = new(mediumGridLayout);
|
||||
}
|
||||
|
||||
public bool ShowTitle { get; set; }
|
||||
|
||||
public void InitializeProperties()
|
||||
{
|
||||
var model = _model.Unsafe;
|
||||
|
||||
@@ -11,6 +11,10 @@ public class SmallGridPropertiesViewModel : IGridPropertiesViewModel
|
||||
{
|
||||
private readonly ExtensionObject<ISmallGridLayout> _model;
|
||||
|
||||
public bool ShowTitle => false;
|
||||
|
||||
public bool ShowSubtitle => false;
|
||||
|
||||
public SmallGridPropertiesViewModel(ISmallGridLayout smallGridLayout)
|
||||
{
|
||||
_model = new(smallGridLayout);
|
||||
|
||||
@@ -36,6 +36,7 @@ public partial class MainListPage : DynamicListPage,
|
||||
"com.microsoft.cmdpal.builtin.websearch",
|
||||
"com.microsoft.cmdpal.builtin.windowssettings",
|
||||
"com.microsoft.cmdpal.builtin.datetime",
|
||||
"com.microsoft.cmdpal.builtin.remotedesktop",
|
||||
];
|
||||
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ using Microsoft.CmdPal.Ext.Calc;
|
||||
using Microsoft.CmdPal.Ext.ClipboardHistory;
|
||||
using Microsoft.CmdPal.Ext.Indexer;
|
||||
using Microsoft.CmdPal.Ext.Registry;
|
||||
using Microsoft.CmdPal.Ext.RemoteDesktop;
|
||||
using Microsoft.CmdPal.Ext.Shell;
|
||||
using Microsoft.CmdPal.Ext.System;
|
||||
using Microsoft.CmdPal.Ext.TimeDate;
|
||||
@@ -151,6 +152,7 @@ public partial class App : Application
|
||||
services.AddSingleton<ICommandProvider, BuiltInsCommandProvider>();
|
||||
services.AddSingleton<ICommandProvider, TimeDateCommandsProvider>();
|
||||
services.AddSingleton<ICommandProvider, SystemCommandExtensionProvider>();
|
||||
services.AddSingleton<ICommandProvider, RemoteDesktopCommandProvider>();
|
||||
|
||||
// Models
|
||||
services.AddSingleton<TopLevelCommandManager>();
|
||||
|
||||
257
src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/DevRibbon.xaml
Normal file
257
src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/DevRibbon.xaml
Normal file
@@ -0,0 +1,257 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<UserControl
|
||||
x:Class="Microsoft.CmdPal.UI.Controls.DevRibbon"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:viewModels="using:Microsoft.CmdPal.UI.ViewModels"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<UserControl.Resources>
|
||||
<DataTemplate x:Key="LogEntryTemplate" x:DataType="viewModels:LogEntryViewModel">
|
||||
<controls:SettingsExpander Description="{x:Bind Description}" Header="{x:Bind Header}">
|
||||
<controls:SettingsExpander.HeaderIcon>
|
||||
<FontIcon Glyph="{x:Bind SeverityGlyph}" />
|
||||
</controls:SettingsExpander.HeaderIcon>
|
||||
<controls:SettingsExpander.Items>
|
||||
<controls:SettingsCard HorizontalContentAlignment="Stretch" ContentAlignment="Vertical">
|
||||
<ScrollViewer
|
||||
MaxWidth="1160"
|
||||
HorizontalScrollMode="Auto"
|
||||
VerticalScrollMode="Auto">
|
||||
<TextBlock
|
||||
FontFamily="Consolas"
|
||||
FontSize="11"
|
||||
Foreground="{ThemeResource SystemControlPageTextBaseMediumBrush}"
|
||||
IsTextSelectionEnabled="True"
|
||||
Text="{x:Bind Details}"
|
||||
TextWrapping="NoWrap" />
|
||||
</ScrollViewer>
|
||||
</controls:SettingsCard>
|
||||
</controls:SettingsExpander.Items>
|
||||
</controls:SettingsExpander>
|
||||
</DataTemplate>
|
||||
|
||||
<converters:BoolToVisibilityConverter
|
||||
x:Key="InvertedBoolToVisibilityConverter"
|
||||
FalseValue="Visible"
|
||||
TrueValue="Collapsed" />
|
||||
|
||||
</UserControl.Resources>
|
||||
|
||||
<Grid>
|
||||
<Border
|
||||
x:Name="RootBorder"
|
||||
Height="26"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Top"
|
||||
Background="{ThemeResource SettingsCardBackground}"
|
||||
BorderBrush="{ThemeResource SurfaceStrokeColorFlyoutBrush}"
|
||||
BorderThickness="1,0,1,1"
|
||||
CornerRadius="0,0,8,8"
|
||||
Opacity="0.3">
|
||||
<Button
|
||||
Padding="0"
|
||||
CornerRadius="0,0,8,8"
|
||||
FontSize="11"
|
||||
PointerEntered="DevRibbonButton_PointerEntered"
|
||||
PointerExited="DevRibbonButton_PointerExited">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<StackPanel
|
||||
Padding="8,4"
|
||||
VerticalAlignment="Center"
|
||||
Background="DarkOrange"
|
||||
Orientation="Horizontal"
|
||||
Visibility="{x:Bind VisibleIfGreaterThanZero(ViewModel.WarningCount), Mode=OneWay}">
|
||||
<FontIcon
|
||||
Margin="0,0,8,0"
|
||||
VerticalAlignment="Center"
|
||||
FontFamily="{ThemeResource SymbolThemeFontFamily}"
|
||||
FontSize="12"
|
||||
Glyph="" />
|
||||
<TextBlock VerticalAlignment="Center">
|
||||
<Run Text="{x:Bind ViewModel.WarningCount, Mode=OneWay}" />
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
Padding="8,4"
|
||||
VerticalAlignment="Center"
|
||||
Background="Maroon"
|
||||
Orientation="Horizontal"
|
||||
Visibility="{x:Bind VisibleIfGreaterThanZero(ViewModel.ErrorCount), Mode=OneWay}">
|
||||
<FontIcon
|
||||
Margin="0,0,8,0"
|
||||
VerticalAlignment="Center"
|
||||
FontFamily="{ThemeResource SymbolThemeFontFamily}"
|
||||
FontSize="12"
|
||||
Glyph="" />
|
||||
<TextBlock VerticalAlignment="Center">
|
||||
<Run Text="{x:Bind ViewModel.ErrorCount, Mode=OneWay}" />
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
<Border Padding="8,4">
|
||||
<Border.Background>
|
||||
<SolidColorBrush Color="{x:Bind ViewModel.TagColor}" />
|
||||
</Border.Background>
|
||||
<TextBlock Padding="4" VerticalAlignment="Center">
|
||||
<Run Text="{x:Bind ViewModel.Tag}" />
|
||||
</TextBlock>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<Button.Flyout>
|
||||
<Flyout
|
||||
Placement="Bottom"
|
||||
ShouldConstrainToRootBounds="False"
|
||||
SystemBackdrop="{ThemeResource AcrylicBackgroundFillColorDefaultBackdrop}">
|
||||
<Flyout.FlyoutPresenterStyle>
|
||||
<Style BasedOn="{StaticResource DefaultFlyoutPresenterStyle}" TargetType="FlyoutPresenter">
|
||||
<Setter Property="MinWidth" Value="600" />
|
||||
<Setter Property="MaxWidth" Value="1200" />
|
||||
<Setter Property="Padding" Value="0" />
|
||||
</Style>
|
||||
</Flyout.FlyoutPresenterStyle>
|
||||
|
||||
<Grid x:Name="FlyoutContent">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<StackPanel Padding="16" Spacing="8">
|
||||
|
||||
<!-- Logs section -->
|
||||
<TextBlock
|
||||
Margin="1,0,0,6"
|
||||
Style="{ThemeResource SettingsSectionHeaderTextBlockStyle}"
|
||||
Text="Logs" />
|
||||
<ItemsControl ItemTemplate="{StaticResource LogEntryTemplate}" ItemsSource="{x:Bind ViewModel.LatestLogs, Mode=OneWay}" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button Command="{x:Bind ViewModel.OpenLogFileCommand}" Content="Open Log File" />
|
||||
<Button Command="{x:Bind ViewModel.OpenLogFolderCommand}" Content="Open Log Folder" />
|
||||
<Button Command="{x:Bind ViewModel.ResetErrorCountersCommand}" Content="Clear Counters" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Build info section -->
|
||||
<TextBlock Style="{ThemeResource SettingsSectionHeaderTextBlockStyle}" Text="Build Info" />
|
||||
<Border
|
||||
Padding="16"
|
||||
Background="{ThemeResource SettingsCardBackground}"
|
||||
BorderBrush="{ThemeResource SettingsCardBorderBrush}"
|
||||
BorderThickness="1">
|
||||
<Grid ColumnSpacing="8">
|
||||
<Grid.Resources>
|
||||
<Style
|
||||
x:Key="KeyTextBlockStyle"
|
||||
BasedOn="{StaticResource CaptionTextBlockStyle}"
|
||||
TargetType="TextBlock">
|
||||
<Setter Property="IsTextSelectionEnabled" Value="True" />
|
||||
<Setter Property="Foreground" Value="{ThemeResource TextFillColorSecondaryBrush}" />
|
||||
</Style>
|
||||
<Style
|
||||
x:Key="ValueTextBlockStyle"
|
||||
BasedOn="{StaticResource CaptionTextBlockStyle}"
|
||||
TargetType="TextBlock">
|
||||
<Setter Property="IsTextSelectionEnabled" Value="True" />
|
||||
<Setter Property="TextAlignment" Value="Right" />
|
||||
</Style>
|
||||
</Grid.Resources>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
Style="{StaticResource KeyTextBlockStyle}"
|
||||
Text="Configuration:" />
|
||||
<TextBlock
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Style="{StaticResource ValueTextBlockStyle}"
|
||||
Text="{x:Bind ViewModel.BuildConfiguration, Mode=OneWay}" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
Style="{StaticResource KeyTextBlockStyle}"
|
||||
Text="AOT:" />
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Style="{StaticResource ValueTextBlockStyle}"
|
||||
Text="{x:Bind ViewModel.IsAot, Mode=OneWay}" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="2"
|
||||
Grid.Column="0"
|
||||
Style="{StaticResource KeyTextBlockStyle}"
|
||||
Text="Trimmed:" />
|
||||
<TextBlock
|
||||
Grid.Row="2"
|
||||
Grid.Column="1"
|
||||
Style="{StaticResource ValueTextBlockStyle}"
|
||||
Text="{x:Bind ViewModel.IsPublishTrimmed, Mode=OneWay}" />
|
||||
|
||||
</Grid>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Footer -->
|
||||
<Border
|
||||
Grid.Row="1"
|
||||
Padding="16"
|
||||
Background="{ThemeResource SettingsCardBackground}"
|
||||
BorderBrush="{ThemeResource SettingsCardBorderBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{ThemeResource ControlCornerRadius}"
|
||||
Visibility="{x:Bind ViewModel.IsAotReleaseConfiguration, Mode=OneWay, Converter={StaticResource InvertedBoolToVisibilityConverter}}">
|
||||
<TextBlock Text="Warning: Test in Release/AOT configuration to verify everything works." TextWrapping="Wrap" />
|
||||
</Border>
|
||||
|
||||
</Grid>
|
||||
</Flyout>
|
||||
</Button.Flyout>
|
||||
</Button>
|
||||
</Border>
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="PointerOver">
|
||||
<Storyboard>
|
||||
<DoubleAnimation
|
||||
Storyboard.TargetName="RootBorder"
|
||||
Storyboard.TargetProperty="Opacity"
|
||||
To="1.0"
|
||||
Duration="0:0:0.1" />
|
||||
</Storyboard>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
<VisualStateGroup x:Name="SeverityStates">
|
||||
<VisualState x:Name="NoLog" />
|
||||
<VisualState x:Name="WarningLog">
|
||||
<Storyboard>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SeverityIcon" Storyboard.TargetProperty="Glyph">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
</VisualState>
|
||||
<VisualState x:Name="ErrorLog">
|
||||
<Storyboard>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SeverityIcon" Storyboard.TargetProperty="Glyph">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls;
|
||||
|
||||
internal sealed partial class DevRibbon : UserControl
|
||||
{
|
||||
public ViewModels.DevRibbonViewModel ViewModel { get; }
|
||||
|
||||
public DevRibbon()
|
||||
{
|
||||
InitializeComponent();
|
||||
ViewModel = new ViewModels.DevRibbonViewModel();
|
||||
|
||||
if (FlyoutContent != null)
|
||||
{
|
||||
FlyoutContent.DataContext = ViewModel;
|
||||
}
|
||||
}
|
||||
|
||||
private void DevRibbonButton_PointerEntered(object sender, PointerRoutedEventArgs e)
|
||||
{
|
||||
VisualStateManager.GoToState(this, "PointerOver", true);
|
||||
}
|
||||
|
||||
private void DevRibbonButton_PointerExited(object sender, PointerRoutedEventArgs e)
|
||||
{
|
||||
VisualStateManager.GoToState(this, "Normal", true);
|
||||
}
|
||||
|
||||
private Visibility VisibleIfGreaterThanZero(int value)
|
||||
{
|
||||
return value > 0 ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Microsoft.CmdPal.UI;
|
||||
|
||||
internal sealed partial class GridItemContainerStyleSelector : StyleSelector
|
||||
{
|
||||
public IGridPropertiesViewModel? GridProperties { get; set; }
|
||||
|
||||
public Style? Small { get; set; }
|
||||
|
||||
public Style? Medium { get; set; }
|
||||
|
||||
public Style? Gallery { get; set; }
|
||||
|
||||
protected override Style? SelectStyleCore(object item, DependencyObject container)
|
||||
{
|
||||
return GridProperties switch
|
||||
{
|
||||
SmallGridPropertiesViewModel => Small,
|
||||
MediumGridPropertiesViewModel => Medium,
|
||||
GalleryGridPropertiesViewModel => Gallery,
|
||||
_ => Medium,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -20,21 +20,12 @@ internal sealed partial class GridItemTemplateSelector : DataTemplateSelector
|
||||
|
||||
protected override DataTemplate? SelectTemplateCore(object item, DependencyObject dependencyObject)
|
||||
{
|
||||
DataTemplate? dataTemplate = Medium;
|
||||
|
||||
if (GridProperties is SmallGridPropertiesViewModel)
|
||||
return GridProperties switch
|
||||
{
|
||||
dataTemplate = Small;
|
||||
}
|
||||
else if (GridProperties is MediumGridPropertiesViewModel)
|
||||
{
|
||||
dataTemplate = Medium;
|
||||
}
|
||||
else if (GridProperties is GalleryGridPropertiesViewModel)
|
||||
{
|
||||
dataTemplate = Gallery;
|
||||
}
|
||||
|
||||
return dataTemplate;
|
||||
SmallGridPropertiesViewModel => Small,
|
||||
MediumGridPropertiesViewModel => Medium,
|
||||
GalleryGridPropertiesViewModel => Gallery,
|
||||
_ => Medium,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,33 +5,151 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:cmdpalUI="using:Microsoft.CmdPal.UI"
|
||||
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
|
||||
xmlns:coreViewModels="using:Microsoft.CmdPal.Core.ViewModels"
|
||||
xmlns:cpcontrols="using:Microsoft.CmdPal.UI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:help="using:Microsoft.CmdPal.UI.Helpers"
|
||||
xmlns:local="using:Microsoft.CmdPal.UI"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ui="using:CommunityToolkit.WinUI"
|
||||
xmlns:viewModels="using:Microsoft.CmdPal.UI.ViewModels"
|
||||
x:Name="PageRoot"
|
||||
Background="Transparent"
|
||||
DataContext="{x:Bind ViewModel, Mode=OneWay}"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Page.Resources>
|
||||
<!-- TODO: Figure out what we want to do here for filtering/grouping and where -->
|
||||
<!-- https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.data.collectionviewsource -->
|
||||
<!--<CollectionViewSource
|
||||
x:Name="ItemsCVS"
|
||||
IsSourceGrouped="True"
|
||||
Source="{x:Bind ViewModel.Items, Mode=OneWay}" />-->
|
||||
|
||||
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
|
||||
<converters:StringVisibilityConverter
|
||||
x:Key="StringVisibilityConverter"
|
||||
EmptyValue="Collapsed"
|
||||
NotEmptyValue="Visible" />
|
||||
<!--
|
||||
GridViewItemCornerRadius is the corner radius defined in GridView template; make
|
||||
it bigger to match the radii of the gallery
|
||||
-->
|
||||
<CornerRadius x:Key="GalleryGridViewItemContainerCornerRadius">6</CornerRadius>
|
||||
<CornerRadius x:Key="IconGridViewItemContainerCornerRadius">4</CornerRadius>
|
||||
<CornerRadius x:Key="GalleryGridViewItemRadius">4</CornerRadius>
|
||||
<CornerRadius x:Key="SmallGridViewItemCornerRadius">8</CornerRadius>
|
||||
<CornerRadius x:Key="MediumGridViewItemCornerRadius">8</CornerRadius>
|
||||
|
||||
<Style x:Key="IconGridViewItemStyle" TargetType="GridViewItem">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="GridViewItem">
|
||||
<ListViewItemPresenter
|
||||
x:Name="Root"
|
||||
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
CheckBoxBorderBrush="{ThemeResource GridViewItemCheckBoxBorderBrush}"
|
||||
CheckBoxBrush="{ThemeResource GridViewItemCheckBoxBrush}"
|
||||
CheckBoxCornerRadius="{ThemeResource GridViewItemCheckBoxCornerRadius}"
|
||||
CheckBoxDisabledBorderBrush="{ThemeResource GridViewItemCheckBoxDisabledBorderBrush}"
|
||||
CheckBoxDisabledBrush="{ThemeResource GridViewItemCheckBoxDisabledBrush}"
|
||||
CheckBoxPointerOverBorderBrush="{ThemeResource GridViewItemCheckBoxPointerOverBorderBrush}"
|
||||
CheckBoxPointerOverBrush="{ThemeResource GridViewItemCheckBoxPointerOverBrush}"
|
||||
CheckBoxPressedBorderBrush="{ThemeResource GridViewItemCheckBoxPressedBorderBrush}"
|
||||
CheckBoxPressedBrush="{ThemeResource GridViewItemCheckBoxPressedBrush}"
|
||||
CheckBoxSelectedBrush="{ThemeResource GridViewItemCheckBoxSelectedBrush}"
|
||||
CheckBoxSelectedDisabledBrush="{ThemeResource GridViewItemCheckBoxSelectedDisabledBrush}"
|
||||
CheckBoxSelectedPointerOverBrush="{ThemeResource GridViewItemCheckBoxSelectedPointerOverBrush}"
|
||||
CheckBoxSelectedPressedBrush="{ThemeResource GridViewItemCheckBoxSelectedPressedBrush}"
|
||||
CheckBrush="{ThemeResource GridViewItemCheckBrush}"
|
||||
CheckDisabledBrush="{ThemeResource GridViewItemCheckDisabledBrush}"
|
||||
CheckMode="{ThemeResource GridViewItemCheckMode}"
|
||||
CheckPressedBrush="{ThemeResource GridViewItemCheckPressedBrush}"
|
||||
ContentMargin="{TemplateBinding Padding}"
|
||||
ContentTransitions="{TemplateBinding ContentTransitions}"
|
||||
Control.IsTemplateFocusTarget="True"
|
||||
CornerRadius="{StaticResource IconGridViewItemContainerCornerRadius}"
|
||||
DisabledOpacity="{ThemeResource ListViewItemDisabledThemeOpacity}"
|
||||
DragBackground="{ThemeResource GridViewItemDragBackground}"
|
||||
DragForeground="{ThemeResource GridViewItemDragForeground}"
|
||||
DragOpacity="{ThemeResource ListViewItemDragThemeOpacity}"
|
||||
FocusBorderBrush="{ThemeResource GridViewItemFocusBorderBrush}"
|
||||
FocusVisualMargin="{TemplateBinding FocusVisualMargin}"
|
||||
FocusVisualPrimaryBrush="{TemplateBinding FocusVisualPrimaryBrush}"
|
||||
FocusVisualPrimaryThickness="{TemplateBinding FocusVisualPrimaryThickness}"
|
||||
FocusVisualSecondaryBrush="{TemplateBinding FocusVisualSecondaryBrush}"
|
||||
FocusVisualSecondaryThickness="{TemplateBinding FocusVisualSecondaryThickness}"
|
||||
PlaceholderBackground="{ThemeResource GridViewItemPlaceholderBackground}"
|
||||
PointerOverBackground="{ThemeResource GridViewItemBackgroundPointerOver}"
|
||||
PointerOverBorderBrush="{ThemeResource GridViewItemPointerOverBorderBrush}"
|
||||
PointerOverForeground="{ThemeResource GridViewItemForegroundPointerOver}"
|
||||
PressedBackground="{ThemeResource GridViewItemBackgroundPressed}"
|
||||
ReorderHintOffset="{ThemeResource GridViewItemReorderHintThemeOffset}"
|
||||
SelectedBackground="{ThemeResource GridViewItemBackgroundSelected}"
|
||||
SelectedBorderBrush="{ThemeResource GridViewItemSelectedBorderBrush}"
|
||||
SelectedBorderThickness="{ThemeResource GridViewItemSelectedBorderThickness}"
|
||||
SelectedDisabledBackground="{ThemeResource GridViewItemBackgroundSelectedDisabled}"
|
||||
SelectedDisabledBorderBrush="{ThemeResource GridViewItemSelectedDisabledBorderBrush}"
|
||||
SelectedForeground="{ThemeResource GridViewItemForegroundSelected}"
|
||||
SelectedInnerBorderBrush="{ThemeResource GridViewItemSelectedInnerBorderBrush}"
|
||||
SelectedPointerOverBackground="{ThemeResource GridViewItemBackgroundSelectedPointerOver}"
|
||||
SelectedPointerOverBorderBrush="{ThemeResource GridViewItemSelectedPointerOverBorderBrush}"
|
||||
SelectedPressedBackground="{ThemeResource GridViewItemBackgroundSelectedPressed}"
|
||||
SelectedPressedBorderBrush="{ThemeResource GridViewItemSelectedPressedBorderBrush}"
|
||||
SelectionCheckMarkVisualEnabled="{ThemeResource GridViewItemSelectionCheckMarkVisualEnabled}" />
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="GalleryGridViewItemStyle" TargetType="GridViewItem">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="GridViewItem">
|
||||
<ListViewItemPresenter
|
||||
x:Name="Root"
|
||||
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
CheckBoxBorderBrush="{ThemeResource GridViewItemCheckBoxBorderBrush}"
|
||||
CheckBoxBrush="{ThemeResource GridViewItemCheckBoxBrush}"
|
||||
CheckBoxCornerRadius="{ThemeResource GridViewItemCheckBoxCornerRadius}"
|
||||
CheckBoxDisabledBorderBrush="{ThemeResource GridViewItemCheckBoxDisabledBorderBrush}"
|
||||
CheckBoxDisabledBrush="{ThemeResource GridViewItemCheckBoxDisabledBrush}"
|
||||
CheckBoxPointerOverBorderBrush="{ThemeResource GridViewItemCheckBoxPointerOverBorderBrush}"
|
||||
CheckBoxPointerOverBrush="{ThemeResource GridViewItemCheckBoxPointerOverBrush}"
|
||||
CheckBoxPressedBorderBrush="{ThemeResource GridViewItemCheckBoxPressedBorderBrush}"
|
||||
CheckBoxPressedBrush="{ThemeResource GridViewItemCheckBoxPressedBrush}"
|
||||
CheckBoxSelectedBrush="{ThemeResource GridViewItemCheckBoxSelectedBrush}"
|
||||
CheckBoxSelectedDisabledBrush="{ThemeResource GridViewItemCheckBoxSelectedDisabledBrush}"
|
||||
CheckBoxSelectedPointerOverBrush="{ThemeResource GridViewItemCheckBoxSelectedPointerOverBrush}"
|
||||
CheckBoxSelectedPressedBrush="{ThemeResource GridViewItemCheckBoxSelectedPressedBrush}"
|
||||
CheckBrush="{ThemeResource GridViewItemCheckBrush}"
|
||||
CheckDisabledBrush="{ThemeResource GridViewItemCheckDisabledBrush}"
|
||||
CheckMode="{ThemeResource GridViewItemCheckMode}"
|
||||
CheckPressedBrush="{ThemeResource GridViewItemCheckPressedBrush}"
|
||||
ContentMargin="{TemplateBinding Padding}"
|
||||
ContentTransitions="{TemplateBinding ContentTransitions}"
|
||||
Control.IsTemplateFocusTarget="True"
|
||||
CornerRadius="{StaticResource GalleryGridViewItemContainerCornerRadius}"
|
||||
DisabledOpacity="{ThemeResource ListViewItemDisabledThemeOpacity}"
|
||||
DragBackground="{ThemeResource GridViewItemDragBackground}"
|
||||
DragForeground="{ThemeResource GridViewItemDragForeground}"
|
||||
DragOpacity="{ThemeResource ListViewItemDragThemeOpacity}"
|
||||
FocusBorderBrush="{ThemeResource GridViewItemFocusBorderBrush}"
|
||||
FocusVisualMargin="{TemplateBinding FocusVisualMargin}"
|
||||
FocusVisualPrimaryBrush="{TemplateBinding FocusVisualPrimaryBrush}"
|
||||
FocusVisualPrimaryThickness="{TemplateBinding FocusVisualPrimaryThickness}"
|
||||
FocusVisualSecondaryBrush="{TemplateBinding FocusVisualSecondaryBrush}"
|
||||
FocusVisualSecondaryThickness="{TemplateBinding FocusVisualSecondaryThickness}"
|
||||
PlaceholderBackground="{ThemeResource GridViewItemPlaceholderBackground}"
|
||||
PointerOverBackground="{ThemeResource GridViewItemBackgroundPointerOver}"
|
||||
PointerOverBorderBrush="{ThemeResource GridViewItemPointerOverBorderBrush}"
|
||||
PointerOverForeground="{ThemeResource GridViewItemForegroundPointerOver}"
|
||||
PressedBackground="{ThemeResource GridViewItemBackgroundPressed}"
|
||||
ReorderHintOffset="{ThemeResource GridViewItemReorderHintThemeOffset}"
|
||||
SelectedBackground="{ThemeResource GridViewItemBackgroundSelected}"
|
||||
SelectedBorderBrush="{ThemeResource GridViewItemSelectedBorderBrush}"
|
||||
SelectedBorderThickness="{ThemeResource GridViewItemSelectedBorderThickness}"
|
||||
SelectedDisabledBackground="{ThemeResource GridViewItemBackgroundSelectedDisabled}"
|
||||
SelectedDisabledBorderBrush="{ThemeResource GridViewItemSelectedDisabledBorderBrush}"
|
||||
SelectedForeground="{ThemeResource GridViewItemForegroundSelected}"
|
||||
SelectedInnerBorderBrush="{ThemeResource GridViewItemSelectedInnerBorderBrush}"
|
||||
SelectedPointerOverBackground="{ThemeResource GridViewItemBackgroundSelectedPointerOver}"
|
||||
SelectedPointerOverBorderBrush="{ThemeResource GridViewItemSelectedPointerOverBorderBrush}"
|
||||
SelectedPressedBackground="{ThemeResource GridViewItemBackgroundSelectedPressed}"
|
||||
SelectedPressedBorderBrush="{ThemeResource GridViewItemSelectedPressedBorderBrush}"
|
||||
SelectionCheckMarkVisualEnabled="{ThemeResource GridViewItemSelectionCheckMarkVisualEnabled}" />
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<DataTemplate x:Key="TagTemplate" x:DataType="coreViewModels:TagViewModel">
|
||||
<cpcontrols:Tag
|
||||
@@ -48,10 +166,17 @@
|
||||
x:Key="GridItemTemplateSelector"
|
||||
x:DataType="coreViewModels:ListItemViewModel"
|
||||
Gallery="{StaticResource GalleryGridItemViewModelTemplate}"
|
||||
GridProperties="{x:Bind ViewModel.GridProperties}"
|
||||
GridProperties="{x:Bind ViewModel.GridProperties, Mode=OneWay}"
|
||||
Medium="{StaticResource MediumGridItemViewModelTemplate}"
|
||||
Small="{StaticResource SmallGridItemViewModelTemplate}" />
|
||||
|
||||
<cmdpalUI:GridItemContainerStyleSelector
|
||||
x:Key="GridItemContainerStyleSelector"
|
||||
Gallery="{StaticResource GalleryGridViewItemStyle}"
|
||||
GridProperties="{x:Bind ViewModel.GridProperties, Mode=OneWay}"
|
||||
Medium="{StaticResource IconGridViewItemStyle}"
|
||||
Small="{StaticResource IconGridViewItemStyle}" />
|
||||
|
||||
<!-- https://learn.microsoft.com/windows/apps/design/controls/itemsview#specify-the-look-of-the-items -->
|
||||
<DataTemplate x:Key="ListItemViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel">
|
||||
<Grid
|
||||
@@ -94,7 +219,7 @@
|
||||
Text="{x:Bind Subtitle, Mode=OneWay}"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
TextWrapping="NoWrap"
|
||||
Visibility="{x:Bind Subtitle, Mode=OneWay, Converter={StaticResource StringVisibilityConverter}}" />
|
||||
Visibility="{x:Bind ShowSubtitle, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
|
||||
<ItemsControl
|
||||
@@ -124,11 +249,11 @@
|
||||
Padding="8,16"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.Name="{x:Bind Title}"
|
||||
AutomationProperties.Name="{x:Bind Title, Mode=OneWay}"
|
||||
BorderThickness="0"
|
||||
CornerRadius="8"
|
||||
CornerRadius="{StaticResource SmallGridViewItemCornerRadius}"
|
||||
Orientation="Vertical"
|
||||
ToolTipService.ToolTip="{x:Bind Title}">
|
||||
ToolTipService.ToolTip="{x:Bind Title, Mode=OneWay}">
|
||||
|
||||
<cpcontrols:IconBox
|
||||
x:Name="GridIconBorder"
|
||||
@@ -145,23 +270,22 @@
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="MediumGridItemViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel">
|
||||
<StackPanel
|
||||
<Grid
|
||||
Width="100"
|
||||
Height="100"
|
||||
Padding="8,16"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.Name="{x:Bind Title}"
|
||||
BorderThickness="0"
|
||||
CornerRadius="8"
|
||||
Orientation="Vertical"
|
||||
ToolTipService.ToolTip="{x:Bind Title}">
|
||||
|
||||
Padding="8"
|
||||
AutomationProperties.Name="{x:Bind Title, Mode=OneWay}"
|
||||
CornerRadius="{StaticResource MediumGridViewItemCornerRadius}"
|
||||
ToolTipService.ToolTip="{x:Bind Title, Mode=OneWay}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<cpcontrols:IconBox
|
||||
x:Name="GridIconBorder"
|
||||
Grid.Row="0"
|
||||
Width="36"
|
||||
Height="36"
|
||||
Margin="0"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
CharacterSpacing="12"
|
||||
@@ -169,21 +293,20 @@
|
||||
Foreground="{ThemeResource TextFillColorPrimary}"
|
||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
|
||||
|
||||
<TextBlock
|
||||
x:Name="TitleTextBlock"
|
||||
MaxHeight="40"
|
||||
Margin="0,8,0,4"
|
||||
Grid.Row="1"
|
||||
Height="32"
|
||||
Margin="0,8,0,0"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
CharacterSpacing="12"
|
||||
FontSize="12"
|
||||
Text="{x:Bind Title}"
|
||||
Text="{x:Bind Title, Mode=OneWay}"
|
||||
TextAlignment="Center"
|
||||
TextTrimming="WordEllipsis"
|
||||
TextWrapping="Wrap"
|
||||
Visibility="{Binding ElementName=PageRoot, Path=DataContext.GridProperties.ShowTitle, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||
</StackPanel>
|
||||
Visibility="{x:Bind LayoutShowsTitle, Mode=OneWay}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="GalleryGridItemViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel">
|
||||
@@ -193,11 +316,11 @@
|
||||
Padding="0"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.Name="{x:Bind Title}"
|
||||
AutomationProperties.Name="{x:Bind Title, Mode=OneWay}"
|
||||
BorderThickness="0"
|
||||
CornerRadius="4"
|
||||
CornerRadius="{StaticResource GalleryGridViewItemRadius}"
|
||||
Orientation="Vertical"
|
||||
ToolTipService.ToolTip="{x:Bind Title}">
|
||||
ToolTipService.ToolTip="{x:Bind Title, Mode=OneWay}">
|
||||
|
||||
<Grid
|
||||
Width="160"
|
||||
@@ -205,12 +328,8 @@
|
||||
Margin="0"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
CornerRadius="4">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
</Grid.RowDefinitions>
|
||||
CornerRadius="{StaticResource GalleryGridViewItemRadius}">
|
||||
<Viewbox
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Center"
|
||||
Stretch="UniformToFill"
|
||||
StretchDirection="Both">
|
||||
@@ -222,35 +341,39 @@
|
||||
</Viewbox>
|
||||
</Grid>
|
||||
|
||||
<StackPanel Padding="4" Orientation="Vertical">
|
||||
<StackPanel
|
||||
Padding="4"
|
||||
Orientation="Vertical"
|
||||
Spacing="4"
|
||||
Visibility="{x:Bind help:BindTransformers.VisibleWhenAny(ShowTitle, ShowSubtitle)}">
|
||||
<TextBlock
|
||||
x:Name="TitleTextBlock"
|
||||
MaxWidth="152"
|
||||
MaxHeight="40"
|
||||
Margin="0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
CharacterSpacing="12"
|
||||
FontSize="14"
|
||||
Foreground="{ThemeResource TextFillColorPrimary}"
|
||||
Text="{x:Bind Title}"
|
||||
Text="{x:Bind Title, Mode=OneWay}"
|
||||
TextAlignment="Center"
|
||||
TextTrimming="WordEllipsis"
|
||||
TextWrapping="NoWrap" />
|
||||
TextWrapping="NoWrap"
|
||||
Visibility="{x:Bind ShowTitle, Mode=OneWay}" />
|
||||
<TextBlock
|
||||
x:Name="SubTitleTextBlock"
|
||||
MaxWidth="152"
|
||||
MaxHeight="40"
|
||||
Margin="0,4,0,0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
CharacterSpacing="11"
|
||||
FontSize="11"
|
||||
Foreground="{ThemeResource TextFillColorTertiary}"
|
||||
Text="{x:Bind Subtitle}"
|
||||
Text="{x:Bind Subtitle, Mode=OneWay}"
|
||||
TextAlignment="Center"
|
||||
TextTrimming="WordEllipsis"
|
||||
TextWrapping="NoWrap" />
|
||||
TextWrapping="NoWrap"
|
||||
Visibility="{x:Bind ShowSubtitle, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
@@ -295,6 +418,7 @@
|
||||
IsDoubleTapEnabled="True"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="Items_ItemClick"
|
||||
ItemContainerStyleSelector="{StaticResource GridItemContainerStyleSelector}"
|
||||
ItemTemplateSelector="{StaticResource GridItemTemplateSelector}"
|
||||
ItemsSource="{x:Bind ViewModel.FilteredItems, Mode=OneWay}"
|
||||
RightTapped="Items_RightTapped"
|
||||
@@ -302,6 +426,7 @@
|
||||
<GridView.ItemContainerTransitions>
|
||||
<TransitionCollection />
|
||||
</GridView.ItemContainerTransitions>
|
||||
<GridView.ItemContainerStyle />
|
||||
</GridView>
|
||||
</controls:Case>
|
||||
</controls:SwitchPresenter>
|
||||
|
||||
@@ -15,4 +15,7 @@ internal static class BindTransformers
|
||||
|
||||
public static Visibility EmptyOrWhitespaceToCollapsed(string? input)
|
||||
=> string.IsNullOrWhiteSpace(input) ? Visibility.Collapsed : Visibility.Visible;
|
||||
|
||||
public static Visibility VisibleWhenAny(bool value1, bool value2)
|
||||
=> (value1 || value2) ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
36
src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/BuildInfo.cs
Normal file
36
src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/BuildInfo.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
// 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.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Helpers;
|
||||
|
||||
internal static class BuildInfo
|
||||
{
|
||||
#if DEBUG
|
||||
public const string Configuration = "Debug";
|
||||
#else
|
||||
public const string Configuration = "Release";
|
||||
#endif
|
||||
|
||||
// Runtime AOT detection
|
||||
public static bool IsNativeAot => !RuntimeFeature.IsDynamicCodeSupported;
|
||||
|
||||
// From assembly metadata (build-time values)
|
||||
public static bool PublishTrimmed => GetBoolMetadata("PublishTrimmed", false);
|
||||
|
||||
// From assembly metadata (build-time values)
|
||||
public static bool PublishAot => GetBoolMetadata("PublishAot", false);
|
||||
|
||||
public static bool IsCiBuild => GetBoolMetadata("CIBuild", false);
|
||||
|
||||
private static string? GetMetadata(string key) =>
|
||||
Assembly.GetExecutingAssembly()
|
||||
.GetCustomAttributes<AssemblyMetadataAttribute>()
|
||||
.FirstOrDefault(a => a.Key == key)?.Value;
|
||||
|
||||
private static bool GetBoolMetadata(string key, bool defaultValue) =>
|
||||
bool.TryParse(GetMetadata(key), out var result) ? result : defaultValue;
|
||||
}
|
||||
@@ -14,5 +14,7 @@
|
||||
Activated="MainWindow_Activated"
|
||||
Closed="MainWindow_Closed"
|
||||
mc:Ignorable="d">
|
||||
<pages:ShellPage x:Name="RootShellPage" />
|
||||
<Grid x:Name="RootElement">
|
||||
<pages:ShellPage />
|
||||
</Grid>
|
||||
</winuiex:WindowEx>
|
||||
|
||||
@@ -11,6 +11,7 @@ using Microsoft.CmdPal.Core.Common.Helpers;
|
||||
using Microsoft.CmdPal.Core.Common.Services;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.Ext.ClipboardHistory.Messages;
|
||||
using Microsoft.CmdPal.UI.Controls;
|
||||
using Microsoft.CmdPal.UI.Events;
|
||||
using Microsoft.CmdPal.UI.Helpers;
|
||||
using Microsoft.CmdPal.UI.Messages;
|
||||
@@ -18,6 +19,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 +35,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 +52,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 +70,7 @@ public sealed partial class MainWindow : WindowEx,
|
||||
|
||||
private DesktopAcrylicController? _acrylicController;
|
||||
private SystemBackdropConfiguration? _configurationSource;
|
||||
private TimeSpan _autoGoHomeInterval = Timeout.InfiniteTimeSpan;
|
||||
|
||||
private WindowPosition _currentWindowPosition = new();
|
||||
|
||||
@@ -69,6 +78,9 @@ public sealed partial class MainWindow : WindowEx,
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_autoGoHomeTimer = new DispatcherTimer();
|
||||
_autoGoHomeTimer.Tick += OnAutoGoHomeTimerOnTick;
|
||||
|
||||
_hwnd = new HWND(WinRT.Interop.WindowNative.GetWindowHandle(this).ToInt32());
|
||||
|
||||
unsafe
|
||||
@@ -102,7 +114,7 @@ public sealed partial class MainWindow : WindowEx,
|
||||
ExtendsContentIntoTitleBar = true;
|
||||
AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Collapsed;
|
||||
SizeChanged += WindowSizeChanged;
|
||||
RootShellPage.Loaded += RootShellPage_Loaded;
|
||||
RootElement.Loaded += RootElementLoaded;
|
||||
|
||||
WM_TASKBAR_RESTART = PInvoke.RegisterWindowMessage("TaskbarCreated");
|
||||
|
||||
@@ -119,7 +131,7 @@ public sealed partial class MainWindow : WindowEx,
|
||||
App.Current.Services.GetService<SettingsModel>()!.SettingsChanged += SettingsChangedHandler;
|
||||
|
||||
// Make sure that we update the acrylic theme when the OS theme changes
|
||||
RootShellPage.ActualThemeChanged += (s, e) => DispatcherQueue.TryEnqueue(UpdateAcrylic);
|
||||
RootElement.ActualThemeChanged += (s, e) => DispatcherQueue.TryEnqueue(UpdateAcrylic);
|
||||
|
||||
// Hardcoding event name to avoid bringing in the PowerToys.interop dependency. Event name must match CMDPAL_SHOW_EVENT from shared_constants.h
|
||||
NativeEventWaiter.WaitForEventLoop("Local\\PowerToysCmdPal-ShowEvent-62336fcd-8611-4023-9b30-091a6af4cc5a", () =>
|
||||
@@ -135,6 +147,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)
|
||||
@@ -145,11 +166,18 @@ public sealed partial class MainWindow : WindowEx,
|
||||
|
||||
private void SettingsChangedHandler(SettingsModel sender, object? args) => HotReloadSettings();
|
||||
|
||||
private void RootShellPage_Loaded(object sender, RoutedEventArgs e) =>
|
||||
|
||||
private void RootElementLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// Now that our content has loaded, we can update our draggable regions
|
||||
UpdateRegionsForCustomTitleBar();
|
||||
|
||||
// Add dev ribbon if enabled
|
||||
if (!BuildInfo.IsCiBuild)
|
||||
{
|
||||
RootElement.Children.Add(new DevRibbon { Margin = new Thickness(-1, -1, 120, -1) });
|
||||
}
|
||||
}
|
||||
|
||||
private void WindowSizeChanged(object sender, WindowSizeChangedEventArgs args) => UpdateRegionsForCustomTitleBar();
|
||||
|
||||
private void PositionCentered()
|
||||
@@ -173,22 +201,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 +221,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 +242,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 +304,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 +323,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 +353,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 +560,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 +629,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);
|
||||
@@ -513,28 +666,28 @@ public sealed partial class MainWindow : WindowEx,
|
||||
private void UpdateRegionsForCustomTitleBar()
|
||||
{
|
||||
// Specify the interactive regions of the title bar.
|
||||
var scaleAdjustment = RootShellPage.XamlRoot.RasterizationScale;
|
||||
var scaleAdjustment = RootElement.XamlRoot.RasterizationScale;
|
||||
|
||||
// Get the rectangle around our XAML content. We're going to mark this
|
||||
// rectangle as "Passthrough", so that the normal window operations
|
||||
// (resizing, dragging) don't apply in this space.
|
||||
var transform = RootShellPage.TransformToVisual(null);
|
||||
var transform = RootElement.TransformToVisual(null);
|
||||
|
||||
// Reserve 16px of space at the top for dragging.
|
||||
var topHeight = 16;
|
||||
var bounds = transform.TransformBounds(new Rect(
|
||||
0,
|
||||
topHeight,
|
||||
RootShellPage.ActualWidth,
|
||||
RootShellPage.ActualHeight));
|
||||
RootElement.ActualWidth,
|
||||
RootElement.ActualHeight));
|
||||
var contentRect = GetRect(bounds, scaleAdjustment);
|
||||
var rectArray = new RectInt32[] { contentRect };
|
||||
var nonClientInputSrc = InputNonClientPointerSource.GetForWindowId(this.AppWindow.Id);
|
||||
nonClientInputSrc.SetRegionRects(NonClientRegionKind.Passthrough, rectArray);
|
||||
|
||||
// Add a drag-able region on top
|
||||
var w = RootShellPage.ActualWidth;
|
||||
_ = RootShellPage.ActualHeight;
|
||||
var w = RootElement.ActualWidth;
|
||||
_ = RootElement.ActualHeight;
|
||||
var dragSides = new RectInt32[]
|
||||
{
|
||||
GetRect(new Rect(0, 0, w, topHeight), scaleAdjustment), // the top, {topHeight=16} tall
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
|
||||
<LangVersion>preview</LangVersion>
|
||||
|
||||
<Version>$(CmdPalVersion)</Version>
|
||||
|
||||
@@ -25,10 +26,10 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- For debugging purposes, uncomment this block to enable AOT builds -->
|
||||
<!-- <PropertyGroup>
|
||||
<!--<PropertyGroup>
|
||||
<EnableCmdPalAOT>true</EnableCmdPalAOT>
|
||||
<CIBuild>true</CIBuild>
|
||||
</PropertyGroup> -->
|
||||
<GeneratePackageLocally>true</GeneratePackageLocally>
|
||||
</PropertyGroup>-->
|
||||
|
||||
<PropertyGroup Condition="'$(EnableCmdPalAOT)' == 'true'">
|
||||
<SelfContained>true</SelfContained>
|
||||
@@ -37,7 +38,7 @@
|
||||
<PublishAot>true</PublishAot>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(CIBuild)'=='true'">
|
||||
<PropertyGroup Condition="'$(CIBuild)' == 'true' or '$(GeneratePackageLocally)' == 'true'">
|
||||
<GenerateAppxPackageOnBuild>true</GenerateAppxPackageOnBuild>
|
||||
<AppxBundle>Never</AppxBundle>
|
||||
<AppxPackageTestDir>$(OutputPath)\AppPackages\Microsoft.CmdPal.UI_$(Version)_Test\</AppxPackageTestDir>
|
||||
@@ -66,6 +67,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="Controls\ActionBar.xaml" />
|
||||
<None Remove="Controls\DevRibbon.xaml" />
|
||||
<None Remove="Controls\KeyVisual\KeyCharPresenter.xaml" />
|
||||
<None Remove="Controls\SearchBar.xaml" />
|
||||
<None Remove="IsEnabledTextBlock.xaml" />
|
||||
@@ -118,6 +120,7 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\common\ManagedTelemetry\Telemetry\ManagedTelemetry.csproj" />
|
||||
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.ClipboardHistory\Microsoft.CmdPal.Ext.ClipboardHistory.csproj" />
|
||||
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.RemoteDesktop\Microsoft.CmdPal.Ext.RemoteDesktop.csproj" />
|
||||
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.System\Microsoft.CmdPal.Ext.System.csproj" />
|
||||
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.WebSearch\Microsoft.CmdPal.Ext.WebSearch.csproj" />
|
||||
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.Indexer\Microsoft.CmdPal.Ext.Indexer.csproj" />
|
||||
@@ -167,6 +170,9 @@
|
||||
<Page Update="Controls\SearchBar.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Update="Controls\DevRibbon.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Update="Styles\TextBox.xaml">
|
||||
<XamlRuntime>$(DefaultXamlRuntime)</XamlRuntime>
|
||||
</Page>
|
||||
@@ -234,4 +240,24 @@
|
||||
</ItemGroup>
|
||||
<!-- </AdaptiveCardsWorkaround> -->
|
||||
|
||||
<!-- Metadata for build information -->
|
||||
<ItemGroup>
|
||||
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
|
||||
<_Parameter1>PublishTrimmed</_Parameter1>
|
||||
<_Parameter2>$(PublishTrimmed)</_Parameter2>
|
||||
</AssemblyAttribute>
|
||||
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
|
||||
<_Parameter1>PublishAot</_Parameter1>
|
||||
<_Parameter2>$(PublishAot)</_Parameter2>
|
||||
</AssemblyAttribute>
|
||||
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
|
||||
<_Parameter1>CIBuild</_Parameter1>
|
||||
<_Parameter2>$(CIBuild)</_Parameter2>
|
||||
</AssemblyAttribute>
|
||||
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
|
||||
<_Parameter1>CommandPaletteBranding</_Parameter1>
|
||||
<_Parameter2>$(CommandPaletteBranding)</_Parameter2>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -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,190 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.UI.Helpers;
|
||||
using Microsoft.UI;
|
||||
using Windows.System;
|
||||
using Windows.UI;
|
||||
using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
internal sealed partial class DevRibbonViewModel : ObservableObject
|
||||
{
|
||||
private const int MaxLogEntries = 2;
|
||||
private const string Release = "Release";
|
||||
private const string Debug = "Debug";
|
||||
|
||||
private static readonly Color ReleaseAotColor = ColorHelper.FromArgb(255, 124, 58, 237);
|
||||
private static readonly Color ReleaseColor = ColorHelper.FromArgb(255, 51, 65, 85);
|
||||
private static readonly Color DebugAotColor = ColorHelper.FromArgb(255, 99, 102, 241);
|
||||
private static readonly Color DebugColor = ColorHelper.FromArgb(255, 107, 114, 128);
|
||||
|
||||
private readonly DispatcherQueue _dispatcherQueue;
|
||||
|
||||
public DevRibbonViewModel()
|
||||
{
|
||||
_dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
Trace.Listeners.Add(new DevRibbonTraceListener(this));
|
||||
|
||||
var configLabel = BuildConfiguration == Release ? "RLS" : "DBG"; /* #no-spell-check-line */
|
||||
var aotLabel = BuildInfo.IsNativeAot ? "⚡AOT" : "NO AOT";
|
||||
Tag = $"{configLabel} | {aotLabel}";
|
||||
|
||||
TagColor = (BuildConfiguration, BuildInfo.IsNativeAot) switch
|
||||
{
|
||||
(Release, true) => ReleaseAotColor,
|
||||
(Release, false) => ReleaseColor,
|
||||
(Debug, true) => DebugAotColor,
|
||||
(Debug, false) => DebugColor,
|
||||
_ => Colors.Fuchsia,
|
||||
};
|
||||
}
|
||||
|
||||
public string BuildConfiguration => BuildInfo.Configuration;
|
||||
|
||||
public bool IsAotReleaseConfiguration => BuildConfiguration == Release && BuildInfo.IsNativeAot;
|
||||
|
||||
public bool IsAot => BuildInfo.IsNativeAot;
|
||||
|
||||
public bool IsPublishTrimmed => BuildInfo.PublishTrimmed;
|
||||
|
||||
public ObservableCollection<LogEntryViewModel> LatestLogs { get; } = [];
|
||||
|
||||
[ObservableProperty]
|
||||
public partial int WarningCount { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial int ErrorCount { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string Tag { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial Color TagColor { get; private set; }
|
||||
|
||||
[RelayCommand]
|
||||
private async Task OpenLogFileAsync()
|
||||
{
|
||||
var logPath = Logger.CurrentLogFile;
|
||||
if (File.Exists(logPath))
|
||||
{
|
||||
await Launcher.LaunchUriAsync(new Uri(logPath));
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task OpenLogFolderAsync()
|
||||
{
|
||||
var logFolderPath = Logger.CurrentVersionLogDirectoryPath;
|
||||
if (Directory.Exists(logFolderPath))
|
||||
{
|
||||
await Launcher.LaunchFolderPathAsync(logFolderPath);
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ResetErrorCounters()
|
||||
{
|
||||
WarningCount = 0;
|
||||
ErrorCount = 0;
|
||||
LatestLogs.Clear();
|
||||
}
|
||||
|
||||
private sealed partial class DevRibbonTraceListener(DevRibbonViewModel viewModel) : TraceListener
|
||||
{
|
||||
private const string TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff";
|
||||
|
||||
[GeneratedRegex(@"^\[(?<timestamp>.*?)\] \[(?<severity>.*?)\] (?<message>.*)")]
|
||||
private static partial Regex LogRegex();
|
||||
|
||||
private readonly Lock _lock = new();
|
||||
private LogEntryViewModel? _latestLogEntry;
|
||||
|
||||
public override void Write(string? message)
|
||||
{
|
||||
// Not required for this scenario.
|
||||
}
|
||||
|
||||
public override void WriteLine(string? message)
|
||||
{
|
||||
if (message is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
var match = LogRegex().Match(message);
|
||||
if (match.Success)
|
||||
{
|
||||
var severity = match.Groups["severity"].Value;
|
||||
var isWarning = severity.Equals("Warning", StringComparison.OrdinalIgnoreCase);
|
||||
var isError = severity.Equals("Error", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (isWarning || isError)
|
||||
{
|
||||
var timestampStr = match.Groups["timestamp"].Value;
|
||||
var timestamp = DateTimeOffset.TryParseExact(
|
||||
timestampStr,
|
||||
TimestampFormat,
|
||||
CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.AssumeLocal,
|
||||
out var parsed)
|
||||
? parsed
|
||||
: DateTimeOffset.Now;
|
||||
|
||||
var logEntry = new LogEntryViewModel(
|
||||
timestamp,
|
||||
severity,
|
||||
match.Groups["message"].Value,
|
||||
string.Empty);
|
||||
|
||||
_latestLogEntry = logEntry;
|
||||
|
||||
viewModel._dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
if (isWarning)
|
||||
{
|
||||
viewModel.WarningCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
viewModel.ErrorCount++;
|
||||
}
|
||||
|
||||
viewModel.LatestLogs.Insert(0, logEntry);
|
||||
|
||||
while (viewModel.LatestLogs.Count > MaxLogEntries)
|
||||
{
|
||||
viewModel.LatestLogs.RemoveAt(viewModel.LatestLogs.Count - 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
_latestLogEntry = null;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (IndentLevel > 0 && _latestLogEntry is { } latest)
|
||||
{
|
||||
viewModel._dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
latest.AppendDetails(message);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// 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.Globalization;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
internal sealed partial class LogEntryViewModel : ObservableObject
|
||||
{
|
||||
private const int HeaderMaxLength = 80;
|
||||
private const string WarningGlyph = "\uE7BA";
|
||||
private const string ErrorGlyph = "\uEA39";
|
||||
private const string TimestampFormat = "HH:mm:ss";
|
||||
|
||||
private DateTimeOffset Timestamp { get; }
|
||||
|
||||
private string Severity { get; }
|
||||
|
||||
private string Message { get; }
|
||||
|
||||
private string FormattedTimestamp { get; }
|
||||
|
||||
public string SeverityGlyph { get; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string Header { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string Description { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string Details { get; private set; }
|
||||
|
||||
public LogEntryViewModel(DateTimeOffset timestamp, string severity, string message, string details)
|
||||
{
|
||||
Timestamp = timestamp;
|
||||
Severity = severity;
|
||||
Message = message;
|
||||
Details = details;
|
||||
|
||||
SeverityGlyph = severity.ToUpperInvariant() switch
|
||||
{
|
||||
"WARNING" => WarningGlyph,
|
||||
"ERROR" => ErrorGlyph,
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
FormattedTimestamp = timestamp.ToString(TimestampFormat, CultureInfo.CurrentCulture);
|
||||
Description = $"{FormattedTimestamp} • {Message}";
|
||||
Header = Message;
|
||||
}
|
||||
|
||||
public void AppendDetails(string? message)
|
||||
{
|
||||
if (string.IsNullOrEmpty(message))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Details += Environment.NewLine + message;
|
||||
|
||||
// Make header the second line of details (because that's actually the message itself):
|
||||
var detailsLines = Details.Split([Environment.NewLine], StringSplitOptions.None);
|
||||
if (detailsLines.Length < 2)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Header = detailsLines[1].Trim();
|
||||
if (Header.Length > HeaderMaxLength)
|
||||
{
|
||||
Header = Header[..(HeaderMaxLength - 1)] + "…";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
// 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.Globalization;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using Microsoft.CmdPal.Ext.RemoteDesktop.Commands;
|
||||
using Microsoft.CmdPal.Ext.RemoteDesktop.Helper;
|
||||
using Microsoft.CmdPal.Ext.RemoteDesktop.Properties;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests;
|
||||
|
||||
[TestClass]
|
||||
public class FallbackRemoteDesktopItemTests
|
||||
{
|
||||
private static readonly CompositeFormat OpenHostCompositeFormat = CompositeFormat.Parse(Resources.remotedesktop_open_host);
|
||||
|
||||
[TestMethod]
|
||||
public void UpdateQuery_WhenMatchingConnectionExists_UsesConnectionName()
|
||||
{
|
||||
var connectionName = "my-rdp-server";
|
||||
|
||||
// Arrange
|
||||
var setup = CreateFallback(connectionName);
|
||||
var fallback = setup.Fallback;
|
||||
|
||||
// Act
|
||||
fallback.UpdateQuery("my-rdp-server");
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(connectionName, fallback.Title);
|
||||
var expectedSubtitle = string.Format(CultureInfo.CurrentCulture, OpenHostCompositeFormat, connectionName);
|
||||
Assert.AreEqual(expectedSubtitle, fallback.Subtitle);
|
||||
|
||||
var command = fallback.Command as OpenRemoteDesktopCommand;
|
||||
Assert.IsNotNull(command);
|
||||
Assert.AreEqual(Resources.remotedesktop_command_connect, command.Name);
|
||||
Assert.AreEqual(connectionName, GetCommandHost(command));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UpdateQuery_WhenQueryIsValidHostWithoutExistingConnection_UsesQuery()
|
||||
{
|
||||
// Arrange
|
||||
var setup = CreateFallback();
|
||||
var fallback = setup.Fallback;
|
||||
const string hostname = "test.corp";
|
||||
|
||||
// Act
|
||||
fallback.UpdateQuery(hostname);
|
||||
|
||||
// Assert
|
||||
var expectedTitle = string.Format(CultureInfo.CurrentCulture, OpenHostCompositeFormat, hostname);
|
||||
Assert.AreEqual(expectedTitle, fallback.Title);
|
||||
Assert.AreEqual(Resources.remotedesktop_title, fallback.Subtitle);
|
||||
|
||||
var command = fallback.Command as OpenRemoteDesktopCommand;
|
||||
Assert.IsNotNull(command);
|
||||
Assert.AreEqual(Resources.remotedesktop_command_connect, command.Name);
|
||||
Assert.AreEqual(hostname, GetCommandHost(command));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UpdateQuery_WhenQueryIsWhitespace_ResetsCommand()
|
||||
{
|
||||
// Arrange
|
||||
var setup = CreateFallback("rdp-server-two");
|
||||
var fallback = setup.Fallback;
|
||||
|
||||
// Act
|
||||
fallback.UpdateQuery(" ");
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(Resources.remotedesktop_command_open, fallback.Title);
|
||||
Assert.AreEqual(string.Empty, fallback.Subtitle);
|
||||
|
||||
var command = fallback.Command as OpenRemoteDesktopCommand;
|
||||
Assert.IsNotNull(command);
|
||||
Assert.AreEqual(Resources.remotedesktop_command_open, command.Name);
|
||||
Assert.AreEqual(string.Empty, GetCommandHost(command));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UpdateQuery_WhenQueryIsInvalidHost_ClearsCommand()
|
||||
{
|
||||
// Arrange
|
||||
var setup = CreateFallback("rdp-server-three");
|
||||
var fallback = setup.Fallback;
|
||||
|
||||
// Act
|
||||
fallback.UpdateQuery("not a valid host");
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(Resources.remotedesktop_command_open, fallback.Title);
|
||||
Assert.AreEqual(string.Empty, fallback.Subtitle);
|
||||
|
||||
var command = fallback.Command as OpenRemoteDesktopCommand;
|
||||
Assert.IsNotNull(command);
|
||||
Assert.AreEqual(Resources.remotedesktop_command_open, command.Name);
|
||||
Assert.AreEqual(string.Empty, GetCommandHost(command));
|
||||
}
|
||||
|
||||
private static string GetCommandHost(OpenRemoteDesktopCommand command)
|
||||
{
|
||||
var field = typeof(OpenRemoteDesktopCommand).GetField("_rdpHost", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
if (field is null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return field.GetValue(command) as string ?? string.Empty;
|
||||
}
|
||||
|
||||
private static (FallbackRemoteDesktopItem Fallback, IRdpConnectionsManager Manager) CreateFallback(params string[] connectionNames)
|
||||
{
|
||||
var settingsManager = new MockSettingsManager(connectionNames);
|
||||
var connectionsManager = new MockRdpConnectionsManager(settingsManager);
|
||||
|
||||
var fallback = new FallbackRemoteDesktopItem(connectionsManager);
|
||||
|
||||
return (fallback, connectionsManager);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<!-- Look at Directory.Build.props in root for common stuff as well -->
|
||||
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<RootNamespace>Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests</RootNamespace>
|
||||
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal\tests\</OutputPath>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="MSTest" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\ext\Microsoft.CmdPal.Ext.RemoteDesktop\Microsoft.CmdPal.Ext.RemoteDesktop.csproj" />
|
||||
<ProjectReference Include="..\Microsoft.CmdPal.Ext.UnitTestsBase\Microsoft.CmdPal.Ext.UnitTestBase.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.CmdPal.Ext.RemoteDesktop.Commands;
|
||||
using Microsoft.CmdPal.Ext.RemoteDesktop.Helper;
|
||||
using Microsoft.CmdPal.Ext.RemoteDesktop.Settings;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests;
|
||||
|
||||
internal sealed class MockRdpConnectionsManager : IRdpConnectionsManager
|
||||
{
|
||||
private readonly List<ConnectionListItem> _connections = new();
|
||||
|
||||
public IReadOnlyCollection<ConnectionListItem> Connections => _connections.AsReadOnly();
|
||||
|
||||
public MockRdpConnectionsManager(ISettingsInterface settingsManager)
|
||||
{
|
||||
_connections.AddRange(settingsManager.PredefinedConnections.Select(ConnectionHelpers.MapToResult));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.CmdPal.Ext.RemoteDesktop.Settings;
|
||||
using ToolkitSettings = Microsoft.CommandPalette.Extensions.Toolkit.Settings;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests;
|
||||
|
||||
internal sealed class MockSettingsManager : ISettingsInterface
|
||||
{
|
||||
private readonly List<string> _connections;
|
||||
|
||||
public IReadOnlyCollection<string> PredefinedConnections => _connections;
|
||||
|
||||
public ToolkitSettings Settings { get; } = new();
|
||||
|
||||
public MockSettingsManager(params string[] predefinedConnections)
|
||||
{
|
||||
_connections = new(predefinedConnections);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// 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.Linq;
|
||||
using Microsoft.CmdPal.Ext.RemoteDesktop.Commands;
|
||||
using Microsoft.CmdPal.Ext.RemoteDesktop.Helper;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests;
|
||||
|
||||
[TestClass]
|
||||
public class RdpConnectionsManagerTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void Constructor_AddsOpenCommandItem()
|
||||
{
|
||||
// Act
|
||||
var manager = new RdpConnectionsManager(new MockSettingsManager(["test.local"]));
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(manager.Connections.Any(item => string.IsNullOrEmpty(item.ConnectionName)));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FindConnection_ReturnsExactMatch()
|
||||
{
|
||||
// Arrange
|
||||
var connectionName = "rdp-test";
|
||||
var connection = new ConnectionListItem(connectionName);
|
||||
|
||||
// Act
|
||||
var result = ConnectionHelpers.FindConnection(connectionName, new[] { connection });
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(connectionName, result.ConnectionName);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FindConnection_ReturnsNullForWhitespaceQuery()
|
||||
{
|
||||
// Arrange
|
||||
var connection = new ConnectionListItem("rdp-test");
|
||||
|
||||
// Act
|
||||
var result = ConnectionHelpers.FindConnection(" ", new[] { connection });
|
||||
|
||||
// Assert
|
||||
Assert.IsNull(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
// 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.Linq;
|
||||
using Microsoft.CmdPal.Ext.RemoteDesktop.Commands;
|
||||
using Microsoft.CmdPal.Ext.RemoteDesktop.Pages;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests;
|
||||
|
||||
[TestClass]
|
||||
public class RemoteDesktopCommandProviderTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void ProviderHasCorrectId()
|
||||
{
|
||||
// Setup
|
||||
var provider = new RemoteDesktopCommandProvider();
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual("com.microsoft.cmdpal.builtin.remotedesktop", provider.Id);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ProviderHasDisplayName()
|
||||
{
|
||||
// Setup
|
||||
var provider = new RemoteDesktopCommandProvider();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(provider.DisplayName);
|
||||
Assert.IsTrue(provider.DisplayName.Length > 0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ProviderHasIcon()
|
||||
{
|
||||
// Setup
|
||||
var provider = new RemoteDesktopCommandProvider();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(provider.Icon);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TopLevelCommandsNotEmpty()
|
||||
{
|
||||
// Setup
|
||||
var provider = new RemoteDesktopCommandProvider();
|
||||
|
||||
// Act
|
||||
var commands = provider.TopLevelCommands();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(commands);
|
||||
Assert.IsTrue(commands.Length > 0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FallbackCommandsNotEmpty()
|
||||
{
|
||||
// Setup
|
||||
var provider = new RemoteDesktopCommandProvider();
|
||||
|
||||
// Act
|
||||
var commands = provider.FallbackCommands();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(commands);
|
||||
Assert.IsTrue(commands.Length > 0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TopLevelCommandsContainListPageCommand()
|
||||
{
|
||||
// Setup
|
||||
var provider = new RemoteDesktopCommandProvider();
|
||||
|
||||
// Act
|
||||
var commands = provider.TopLevelCommands();
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(1, commands.Length);
|
||||
Assert.IsInstanceOfType(commands.Single().Command, typeof(RemoteDesktopListPage));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FallbackCommandsContainFallbackItem()
|
||||
{
|
||||
// Setup
|
||||
var provider = new RemoteDesktopCommandProvider();
|
||||
|
||||
// Act
|
||||
var commands = provider.FallbackCommands();
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(1, commands.Length);
|
||||
Assert.IsInstanceOfType(commands.Single(), typeof(FallbackRemoteDesktopItem));
|
||||
}
|
||||
}
|
||||
@@ -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" };
|
||||
}
|
||||
@@ -18,6 +18,8 @@ public class MockSettingsInterface : ISettingsInterface
|
||||
|
||||
public int HistoryItemCount { get; set; }
|
||||
|
||||
public string CustomSearchUri { get; }
|
||||
|
||||
public IReadOnlyList<HistoryItem> HistoryItems => _historyItems;
|
||||
|
||||
public MockSettingsInterface(int historyItemCount = 0, bool globalIfUri = true, List<HistoryItem> mockHistory = null)
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.4 KiB |
@@ -0,0 +1,21 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="3" y="3.55078" width="9.36537" height="9.36537" rx="0.720413" fill="url(#paint0_linear_2155_27162)"/>
|
||||
<rect x="1" y="2" width="13" height="9" rx="0.722222" fill="url(#paint1_linear_2155_27162)"/>
|
||||
<circle cx="11.4" cy="9.4" r="4.4" fill="url(#paint2_radial_2155_27162)"/>
|
||||
<path d="M13.8703 11.2497C13.964 11.3434 14.116 11.3434 14.2097 11.2497C14.3034 11.156 14.3034 11.004 14.2097 10.9103L12.4594 9.16L14.2097 7.40971C14.3034 7.31598 14.3034 7.16402 14.2097 7.07029C14.116 6.97657 13.964 6.97657 13.8703 7.07029L11.9503 8.9903C11.8566 9.08402 11.8566 9.23598 11.9503 9.32971L13.8703 11.2497ZM9.40971 8.0303C9.31598 7.93657 9.16402 7.93657 9.07029 8.0303C8.97657 8.12402 8.97657 8.27598 9.07029 8.36971L10.8206 10.12L9.07029 11.8703C8.97657 11.964 8.97657 12.116 9.07029 12.2097C9.16402 12.3034 9.31598 12.3034 9.40971 12.2097L11.3297 10.2897C11.4234 10.196 11.4234 10.044 11.3297 9.95031L9.40971 8.0303Z" fill="#666666" stroke="#666666" stroke-width="0.146667"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_2155_27162" x1="3.22298" y1="3.55078" x2="8.52487" y2="6.68847" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#246FB0"/>
|
||||
<stop offset="1" stop-color="#14518A"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_2155_27162" x1="1.15476" y1="1.66667" x2="14.867" y2="9.90553" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#86D6F9"/>
|
||||
<stop offset="1" stop-color="#1FA3E4"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="paint2_radial_2155_27162" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(10.9111 6.22222) rotate(90) scale(7.57778)">
|
||||
<stop stop-color="#E7ECF1"/>
|
||||
<stop offset="0.84" stop-color="#D2D4D6"/>
|
||||
<stop offset="1" stop-color="#A9ABAC"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using Microsoft.CmdPal.Ext.RemoteDesktop.Properties;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.RemoteDesktop.Commands;
|
||||
|
||||
internal sealed partial class ConnectionListItem : ListItem
|
||||
{
|
||||
public ConnectionListItem(string connectionName)
|
||||
{
|
||||
ConnectionName = connectionName;
|
||||
|
||||
if (string.IsNullOrEmpty(connectionName))
|
||||
{
|
||||
Title = Resources.remotedesktop_open_rdp;
|
||||
Subtitle = Resources.remotedesktop_subtitle;
|
||||
}
|
||||
else
|
||||
{
|
||||
Title = connectionName;
|
||||
CompositeFormat remoteDesktopOpenHostFormat = CompositeFormat.Parse(Resources.remotedesktop_open_host);
|
||||
Subtitle = string.Format(CultureInfo.CurrentCulture, remoteDesktopOpenHostFormat, connectionName);
|
||||
}
|
||||
|
||||
Icon = Icons.RDPIcon;
|
||||
Command = new OpenRemoteDesktopCommand(connectionName);
|
||||
}
|
||||
|
||||
public string ConnectionName { get; }
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// 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.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.CmdPal.Ext.RemoteDesktop.Helper;
|
||||
using Microsoft.CmdPal.Ext.RemoteDesktop.Properties;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.RemoteDesktop.Commands;
|
||||
|
||||
internal sealed partial class FallbackRemoteDesktopItem : FallbackCommandItem
|
||||
{
|
||||
private const string _id = "com.microsoft.cmdpal.builtin.remotedesktop.fallback";
|
||||
|
||||
private static readonly UriHostNameType[] ValidUriHostNameTypes = [
|
||||
UriHostNameType.IPv6,
|
||||
UriHostNameType.IPv4,
|
||||
UriHostNameType.Dns
|
||||
];
|
||||
|
||||
private static readonly CompositeFormat RemoteDesktopOpenHostFormat = CompositeFormat.Parse(Resources.remotedesktop_open_host);
|
||||
private readonly IRdpConnectionsManager _rdpConnectionsManager;
|
||||
|
||||
public FallbackRemoteDesktopItem(IRdpConnectionsManager rdpConnectionsManager)
|
||||
: base(new OpenRemoteDesktopCommand(string.Empty), Resources.remotedesktop_title)
|
||||
{
|
||||
_rdpConnectionsManager = rdpConnectionsManager;
|
||||
|
||||
Title = string.Empty;
|
||||
Subtitle = string.Empty;
|
||||
Icon = Icons.RDPIcon;
|
||||
}
|
||||
|
||||
public override void UpdateQuery(string query)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
Title = string.Empty;
|
||||
Subtitle = string.Empty;
|
||||
Command = new OpenRemoteDesktopCommand(string.Empty);
|
||||
return;
|
||||
}
|
||||
|
||||
var connections = _rdpConnectionsManager.Connections.Where(w => !string.IsNullOrWhiteSpace(w.ConnectionName));
|
||||
|
||||
var queryConnection = ConnectionHelpers.FindConnection(query, connections);
|
||||
|
||||
if (queryConnection is not null && !string.IsNullOrWhiteSpace(queryConnection.ConnectionName))
|
||||
{
|
||||
var connectionName = queryConnection.ConnectionName;
|
||||
|
||||
Command = new OpenRemoteDesktopCommand(connectionName);
|
||||
Title = connectionName;
|
||||
Subtitle = string.Format(CultureInfo.CurrentCulture, RemoteDesktopOpenHostFormat, connectionName);
|
||||
}
|
||||
else if (ValidUriHostNameTypes.Contains(Uri.CheckHostName(query)))
|
||||
{
|
||||
var connectionName = query.Trim();
|
||||
Command = new OpenRemoteDesktopCommand(connectionName);
|
||||
Title = string.Format(CultureInfo.CurrentCulture, RemoteDesktopOpenHostFormat, connectionName);
|
||||
Subtitle = Resources.remotedesktop_title;
|
||||
}
|
||||
else
|
||||
{
|
||||
Title = string.Empty;
|
||||
Subtitle = string.Empty;
|
||||
Command = new OpenRemoteDesktopCommand(string.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// 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.Diagnostics;
|
||||
using System.Text;
|
||||
using Microsoft.CmdPal.Ext.RemoteDesktop.Properties;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.RemoteDesktop.Commands;
|
||||
|
||||
internal sealed partial class OpenRemoteDesktopCommand : BaseObservable, IInvokableCommand
|
||||
{
|
||||
private static readonly CompositeFormat ProcessErrorFormat =
|
||||
CompositeFormat.Parse(Resources.remotedesktop_log_mstsc_error);
|
||||
|
||||
private static readonly CompositeFormat InvalidHostnameFormat =
|
||||
CompositeFormat.Parse(Resources.remotedesktop_log_invalid_hostname);
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public string Id { get; } = "com.microsoft.cmdpal.builtin.remotedesktop.openrdp";
|
||||
|
||||
public IIconInfo Icon => Icons.RDPIcon;
|
||||
|
||||
private readonly string _rdpHost;
|
||||
|
||||
public OpenRemoteDesktopCommand(string rdpHost)
|
||||
{
|
||||
_rdpHost = rdpHost;
|
||||
|
||||
Name = string.IsNullOrWhiteSpace(_rdpHost) ?
|
||||
Resources.remotedesktop_command_open :
|
||||
Resources.remotedesktop_command_connect;
|
||||
}
|
||||
|
||||
public ICommandResult Invoke(object sender)
|
||||
{
|
||||
using var process = new Process();
|
||||
process.StartInfo.UseShellExecute = false;
|
||||
process.StartInfo.WorkingDirectory = Environment.SpecialFolder.MyDocuments.ToString();
|
||||
process.StartInfo.FileName = "mstsc";
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_rdpHost))
|
||||
{
|
||||
// validate that _rdpHost is a proper hostname or IP address
|
||||
if (Uri.CheckHostName(_rdpHost) == UriHostNameType.Unknown)
|
||||
{
|
||||
return CommandResult.ShowToast(new ToastArgs()
|
||||
{
|
||||
Message = string.Format(
|
||||
System.Globalization.CultureInfo.CurrentCulture,
|
||||
InvalidHostnameFormat,
|
||||
_rdpHost),
|
||||
Result = CommandResult.KeepOpen(),
|
||||
});
|
||||
}
|
||||
|
||||
process.StartInfo.Arguments = $"/v:{_rdpHost}";
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
process.Start();
|
||||
|
||||
return CommandResult.Dismiss();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return CommandResult.ShowToast(new ToastArgs()
|
||||
{
|
||||
Message = string.Format(
|
||||
System.Globalization.CultureInfo.CurrentCulture,
|
||||
ProcessErrorFormat,
|
||||
ex.Message),
|
||||
Result = CommandResult.KeepOpen(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.CmdPal.Ext.RemoteDesktop.Commands;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.RemoteDesktop.Helper;
|
||||
|
||||
internal static class ConnectionHelpers
|
||||
{
|
||||
public static ConnectionListItem MapToResult(string item) => new(item);
|
||||
|
||||
public static ConnectionListItem? FindConnection(string query, IEnumerable<ConnectionListItem> connections)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var matchedConnection = ListHelpers.FilterList(
|
||||
connections,
|
||||
query,
|
||||
(s, i) => ListHelpers.ScoreListItem(s, i))
|
||||
.FirstOrDefault();
|
||||
return matchedConnection;
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.CmdPal.Ext.RemoteDesktop.Commands;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.RemoteDesktop.Helper;
|
||||
|
||||
internal interface IRdpConnectionsManager
|
||||
{
|
||||
IReadOnlyCollection<ConnectionListItem> Connections { get; }
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// 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.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Microsoft.CmdPal.Ext.RemoteDesktop.Commands;
|
||||
using Microsoft.CmdPal.Ext.RemoteDesktop.Settings;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.RemoteDesktop.Helper;
|
||||
|
||||
internal class RdpConnectionsManager : IRdpConnectionsManager
|
||||
{
|
||||
private readonly ISettingsInterface _settingsManager;
|
||||
private readonly ConnectionListItem _openRdpCommandListItem = new(string.Empty);
|
||||
|
||||
private ReadOnlyCollection<ConnectionListItem> _connections = new(Array.Empty<ConnectionListItem>());
|
||||
|
||||
private const int MinutesToCache = 1;
|
||||
private DateTime? _connectionsLastLoaded;
|
||||
|
||||
public RdpConnectionsManager(ISettingsInterface settingsManager)
|
||||
{
|
||||
_settingsManager = settingsManager;
|
||||
_settingsManager.Settings.SettingsChanged += (s, e) =>
|
||||
{
|
||||
_connectionsLastLoaded = null;
|
||||
};
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<ConnectionListItem> Connections
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!_connectionsLastLoaded.HasValue ||
|
||||
(DateTime.Now - _connectionsLastLoaded.Value).TotalMinutes >= MinutesToCache)
|
||||
{
|
||||
var registryConnections = GetRdpConnectionsFromRegistry();
|
||||
var predefinedConnections = GetPredefinedConnectionsFromSettings();
|
||||
_connectionsLastLoaded = DateTime.Now;
|
||||
|
||||
var newConnections = new List<ConnectionListItem>(registryConnections.Count + predefinedConnections.Count + 1);
|
||||
newConnections.AddRange(registryConnections);
|
||||
newConnections.AddRange(predefinedConnections);
|
||||
newConnections.Insert(0, _openRdpCommandListItem);
|
||||
|
||||
Interlocked.Exchange(ref _connections, new ReadOnlyCollection<ConnectionListItem>(newConnections));
|
||||
}
|
||||
|
||||
return _connections;
|
||||
}
|
||||
}
|
||||
|
||||
private List<ConnectionListItem> GetRdpConnectionsFromRegistry()
|
||||
{
|
||||
using var key = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Terminal Server Client\Default");
|
||||
|
||||
var validConnections = new List<ConnectionListItem>();
|
||||
|
||||
if (key is not null)
|
||||
{
|
||||
validConnections = key.GetValueNames()
|
||||
.Select(name => key.GetValue(name))
|
||||
.OfType<string>() // Keep only string values
|
||||
.Select(v => v.Trim()) // Normalize
|
||||
.Where(v => !string.IsNullOrWhiteSpace(v))
|
||||
.Distinct() // Remove dupes if any
|
||||
.Select(ConnectionHelpers.MapToResult)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
return validConnections;
|
||||
}
|
||||
|
||||
private List<ConnectionListItem> GetPredefinedConnectionsFromSettings()
|
||||
{
|
||||
var validConnections = _settingsManager.PredefinedConnections
|
||||
.Select(s => s.Trim())
|
||||
.Where(value => !string.IsNullOrWhiteSpace(value))
|
||||
.Select(ConnectionHelpers.MapToResult)
|
||||
.ToList();
|
||||
|
||||
return validConnections;
|
||||
}
|
||||
}
|
||||
@@ -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.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.RemoteDesktop;
|
||||
|
||||
internal static class Icons
|
||||
{
|
||||
internal static IconInfo RDPIcon { get; } = IconHelpers.FromRelativePath("Assets\\RemoteDesktop.svg");
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
|
||||
<Import Project="..\..\..\..\Common.Dotnet.AotCompatibility.props" />
|
||||
<Import Project="..\Common.ExtDependencies.props" />
|
||||
<PropertyGroup>
|
||||
<RootNamespace>Microsoft.CmdPal.Ext.RemoteDesktop</RootNamespace>
|
||||
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal</OutputPath>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<!-- MRT from windows app sdk will search for a pri file with the same name of the module before defaulting to resources.pri -->
|
||||
<ProjectPriFileName>Microsoft.CmdPal.Ext.RemoteDesktop.pri</ProjectPriFileName>
|
||||
<nullable>enable</nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
|
||||
<!-- CmdPal Toolkit reference now included via Common.ExtDependencies.props -->
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Update="Properties\Resources.Designer.cs">
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="Properties\Resources.resx">
|
||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="Assets\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Update="Assets\RemoteDesktop.png">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Update="Assets\RemoteDesktop.svg">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,27 @@
|
||||
// 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.Linq;
|
||||
using Microsoft.CmdPal.Ext.RemoteDesktop.Helper;
|
||||
using Microsoft.CmdPal.Ext.RemoteDesktop.Properties;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.RemoteDesktop.Pages;
|
||||
|
||||
internal sealed partial class RemoteDesktopListPage : ListPage
|
||||
{
|
||||
private readonly IRdpConnectionsManager _rdpConnectionsManager;
|
||||
|
||||
public RemoteDesktopListPage(IRdpConnectionsManager rdpConnectionsManager)
|
||||
{
|
||||
Icon = Icons.RDPIcon;
|
||||
Name = Resources.remotedesktop_title;
|
||||
Id = "com.microsoft.cmdpal.builtin.remotedesktop";
|
||||
|
||||
_rdpConnectionsManager = rdpConnectionsManager;
|
||||
}
|
||||
|
||||
public override IListItem[] GetItems() => _rdpConnectionsManager.Connections.ToArray();
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests")]
|
||||
153
src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Properties/Resources.Designer.cs
generated
Normal file
153
src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Properties/Resources.Designer.cs
generated
Normal file
@@ -0,0 +1,153 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// Runtime Version:4.0.30319.42000
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.RemoteDesktop.Properties {
|
||||
using System;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A strongly-typed resource class, for looking up localized strings, etc.
|
||||
/// </summary>
|
||||
// This class was auto-generated by the StronglyTypedResourceBuilder
|
||||
// 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.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
public class Resources {
|
||||
|
||||
private static global::System.Resources.ResourceManager resourceMan;
|
||||
|
||||
private static global::System.Globalization.CultureInfo resourceCulture;
|
||||
|
||||
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
||||
internal Resources() {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the cached ResourceManager instance used by this class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
public static global::System.Resources.ResourceManager ResourceManager {
|
||||
get {
|
||||
if (object.ReferenceEquals(resourceMan, null)) {
|
||||
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.CmdPal.Ext.RemoteDesktop.Properties.Resources", typeof(Resources).Assembly);
|
||||
resourceMan = temp;
|
||||
}
|
||||
return resourceMan;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overrides the current thread's CurrentUICulture property for all
|
||||
/// resource lookups using this strongly typed resource class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
public static global::System.Globalization.CultureInfo Culture {
|
||||
get {
|
||||
return resourceCulture;
|
||||
}
|
||||
set {
|
||||
resourceCulture = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Connect.
|
||||
/// </summary>
|
||||
public static string remotedesktop_command_connect {
|
||||
get {
|
||||
return ResourceManager.GetString("remotedesktop_command_connect", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Open.
|
||||
/// </summary>
|
||||
public static string remotedesktop_command_open {
|
||||
get {
|
||||
return ResourceManager.GetString("remotedesktop_command_open", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to The hostname '{0}' was invalid. Ensure you're using a valid hostname or IP address..
|
||||
/// </summary>
|
||||
public static string remotedesktop_log_invalid_hostname {
|
||||
get {
|
||||
return ResourceManager.GetString("remotedesktop_log_invalid_hostname", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Unable to initialize Microsoft Terminal Service Client. Ensure it is enabled in Windows Settings.\r{0}.
|
||||
/// </summary>
|
||||
public static string remotedesktop_log_mstsc_error {
|
||||
get {
|
||||
return ResourceManager.GetString("remotedesktop_log_mstsc_error", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Connect to {0}.
|
||||
/// </summary>
|
||||
public static string remotedesktop_open_host {
|
||||
get {
|
||||
return ResourceManager.GetString("remotedesktop_open_host", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Open Remote Desktop Client.
|
||||
/// </summary>
|
||||
public static string remotedesktop_open_rdp {
|
||||
get {
|
||||
return ResourceManager.GetString("remotedesktop_open_rdp", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to A list of connections to include in the query results by default.
|
||||
/// </summary>
|
||||
public static string remotedesktop_settings_predefined_connections_description {
|
||||
get {
|
||||
return ResourceManager.GetString("remotedesktop_settings_predefined_connections_description", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Predefined connections.
|
||||
/// </summary>
|
||||
public static string remotedesktop_settings_predefined_connections_title {
|
||||
get {
|
||||
return ResourceManager.GetString("remotedesktop_settings_predefined_connections_title", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Establish Remote Desktop connections.
|
||||
/// </summary>
|
||||
public static string remotedesktop_subtitle {
|
||||
get {
|
||||
return ResourceManager.GetString("remotedesktop_subtitle", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Remote Desktop.
|
||||
/// </summary>
|
||||
public static string remotedesktop_title {
|
||||
get {
|
||||
return ResourceManager.GetString("remotedesktop_title", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
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
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<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
|
||||
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
|
||||
mimetype set.
|
||||
|
||||
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
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
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
|
||||
: 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
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="remotedesktop_title" xml:space="preserve">
|
||||
<value>Remote Desktop</value>
|
||||
</data>
|
||||
<data name="remotedesktop_subtitle" xml:space="preserve">
|
||||
<value>Establish Remote Desktop connections</value>
|
||||
</data>
|
||||
<data name="remotedesktop_command_open" xml:space="preserve">
|
||||
<value>Open</value>
|
||||
</data>
|
||||
<data name="remotedesktop_open_host" xml:space="preserve">
|
||||
<value>Connect to {0}</value>
|
||||
</data>
|
||||
<data name="remotedesktop_command_connect" xml:space="preserve">
|
||||
<value>Connect</value>
|
||||
</data>
|
||||
<data name="remotedesktop_open_rdp" xml:space="preserve">
|
||||
<value>Open Remote Desktop Client</value>
|
||||
</data>
|
||||
<data name="remotedesktop_settings_predefined_connections_title" xml:space="preserve">
|
||||
<value>Predefined connections</value>
|
||||
</data>
|
||||
<data name="remotedesktop_settings_predefined_connections_description" xml:space="preserve">
|
||||
<value>A list of connections to include in the query results by default</value>
|
||||
</data>
|
||||
<data name="remotedesktop_log_mstsc_error" xml:space="preserve">
|
||||
<value>Unable to initialize Microsoft Terminal Service Client. Ensure it is enabled in Windows Settings.\r{0}</value>
|
||||
</data>
|
||||
<data name="remotedesktop_log_invalid_hostname" xml:space="preserve">
|
||||
<value>The hostname '{0}' was invalid. Ensure you're using a valid hostname or IP address.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -0,0 +1,45 @@
|
||||
// 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.RemoteDesktop.Commands;
|
||||
using Microsoft.CmdPal.Ext.RemoteDesktop.Helper;
|
||||
using Microsoft.CmdPal.Ext.RemoteDesktop.Pages;
|
||||
using Microsoft.CmdPal.Ext.RemoteDesktop.Properties;
|
||||
using Microsoft.CmdPal.Ext.RemoteDesktop.Settings;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.RemoteDesktop;
|
||||
|
||||
public partial class RemoteDesktopCommandProvider : CommandProvider
|
||||
{
|
||||
private readonly CommandItem listPageCommand;
|
||||
private readonly FallbackRemoteDesktopItem fallback;
|
||||
|
||||
public RemoteDesktopCommandProvider()
|
||||
{
|
||||
Id = "com.microsoft.cmdpal.builtin.remotedesktop";
|
||||
DisplayName = Resources.remotedesktop_title;
|
||||
Icon = Icons.RDPIcon;
|
||||
|
||||
var settingsManager = new SettingsManager();
|
||||
var rdpConnectionsManager = new RdpConnectionsManager(settingsManager);
|
||||
var listPage = new RemoteDesktopListPage(rdpConnectionsManager);
|
||||
|
||||
fallback = new FallbackRemoteDesktopItem(rdpConnectionsManager);
|
||||
|
||||
listPageCommand = new CommandItem(listPage)
|
||||
{
|
||||
Subtitle = Resources.remotedesktop_subtitle,
|
||||
Icon = Icons.RDPIcon,
|
||||
MoreCommands = [
|
||||
new CommandContextItem(settingsManager.Settings.SettingsPage),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
public override ICommandItem[] TopLevelCommands() => [listPageCommand];
|
||||
|
||||
public override IFallbackCommandItem[] FallbackCommands() => [fallback];
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using ToolkitSettings = Microsoft.CommandPalette.Extensions.Toolkit.Settings;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.RemoteDesktop.Settings;
|
||||
|
||||
internal interface ISettingsInterface
|
||||
{
|
||||
public IReadOnlyCollection<string> PredefinedConnections { get; }
|
||||
|
||||
public ToolkitSettings Settings { get; }
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.CmdPal.Ext.RemoteDesktop.Properties;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.RemoteDesktop.Settings;
|
||||
|
||||
internal class SettingsManager : JsonSettingsManager, ISettingsInterface
|
||||
{
|
||||
// Line break character used in WinUI3 TextBox and TextBlock.
|
||||
private const char TEXTBOXNEWLINE = '\r';
|
||||
|
||||
private static readonly string _namespace = "com.microsoft.cmdpal.builtin.remotedesktop";
|
||||
|
||||
private static string Namespaced(string propertyName) => $"{_namespace}.{propertyName}";
|
||||
|
||||
private readonly TextSetting _predefinedConnections = new(
|
||||
Namespaced(nameof(PredefinedConnections)),
|
||||
Resources.remotedesktop_settings_predefined_connections_title,
|
||||
Resources.remotedesktop_settings_predefined_connections_description,
|
||||
string.Empty)
|
||||
{
|
||||
Multiline = true,
|
||||
Placeholder = $"server1.domain.com{TEXTBOXNEWLINE}server2.domain.com{TEXTBOXNEWLINE}192.168.1.1",
|
||||
};
|
||||
|
||||
public IReadOnlyCollection<string> PredefinedConnections => _predefinedConnections.Value?.Split(TEXTBOXNEWLINE).ToList() ?? [];
|
||||
|
||||
internal static string SettingsJsonPath()
|
||||
{
|
||||
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
return Path.Combine(directory, "settings.json");
|
||||
}
|
||||
|
||||
public SettingsManager()
|
||||
{
|
||||
FilePath = SettingsJsonPath();
|
||||
|
||||
Settings.Add(_predefinedConnections);
|
||||
|
||||
// Load settings from file upon initialization
|
||||
LoadSettings();
|
||||
|
||||
Settings.SettingsChanged += (s, a) => this.SaveSettings();
|
||||
}
|
||||
}
|
||||
@@ -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,36 +4,39 @@
|
||||
|
||||
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}"))
|
||||
var uri = BuildUri();
|
||||
|
||||
if (!_browserInfoService.Open(uri))
|
||||
{
|
||||
// TODO GH# 138 --> actually display feedback from the extension somewhere.
|
||||
return CommandResult.KeepOpen();
|
||||
}
|
||||
|
||||
// remember only the query, not the full URI
|
||||
if (_settingsManager.HistoryItemCount != 0)
|
||||
{
|
||||
_settingsManager.AddHistoryItem(new HistoryItem(Arguments, DateTime.Now));
|
||||
@@ -41,4 +44,28 @@ internal sealed partial class SearchWebCommand : InvokableCommand
|
||||
|
||||
return CommandResult.Dismiss();
|
||||
}
|
||||
|
||||
private string BuildUri()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_settingsManager.CustomSearchUri))
|
||||
{
|
||||
return $"? " + Arguments;
|
||||
}
|
||||
|
||||
// if the custom search URI contains query placeholder, replace it with the actual query
|
||||
// otherwise append the query to the end of the URI
|
||||
// support {query}, %query% or %s as placeholder
|
||||
var placeholderVariants = new[] { "{query}", "%query%", "%s" };
|
||||
foreach (var placeholder in placeholderVariants)
|
||||
{
|
||||
if (_settingsManager.CustomSearchUri.Contains(placeholder, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return _settingsManager.CustomSearchUri.Replace(placeholder, Uri.EscapeDataString(Arguments), StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
// is this too smart?
|
||||
var separator = _settingsManager.CustomSearchUri.Contains('?') ? '&' : '?';
|
||||
return $"{_settingsManager.CustomSearchUri}{separator}q={Uri.EscapeDataString(Arguments)}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,5 +18,7 @@ public interface ISettingsInterface
|
||||
|
||||
public IReadOnlyList<HistoryItem> HistoryItems { get; }
|
||||
|
||||
string CustomSearchUri { get; }
|
||||
|
||||
public void AddHistoryItem(HistoryItem historyItem);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,15 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
|
||||
Resources.plugin_global_if_uri,
|
||||
false);
|
||||
|
||||
private readonly TextSetting _customSearchUri = new(
|
||||
Namespaced(nameof(CustomSearchUri)),
|
||||
Resources.plugin_custom_search_uri,
|
||||
Resources.plugin_custom_search_uri,
|
||||
string.Empty)
|
||||
{
|
||||
Placeholder = Resources.plugin_custom_search_uri_placeholder,
|
||||
};
|
||||
|
||||
private readonly ChoiceSetSetting _historyItemCount = new(
|
||||
Namespaced(HistoryItemCountLegacySettingsKey),
|
||||
Resources.plugin_history_item_count,
|
||||
@@ -51,6 +60,8 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
|
||||
|
||||
public int HistoryItemCount => int.TryParse(_historyItemCount.Value, out var value) && value >= 0 ? value : 0;
|
||||
|
||||
public string CustomSearchUri => _customSearchUri.Value ?? string.Empty;
|
||||
|
||||
public IReadOnlyList<HistoryItem> HistoryItems => _history.HistoryItems;
|
||||
|
||||
public SettingsManager()
|
||||
@@ -59,6 +70,7 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
|
||||
|
||||
Settings.Add(_globalIfURI);
|
||||
Settings.Add(_historyItemCount);
|
||||
Settings.Add(_customSearchUri);
|
||||
|
||||
LoadSettings();
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -150,6 +159,24 @@ namespace Microsoft.CmdPal.Ext.WebSearch.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Custom search engine URL.
|
||||
/// </summary>
|
||||
public static string plugin_custom_search_uri {
|
||||
get {
|
||||
return ResourceManager.GetString("plugin_custom_search_uri", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Use {query} or %s as the search query placeholder; e.g. https://www.bing.com/search?q={query}.
|
||||
/// </summary>
|
||||
public static string plugin_custom_search_uri_placeholder {
|
||||
get {
|
||||
return ResourceManager.GetString("plugin_custom_search_uri_placeholder", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Searches the web with your default search engine.
|
||||
/// </summary>
|
||||
|
||||
@@ -184,4 +184,13 @@
|
||||
<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>
|
||||
<data name="plugin_custom_search_uri" xml:space="preserve">
|
||||
<value>Custom search engine URL</value>
|
||||
</data>
|
||||
<data name="plugin_custom_search_uri_placeholder" xml:space="preserve">
|
||||
<value>Use {query} or %s as the search query placeholder; e.g. https://www.bing.com/search?q={query}</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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,13 +9,6 @@ namespace SamplePagesExtension;
|
||||
|
||||
internal sealed partial class SampleGalleryListPage : ListPage
|
||||
{
|
||||
public SampleGalleryListPage()
|
||||
{
|
||||
Icon = new IconInfo("\uE7C5");
|
||||
Name = "Sample Gallery List Page";
|
||||
GridProperties = new GalleryGridLayout();
|
||||
}
|
||||
|
||||
public override IListItem[] GetItems()
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
// 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;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace SamplePagesExtension;
|
||||
|
||||
internal sealed partial class SampleGridsListPage : ListPage
|
||||
{
|
||||
private readonly IListItem[] _items =
|
||||
[
|
||||
new ListItem(new SampleGalleryListPage { GridProperties = new GalleryGridLayout { ShowTitle = true, ShowSubtitle = true } })
|
||||
{
|
||||
Title = "Gallery list page (title and subtitle)",
|
||||
Subtitle = "A sample gallery list page with images",
|
||||
Icon = IconHelpers.FromRelativePath("Assets/Images/Swirls.png"),
|
||||
},
|
||||
new ListItem(new SampleGalleryListPage { GridProperties = new GalleryGridLayout { ShowTitle = true, ShowSubtitle = false } })
|
||||
{
|
||||
Title = "Gallery list page (title, no subtitle)",
|
||||
Subtitle = "A sample gallery list page with images",
|
||||
Icon = IconHelpers.FromRelativePath("Assets/Images/Swirls.png"),
|
||||
},
|
||||
new ListItem(new SampleGalleryListPage { GridProperties = new GalleryGridLayout { ShowTitle = false, ShowSubtitle = false } })
|
||||
{
|
||||
Title = "Gallery list page (no title, no subtitle)",
|
||||
Subtitle = "A sample gallery list page with images",
|
||||
Icon = IconHelpers.FromRelativePath("Assets/Images/Swirls.png"),
|
||||
},
|
||||
new ListItem(new SampleGalleryListPage { GridProperties = new SmallGridLayout() })
|
||||
{
|
||||
Title = "Small grid list page",
|
||||
Subtitle = "A sample grid list page with text items",
|
||||
Icon = IconHelpers.FromRelativePath("Assets/Images/Win-Digital.png"),
|
||||
},
|
||||
new ListItem(new SampleGalleryListPage { GridProperties = new MediumGridLayout { ShowTitle = true } })
|
||||
{
|
||||
Title = "Medium grid (with title)",
|
||||
Subtitle = "A sample grid list page with text items",
|
||||
Icon = IconHelpers.FromRelativePath("Assets/Images/Win-Digital.png"),
|
||||
},
|
||||
new ListItem(new SampleGalleryListPage { GridProperties = new MediumGridLayout { ShowTitle = false } })
|
||||
{
|
||||
Title = "Medium grid (hidden title)",
|
||||
Subtitle = "A sample grid list page with text items",
|
||||
Icon = IconHelpers.FromRelativePath("Assets/Images/Win-Digital.png"),
|
||||
}
|
||||
];
|
||||
|
||||
public SampleGridsListPage()
|
||||
{
|
||||
Icon = new IconInfo("\uE7C5");
|
||||
Name = "Grid and gallery lists";
|
||||
}
|
||||
|
||||
public override IListItem[] GetItems() => _items;
|
||||
}
|
||||
@@ -34,9 +34,9 @@ public partial class SamplesListPage : ListPage
|
||||
Title = "Dynamic List Page Command",
|
||||
Subtitle = "Changes the list of items in response to the typed query",
|
||||
},
|
||||
new ListItem(new SampleGalleryListPage())
|
||||
new ListItem(new SampleGridsListPage())
|
||||
{
|
||||
Title = "Gallery List Page Command",
|
||||
Title = "Grid views and galleries",
|
||||
Subtitle = "Displays items as a gallery",
|
||||
},
|
||||
new ListItem(new OnLoadPage())
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user