mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-01-08 21:36:54 +01:00
Compare commits
2 Commits
yuleng/ima
...
dev/vanzue
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b568e93dc | ||
|
|
4b1e0f6220 |
7
.github/actions/spell-check/allow/code.txt
vendored
7
.github/actions/spell-check/allow/code.txt
vendored
@@ -321,10 +321,3 @@ REGSTR
|
||||
|
||||
# Misc Win32 APIs and PInvokes
|
||||
INVOKEIDLIST
|
||||
|
||||
# PowerRename metadata pattern abbreviations (used in tests and regex patterns)
|
||||
DDDD
|
||||
FFF
|
||||
HHH
|
||||
riday
|
||||
YYY
|
||||
|
||||
24
.github/actions/spell-check/expect.txt
vendored
24
.github/actions/spell-check/expect.txt
vendored
@@ -97,7 +97,6 @@ atl
|
||||
ATX
|
||||
ATRIOX
|
||||
aumid
|
||||
authenticode
|
||||
Authenticode
|
||||
AUTOBUDDY
|
||||
AUTOCHECKBOX
|
||||
@@ -143,7 +142,6 @@ bmi
|
||||
BNumber
|
||||
BODGY
|
||||
BOklab
|
||||
Bootstrappers
|
||||
BOOTSTRAPPERINSTALLFOLDER
|
||||
BOTTOMALIGN
|
||||
boxmodel
|
||||
@@ -171,12 +169,9 @@ BYPOSITION
|
||||
CALCRECT
|
||||
CALG
|
||||
callbackptr
|
||||
cabstr
|
||||
calpwstr
|
||||
caub
|
||||
Cangjie
|
||||
CANRENAME
|
||||
Carlseibert
|
||||
Canvascustomlayout
|
||||
CAPTUREBLT
|
||||
CAPTURECHANGED
|
||||
@@ -282,7 +277,6 @@ cpptools
|
||||
cppvsdbg
|
||||
cppwinrt
|
||||
createdump
|
||||
creativecommons
|
||||
CREATEPROCESS
|
||||
CREATESCHEDULEDTASK
|
||||
CREATESTRUCT
|
||||
@@ -345,7 +339,6 @@ Deact
|
||||
debugbreak
|
||||
decryptor
|
||||
Dedup
|
||||
dfx
|
||||
Deduplicator
|
||||
Deeplink
|
||||
DEFAULTBOOTSTRAPPERINSTALLFOLDER
|
||||
@@ -519,7 +512,6 @@ EXTRINSICPROPERTIES
|
||||
eyetracker
|
||||
FANCYZONESDRAWLAYOUTTEST
|
||||
FANCYZONESEDITOR
|
||||
FNumber
|
||||
FARPROC
|
||||
fdx
|
||||
fesf
|
||||
@@ -703,7 +695,6 @@ HTCLIENT
|
||||
hthumbnail
|
||||
HTOUCHINPUT
|
||||
HTTRANSPARENT
|
||||
hutchinsoniana
|
||||
HVal
|
||||
HValue
|
||||
Hvci
|
||||
@@ -725,9 +716,7 @@ IDCANCEL
|
||||
IDD
|
||||
idk
|
||||
idl
|
||||
IIM
|
||||
idlist
|
||||
ifd
|
||||
IDOK
|
||||
IDOn
|
||||
IDR
|
||||
@@ -744,7 +733,6 @@ Ijwhost
|
||||
ILD
|
||||
IMAGEHLP
|
||||
IMAGERESIZERCONTEXTMENU
|
||||
IPTC
|
||||
IMAGERESIZEREXT
|
||||
imageresizerinput
|
||||
imageresizersettings
|
||||
@@ -884,7 +872,6 @@ LOCKTYPE
|
||||
LOGFONT
|
||||
LOGFONTW
|
||||
logon
|
||||
lon
|
||||
LOGMSG
|
||||
LOGPIXELSX
|
||||
LOGPIXELSY
|
||||
@@ -976,7 +963,6 @@ MENUITEMINFOW
|
||||
MERGECOPY
|
||||
MERGEPAINT
|
||||
Metadatas
|
||||
metadatamatters
|
||||
metafile
|
||||
mfc
|
||||
Mgmt
|
||||
@@ -1044,7 +1030,6 @@ msiexec
|
||||
MSIFASTINSTALL
|
||||
MSIHANDLE
|
||||
MSIRESTARTMANAGERCONTROL
|
||||
MSIs
|
||||
msixbundle
|
||||
MSIXCA
|
||||
MSLLHOOKSTRUCT
|
||||
@@ -1136,7 +1121,6 @@ NONCLIENTMETRICSW
|
||||
NONELEVATED
|
||||
nonspace
|
||||
nonstd
|
||||
nullrefs
|
||||
NOOWNERZORDER
|
||||
NOPARENTNOTIFY
|
||||
NOPREFIX
|
||||
@@ -1293,7 +1277,6 @@ pnid
|
||||
PNMLINK
|
||||
Poc
|
||||
Podcasts
|
||||
Photoshop
|
||||
POINTERID
|
||||
POINTERUPDATE
|
||||
Pokedex
|
||||
@@ -1496,7 +1479,6 @@ sacl
|
||||
safeprojectname
|
||||
SAMEKEYPREVIOUSLYMAPPED
|
||||
SAMESHORTCUTPREVIOUSLYMAPPED
|
||||
samsung
|
||||
sancov
|
||||
SAVEFAILED
|
||||
scanled
|
||||
@@ -1859,7 +1841,6 @@ USEINSTALLERFORTEST
|
||||
USESHOWWINDOW
|
||||
USESTDHANDLES
|
||||
USRDLL
|
||||
utm
|
||||
UType
|
||||
uuidv
|
||||
uwp
|
||||
@@ -1949,7 +1930,6 @@ wgpocpl
|
||||
WHEREID
|
||||
wic
|
||||
wifi
|
||||
wikimedia
|
||||
wikipedia
|
||||
WIL
|
||||
winapi
|
||||
@@ -2044,9 +2024,7 @@ XAxis
|
||||
XButton
|
||||
xclip
|
||||
xcopy
|
||||
xap
|
||||
XDeployment
|
||||
XDimension
|
||||
xdf
|
||||
XDocument
|
||||
XElement
|
||||
@@ -2064,7 +2042,6 @@ xsi
|
||||
XSpeed
|
||||
XStr
|
||||
xstyler
|
||||
xmp
|
||||
XTimer
|
||||
XUP
|
||||
XVIRTUALSCREEN
|
||||
@@ -2072,7 +2049,6 @@ xxxxxx
|
||||
YAxis
|
||||
ycombinator
|
||||
YIncrement
|
||||
YDimension
|
||||
yinle
|
||||
yinyue
|
||||
YPels
|
||||
|
||||
53
.pipelines/ESRPSigning_installer.json
Normal file
53
.pipelines/ESRPSigning_installer.json
Normal file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"Version": "1.0.0",
|
||||
"UseMinimatch": false,
|
||||
"SignBatches": [
|
||||
{
|
||||
"MatchedPath": [
|
||||
"PowerToysSetupCustomActionsVNext.dll",
|
||||
"SilentFilesInUseBAFunction.dll",
|
||||
"PowerToys*Setup-*.exe",
|
||||
"PowerToys*Setup-*.msi"
|
||||
],
|
||||
"SigningInfo": {
|
||||
"Operations": [
|
||||
{
|
||||
"KeyCode": "CP-230012",
|
||||
"OperationSetCode": "SigntoolSign",
|
||||
"Parameters": [
|
||||
{
|
||||
"parameterName": "OpusName",
|
||||
"parameterValue": "Microsoft"
|
||||
},
|
||||
{
|
||||
"parameterName": "OpusInfo",
|
||||
"parameterValue": "http://www.microsoft.com"
|
||||
},
|
||||
{
|
||||
"parameterName": "FileDigest",
|
||||
"parameterValue": "/fd \"SHA256\""
|
||||
},
|
||||
{
|
||||
"parameterName": "PageHash",
|
||||
"parameterValue": "/NPH"
|
||||
},
|
||||
{
|
||||
"parameterName": "TimeStamp",
|
||||
"parameterValue": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256"
|
||||
}
|
||||
],
|
||||
"ToolName": "sign",
|
||||
"ToolVersion": "1.0"
|
||||
},
|
||||
{
|
||||
"KeyCode": "CP-230012",
|
||||
"OperationSetCode": "SigntoolVerify",
|
||||
"Parameters": [],
|
||||
"ToolName": "sign",
|
||||
"ToolVersion": "1.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -512,6 +512,14 @@ jobs:
|
||||
versionNumber: ${{ parameters.versionNumber }}
|
||||
additionalBuildOptions: ${{ parameters.additionalBuildOptions }}
|
||||
|
||||
- template: steps-build-installer-vnext.yml
|
||||
parameters:
|
||||
codeSign: ${{ parameters.codeSign }}
|
||||
signingIdentity: ${{ parameters.signingIdentity }}
|
||||
versionNumber: ${{ parameters.versionNumber }}
|
||||
additionalBuildOptions: ${{ parameters.additionalBuildOptions }}
|
||||
buildUserInstaller: true # NOTE: This is the distinction between the above and below rules
|
||||
|
||||
# This saves ~1GiB per architecture. We won't need these later.
|
||||
# Removes:
|
||||
# - All .pdb files from any static libs .libs (which were only used during linking)
|
||||
|
||||
@@ -2,6 +2,9 @@ parameters:
|
||||
- name: versionNumber
|
||||
type: string
|
||||
default: "0.0.1"
|
||||
- name: buildUserInstaller
|
||||
type: boolean
|
||||
default: false
|
||||
- name: codeSign
|
||||
type: boolean
|
||||
default: false
|
||||
@@ -22,26 +25,43 @@ steps:
|
||||
arguments: 'install --global wix --version 5.0.2'
|
||||
|
||||
- pwsh: |-
|
||||
Write-Host "##vso[task.setvariable variable=InstallerMachineRoot]installer\PowerToysSetupVNext\$(BuildPlatform)\$(BuildConfiguration)\MachineSetup"
|
||||
Write-Host "##vso[task.setvariable variable=InstallerUserRoot]installer\PowerToysSetupVNext\$(BuildPlatform)\$(BuildConfiguration)\UserSetup"
|
||||
Write-Host "##vso[task.setvariable variable=InstallerMachineBasename]PowerToysSetup-${{ parameters.versionNumber }}-$(BuildPlatform)"
|
||||
Write-Host "##vso[task.setvariable variable=InstallerUserBasename]PowerToysUserSetup-${{ parameters.versionNumber }}-$(BuildPlatform)"
|
||||
displayName: Prepare Installer variables
|
||||
& git clean -xfd -e *exe -- .\installer\
|
||||
displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Clean installer to reduce cross-contamination
|
||||
|
||||
- pwsh: |-
|
||||
# Determine whether this is a per-user build
|
||||
$IsPerUser = $${{ parameters.buildUserInstaller }}
|
||||
|
||||
# Build slug used to locate the artifacts
|
||||
$InstallerBuildSlug = if ($IsPerUser) { 'UserSetup' } else { 'MachineSetup' }
|
||||
|
||||
# VNext bundle folder; base name intentionally omits the VNext suffix
|
||||
$InstallerFolder = 'PowerToysSetupVNext'
|
||||
if ($IsPerUser) {
|
||||
$InstallerBasename = "PowerToysUserSetup-${{ parameters.versionNumber }}-$(BuildPlatform)"
|
||||
}
|
||||
else {
|
||||
$InstallerBasename = "PowerToysSetup-${{ parameters.versionNumber }}-$(BuildPlatform)"
|
||||
}
|
||||
|
||||
# Export variables for downstream steps
|
||||
Write-Host "##vso[task.setvariable variable=InstallerBuildSlug]$InstallerBuildSlug"
|
||||
Write-Host "##vso[task.setvariable variable=InstallerRelativePath]$(BuildPlatform)\$(BuildConfiguration)\$InstallerBuildSlug"
|
||||
Write-Host "##vso[task.setvariable variable=InstallerBasename]$InstallerBasename"
|
||||
Write-Host "##vso[task.setvariable variable=InstallerFolder]$InstallerFolder"
|
||||
displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Prepare Installer variables
|
||||
|
||||
# This dll needs to be built and signed before building the MSI.
|
||||
# The Custom Actions project contains a pre-build event that prepares the .wxs files
|
||||
# by filling them out with all our components. We pass RunBuildEvents=true to force
|
||||
# that logic to run.
|
||||
- task: VSBuild@1
|
||||
displayName: Build Shared Support DLLs
|
||||
displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Build PowerToysSetupCustomActionsVNext
|
||||
inputs:
|
||||
solution: "**/installer/PowerToysSetup.sln"
|
||||
vsVersion: 17.0
|
||||
msbuildArgs: >-
|
||||
/t:PowerToysSetupCustomActionsVNext;SilentFilesInUseBAFunction
|
||||
/p:RunBuildEvents=true;RestorePackagesConfig=true;CIBuild=true
|
||||
/t:PowerToysSetupCustomActionsVNext
|
||||
/p:RunBuildEvents=true;PerUser=${{parameters.buildUserInstaller}};RestorePackagesConfig=true;CIBuild=true
|
||||
-restore -graph
|
||||
/bl:$(LogOutputDirectory)\installer-actions.binlog
|
||||
/bl:$(LogOutputDirectory)\installer-$(InstallerBuildSlug)-actions.binlog
|
||||
${{ parameters.additionalBuildOptions }}
|
||||
platform: $(BuildPlatform)
|
||||
configuration: $(BuildConfiguration)
|
||||
@@ -50,53 +70,28 @@ steps:
|
||||
maximumCpuCount: true
|
||||
|
||||
- ${{ if eq(parameters.codeSign, true) }}:
|
||||
- template: steps-esrp-sign-files-authenticode.yml
|
||||
- template: steps-esrp-signing.yml
|
||||
parameters:
|
||||
displayName: Sign Shared Support DLLs
|
||||
displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Sign PowerToysSetupCustomActionsVNext
|
||||
signingIdentity: ${{ parameters.signingIdentity }}
|
||||
folder: 'installer'
|
||||
pattern: |-
|
||||
**/PowerToysSetupCustomActionsVNext.dll
|
||||
**/SilentFilesInUseBAFunction.dll
|
||||
inputs:
|
||||
FolderPath: 'installer/PowerToysSetupCustomActionsVNext/$(InstallerRelativePath)'
|
||||
signType: batchSigning
|
||||
batchSignPolicyFile: '$(build.sourcesdirectory)\.pipelines\ESRPSigning_installer.json'
|
||||
ciPolicyFile: '$(build.sourcesdirectory)\.pipelines\CIPolicy.xml'
|
||||
|
||||
## INSTALLER START
|
||||
#### MSI BUILDING AND SIGNING
|
||||
#
|
||||
# The MSI build contains code that reverts the .wxs files to their in-tree versions.
|
||||
# This is only supposed to happen during local builds. Since this build system is
|
||||
# supposed to run side by side--machine and then user--we do NOT want to destroy
|
||||
# the .wxs files. Therefore, we pass RunBuildEvents=false to suppress all of that
|
||||
# logic.
|
||||
#
|
||||
# We pass BuildProjectReferences=false so that it does not recompile the DLLs we just built.
|
||||
# We only pass -restore on the first one because the second run should already have all
|
||||
# of the dependencies.
|
||||
- task: VSBuild@1
|
||||
displayName: 💻 Build VNext MSI
|
||||
displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Build VNext MSI
|
||||
inputs:
|
||||
solution: "**/installer/PowerToysSetup.sln"
|
||||
vsVersion: 17.0
|
||||
msbuildArgs: >-
|
||||
-restore
|
||||
/t:PowerToysInstallerVNext
|
||||
/p:RunBuildEvents=false;PerUser=false;BuildProjectReferences=false;CIBuild=true
|
||||
/bl:$(LogOutputDirectory)\installer-machine-msi.binlog
|
||||
${{ parameters.additionalBuildOptions }}
|
||||
platform: $(BuildPlatform)
|
||||
configuration: $(BuildConfiguration)
|
||||
clean: false # don't undo our hard work above by deleting the CustomActions dll
|
||||
msbuildArchitecture: x64
|
||||
maximumCpuCount: true
|
||||
|
||||
- task: VSBuild@1
|
||||
displayName: 👤 Build VNext MSI
|
||||
inputs:
|
||||
solution: "**/installer/PowerToysSetup.sln"
|
||||
vsVersion: 17.0
|
||||
msbuildArgs: >-
|
||||
/t:PowerToysInstallerVNext
|
||||
/p:RunBuildEvents=false;PerUser=true;BuildProjectReferences=false;CIBuild=true
|
||||
/bl:$(LogOutputDirectory)\installer-user-msi.binlog
|
||||
/p:RunBuildEvents=false;PerUser=${{parameters.buildUserInstaller}};BuildProjectReferences=false;CIBuild=true
|
||||
/bl:$(LogOutputDirectory)\installer-$(InstallerBuildSlug)-msi.binlog
|
||||
${{ parameters.additionalBuildOptions }}
|
||||
platform: $(BuildPlatform)
|
||||
configuration: $(BuildConfiguration)
|
||||
@@ -105,66 +100,77 @@ steps:
|
||||
maximumCpuCount: true
|
||||
|
||||
- script: |-
|
||||
wix msi decompile $(InstallerMachineRoot)\$(InstallerMachineBasename).msi -x $(build.sourcesdirectory)\extractedMachineMsi
|
||||
wix msi decompile $(InstallerUserRoot)\$(InstallerUserBasename).msi -x $(build.sourcesdirectory)\extractedUserMsi
|
||||
dir $(build.sourcesdirectory)\extractedMachineMsi
|
||||
dir $(build.sourcesdirectory)\extractedUserMsi
|
||||
displayName: "WiX5: Extract and verify MSIs"
|
||||
wix msi decompile installer\$(InstallerFolder)\$(InstallerRelativePath)\$(InstallerBasename).msi -x $(build.sourcesdirectory)\extractedMsi
|
||||
dir $(build.sourcesdirectory)\extractedMsi
|
||||
displayName: "${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} WiX5: Extract and verify MSI"
|
||||
|
||||
# Check if deps.json files don't reference different dll versions.
|
||||
- pwsh: |-
|
||||
& '.pipelines/verifyDepsJsonLibraryVersions.ps1' -targetDir '$(build.sourcesdirectory)\extractedMachineMsi\File'
|
||||
& '.pipelines/verifyDepsJsonLibraryVersions.ps1' -targetDir '$(build.sourcesdirectory)\extractedUserMsi\File'
|
||||
displayName: Audit deps.json in MSI extracted files
|
||||
& '.pipelines/verifyDepsJsonLibraryVersions.ps1' -targetDir '$(build.sourcesdirectory)\extractedMsi\File'
|
||||
displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Audit deps.json in MSI extracted files
|
||||
|
||||
- ${{ if eq(parameters.codeSign, true) }}:
|
||||
- pwsh: |-
|
||||
& .pipelines/versionAndSignCheck.ps1 -targetDir '$(build.sourcesdirectory)\extractedMachineMsi\File'
|
||||
& .pipelines/versionAndSignCheck.ps1 -targetDir '$(build.sourcesdirectory)\extractedMachineMsi\Binary'
|
||||
& .pipelines/versionAndSignCheck.ps1 -targetDir '$(build.sourcesdirectory)\extractedUserMsi\File'
|
||||
& .pipelines/versionAndSignCheck.ps1 -targetDir '$(build.sourcesdirectory)\extractedUserMsi\Binary'
|
||||
git clean -xfd ./extractedMachineMsi ./extractedUserMsi
|
||||
displayName: Verify all binaries are signed and versioned
|
||||
& .pipelines/versionAndSignCheck.ps1 -targetDir '$(build.sourcesdirectory)\extractedMsi\File'
|
||||
& .pipelines/versionAndSignCheck.ps1 -targetDir '$(build.sourcesdirectory)\extractedMsi\Binary'
|
||||
git clean -xfd ./extractedMsi
|
||||
displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Verify all binaries are signed and versioned
|
||||
|
||||
- template: steps-esrp-sign-files-authenticode.yml
|
||||
- template: steps-esrp-signing.yml
|
||||
parameters:
|
||||
displayName: Sign VNext MSIs
|
||||
displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Sign VNext MSI
|
||||
signingIdentity: ${{ parameters.signingIdentity }}
|
||||
folder: 'installer'
|
||||
pattern: '**/PowerToys*Setup-*.msi'
|
||||
inputs:
|
||||
FolderPath: 'installer/$(InstallerFolder)/$(InstallerRelativePath)'
|
||||
signType: batchSigning
|
||||
batchSignPolicyFile: '$(build.sourcesdirectory)\.pipelines\ESRPSigning_installer.json'
|
||||
ciPolicyFile: '$(build.sourcesdirectory)\.pipelines\CIPolicy.xml'
|
||||
|
||||
#### END MSI
|
||||
|
||||
#### BOOTSTRAP BUILDING AND SIGNING
|
||||
# We pass BuildProjectReferences=false so that it does not recompile the DLLs we just built.
|
||||
# We only pass -restore on the first one because the second run should already have all
|
||||
# of the dependencies.
|
||||
#### BUILDING AND SIGNING SilentFilesInUseBAFunction DLL
|
||||
- task: VSBuild@1
|
||||
displayName: 💻 Build VNext Bootstrapper
|
||||
displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Build SilentFilesInUseBAFunction
|
||||
inputs:
|
||||
solution: "**/installer/PowerToysSetup.sln"
|
||||
vsVersion: 17.0
|
||||
msbuildArgs: >-
|
||||
/t:SilentFilesInUseBAFunction
|
||||
/p:RunBuildEvents=true;PerUser=${{parameters.buildUserInstaller}};RestorePackagesConfig=true;CIBuild=true
|
||||
-restore -graph
|
||||
/bl:$(LogOutputDirectory)\installer-$(InstallerBuildSlug)-SilentFilesInUseBAFunction.binlog
|
||||
${{ parameters.additionalBuildOptions }}
|
||||
platform: $(BuildPlatform)
|
||||
configuration: $(BuildConfiguration)
|
||||
clean: false # don't undo our hard work above by deleting the msi
|
||||
msbuildArchitecture: x64
|
||||
maximumCpuCount: true
|
||||
|
||||
- ${{ if eq(parameters.codeSign, true) }}:
|
||||
- template: steps-esrp-signing.yml
|
||||
parameters:
|
||||
displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Sign SilentFilesInUseBAFunction
|
||||
signingIdentity: ${{ parameters.signingIdentity }}
|
||||
inputs:
|
||||
FolderPath: 'installer/$(BuildPlatform)/$(BuildConfiguration)'
|
||||
signType: batchSigning
|
||||
batchSignPolicyFile: '$(build.sourcesdirectory)\.pipelines\ESRPSigning_installer.json'
|
||||
ciPolicyFile: '$(build.sourcesdirectory)\.pipelines\CIPolicy.xml'
|
||||
|
||||
#### END BUILDING AND SIGNING SilentFilesInUseBAFunction DLL
|
||||
|
||||
#### BOOTSTRAP BUILDING AND SIGNING
|
||||
- task: VSBuild@1
|
||||
displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Build VNext Bootstrapper
|
||||
inputs:
|
||||
solution: "**/installer/PowerToysSetup.sln"
|
||||
vsVersion: 17.0
|
||||
msbuildArgs: >-
|
||||
-restore
|
||||
/t:PowerToysBootstrapperVNext
|
||||
/p:PerUser=false;BuildProjectReferences=false;CIBuild=true
|
||||
/bl:$(LogOutputDirectory)\installer-machine-bootstrapper.binlog
|
||||
${{ parameters.additionalBuildOptions }}
|
||||
platform: $(BuildPlatform)
|
||||
configuration: $(BuildConfiguration)
|
||||
clean: false # don't undo our hard work above by deleting the MSI nor SilentFilesInUseBAFunction
|
||||
msbuildArchitecture: x64
|
||||
maximumCpuCount: true
|
||||
|
||||
- task: VSBuild@1
|
||||
displayName: 👤 Build VNext Bootstrapper
|
||||
inputs:
|
||||
solution: "**/installer/PowerToysSetup.sln"
|
||||
vsVersion: 17.0
|
||||
msbuildArgs: >-
|
||||
/t:PowerToysBootstrapperVNext
|
||||
/p:PerUser=true;BuildProjectReferences=false;CIBuild=true
|
||||
/bl:$(LogOutputDirectory)\installer-user-bootstrapper.binlog
|
||||
/p:PerUser=${{parameters.buildUserInstaller}};CIBuild=true
|
||||
/bl:$(LogOutputDirectory)\installer-$(InstallerBuildSlug)-bootstrapper.binlog
|
||||
-restore -graph
|
||||
${{ parameters.additionalBuildOptions }}
|
||||
platform: $(BuildPlatform)
|
||||
configuration: $(BuildConfiguration)
|
||||
@@ -175,41 +181,54 @@ steps:
|
||||
# The entirety of bundle unpacking/re-packing is unnecessary if we are not code signing it.
|
||||
- ${{ if eq(parameters.codeSign, true) }}:
|
||||
- script: |-
|
||||
wix burn detach $(InstallerMachineRoot)\$(InstallerMachineBasename).exe -engine installer\machine-engine.exe
|
||||
wix burn detach $(InstallerUserRoot)\$(InstallerUserBasename).exe -engine installer\user-engine.exe
|
||||
displayName: "WiX5: Extract Engines from Bundles"
|
||||
wix burn detach installer\$(InstallerFolder)\$(InstallerRelativePath)\$(InstallerBasename).exe -engine installer\engine.exe
|
||||
displayName: "${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} WiX5: Extract Engine from Bundle"
|
||||
|
||||
- template: steps-esrp-sign-files-authenticode.yml
|
||||
- template: steps-esrp-signing.yml
|
||||
parameters:
|
||||
displayName: Sign WiX Engines
|
||||
displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Sign WiX Engine
|
||||
signingIdentity: ${{ parameters.signingIdentity }}
|
||||
folder: "installer"
|
||||
pattern: '*-engine.exe'
|
||||
inputs:
|
||||
FolderPath: "installer"
|
||||
Pattern: engine.exe
|
||||
signConfigType: inlineSignParams
|
||||
inlineOperation: |
|
||||
[
|
||||
{
|
||||
"KeyCode": "CP-230012",
|
||||
"OperationCode": "SigntoolSign",
|
||||
"Parameters": {
|
||||
"OpusName": "Microsoft",
|
||||
"OpusInfo": "http://www.microsoft.com",
|
||||
"FileDigest": "/fd \"SHA256\"",
|
||||
"PageHash": "/NPH",
|
||||
"TimeStamp": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256"
|
||||
},
|
||||
"ToolName": "sign",
|
||||
"ToolVersion": "1.0"
|
||||
},
|
||||
{
|
||||
"KeyCode": "CP-230012",
|
||||
"OperationCode": "SigntoolVerify",
|
||||
"Parameters": {},
|
||||
"ToolName": "sign",
|
||||
"ToolVersion": "1.0"
|
||||
}
|
||||
]
|
||||
|
||||
- script: |-
|
||||
wix burn reattach $(InstallerMachineRoot)\$(InstallerMachineBasename).exe -engine installer\machine-engine.exe -o $(InstallerMachineRoot)\$(InstallerMachineBasename).exe
|
||||
wix burn reattach $(InstallerUserRoot)\$(InstallerUserBasename).exe -engine installer\user-engine.exe -o $(InstallerUserRoot)\$(InstallerUserBasename).exe
|
||||
displayName: "WiX5: Reattach Engines to Bundles"
|
||||
wix burn reattach installer\$(InstallerFolder)\$(InstallerRelativePath)\$(InstallerBasename).exe -engine installer\engine.exe -o installer\$(InstallerFolder)\$(InstallerRelativePath)\$(InstallerBasename).exe
|
||||
displayName: "${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} WiX5: Reattach Engine to Bundle"
|
||||
|
||||
- pwsh: |-
|
||||
& wix burn extract -oba installer\ba\m "$(InstallerMachineRoot)\$(InstallerMachineBasename).exe"
|
||||
& wix burn extract -oba installer\ba\u "$(InstallerUserRoot)\$(InstallerUserBasename).exe"
|
||||
Get-ChildItem installer\ba -Recurse -Include *.exe,*.dll | Get-AuthenticodeSignature | ForEach-Object {
|
||||
If ($_.Status -Ne "Valid") {
|
||||
Write-Error $_.StatusMessage
|
||||
} Else {
|
||||
Write-Host $_.StatusMessage
|
||||
}
|
||||
}
|
||||
& git clean -fdx installer\ba
|
||||
displayName: "WiX5: Verify Bootstrapper content is signed"
|
||||
|
||||
- template: steps-esrp-sign-files-authenticode.yml
|
||||
- template: steps-esrp-signing.yml
|
||||
parameters:
|
||||
displayName: Sign Final Bootstrappers
|
||||
displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Sign Final Bootstrapper
|
||||
signingIdentity: ${{ parameters.signingIdentity }}
|
||||
folder: 'installer'
|
||||
pattern: '**/PowerToys*Setup-*.exe'
|
||||
inputs:
|
||||
FolderPath: 'installer/$(InstallerFolder)/$(InstallerRelativePath)'
|
||||
signType: batchSigning
|
||||
batchSignPolicyFile: '$(build.sourcesdirectory)\.pipelines\ESRPSigning_installer.json'
|
||||
ciPolicyFile: '$(build.sourcesdirectory)\.pipelines\CIPolicy.xml'
|
||||
|
||||
#### END BOOTSTRAP
|
||||
## END INSTALLER
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
parameters:
|
||||
- name: displayName
|
||||
type: string
|
||||
default: Sign Specific Files
|
||||
- name: folder
|
||||
type: string
|
||||
- name: pattern
|
||||
type: string
|
||||
- name: signingIdentity
|
||||
type: object
|
||||
default: {}
|
||||
|
||||
steps:
|
||||
- template: steps-esrp-signing.yml
|
||||
parameters:
|
||||
displayName: ${{ parameters.displayName }}
|
||||
signingIdentity: ${{ parameters.signingIdentity }}
|
||||
inputs:
|
||||
FolderPath: ${{ parameters.folder }}
|
||||
Pattern: ${{ parameters.pattern }}
|
||||
UseMinimatch: true
|
||||
signConfigType: inlineSignParams
|
||||
inlineOperation: |-
|
||||
[
|
||||
{
|
||||
"KeyCode": "CP-230012",
|
||||
"OperationCode": "SigntoolSign",
|
||||
"Parameters": {
|
||||
"OpusName": "Microsoft",
|
||||
"OpusInfo": "http://www.microsoft.com",
|
||||
"FileDigest": "/fd \"SHA256\"",
|
||||
"PageHash": "/NPH",
|
||||
"TimeStamp": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256"
|
||||
},
|
||||
"ToolName": "sign",
|
||||
"ToolVersion": "1.0"
|
||||
},
|
||||
{
|
||||
"KeyCode": "CP-230012",
|
||||
"OperationCode": "SigntoolVerify",
|
||||
"Parameters": {},
|
||||
"ToolName": "sign",
|
||||
"ToolVersion": "1.0"
|
||||
}
|
||||
]
|
||||
@@ -74,6 +74,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerRename.UnitTests", "sr
|
||||
EndProject
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ModuleTemplateCompileTest", "tools\project_template\ModuleTemplate\ModuleTemplateCompileTest.vcxproj", "{64A80062-4D8B-4229-8A38-DFA1D7497749}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ptcli", "tools\ptcli\ptcli.csproj", "{2589570C-B068-41CA-A554-BDCAE6FC4CAC}"
|
||||
EndProject
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "KeyboardManager", "src\modules\keyboardmanager\dll\KeyboardManager.vcxproj", "{89F34AF7-1C34-4A72-AA6E-534BCF972BD9}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "imageresizer", "imageresizer", "{6C7F47CC-2151-44A3-A546-41C70025132C}"
|
||||
@@ -926,6 +928,14 @@ Global
|
||||
{64A80062-4D8B-4229-8A38-DFA1D7497749}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{64A80062-4D8B-4229-8A38-DFA1D7497749}.Release|x64.ActiveCfg = Release|x64
|
||||
{64A80062-4D8B-4229-8A38-DFA1D7497749}.Release|x64.Build.0 = Release|x64
|
||||
{2589570C-B068-41CA-A554-BDCAE6FC4CAC}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{2589570C-B068-41CA-A554-BDCAE6FC4CAC}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{2589570C-B068-41CA-A554-BDCAE6FC4CAC}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{2589570C-B068-41CA-A554-BDCAE6FC4CAC}.Debug|x64.Build.0 = Debug|x64
|
||||
{2589570C-B068-41CA-A554-BDCAE6FC4CAC}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||
{2589570C-B068-41CA-A554-BDCAE6FC4CAC}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{2589570C-B068-41CA-A554-BDCAE6FC4CAC}.Release|x64.ActiveCfg = Release|x64
|
||||
{2589570C-B068-41CA-A554-BDCAE6FC4CAC}.Release|x64.Build.0 = Release|x64
|
||||
{89F34AF7-1C34-4A72-AA6E-534BCF972BD9}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{89F34AF7-1C34-4A72-AA6E-534BCF972BD9}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{89F34AF7-1C34-4A72-AA6E-534BCF972BD9}.Debug|x64.ActiveCfg = Debug|x64
|
||||
|
||||
@@ -34,8 +34,12 @@
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<PropertyGroup>
|
||||
<OutDir>$(Platform)\$(Configuration)\SetupShared\</OutDir>
|
||||
<IntDir>$(SolutionDir)$(ProjectName)\$(Platform)\$(Configuration)\SetupShared\obj\</IntDir>
|
||||
<OutDir Condition=" '$(PerUser)' != 'true' ">$(Platform)\$(Configuration)\MachineSetup\</OutDir>
|
||||
<OutDir Condition=" '$(PerUser)' == 'true' ">$(Platform)\$(Configuration)\UserSetup\</OutDir>
|
||||
<IntDir Condition=" '$(PerUser)' != 'true' ">$(SolutionDir)$(ProjectName)\$(Platform)\$(Configuration)\MachineSetup\obj\</IntDir>
|
||||
<IntDir Condition=" '$(PerUser)' == 'true' ">$(SolutionDir)$(ProjectName)\$(Platform)\$(Configuration)\UserSetup\obj\</IntDir>
|
||||
<NormalizedPerUserValue>false</NormalizedPerUserValue>
|
||||
<NormalizedPerUserValue Condition=" '$(PerUser)' == 'true' ">true</NormalizedPerUserValue>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
|
||||
<LinkIncremental>true</LinkIncremental>
|
||||
@@ -75,7 +79,8 @@
|
||||
call cmd /C "copy ""$(ProjectDir)..\PowerToysSetupVNext\WinAppSDK.wxs"" ""$(ProjectDir)..\PowerToysSetupVNext\WinAppSDK.wxs.bk""""
|
||||
call cmd /C "copy ""$(ProjectDir)..\PowerToysSetupVNext\WinUI3Applications.wxs"" ""$(ProjectDir)..\PowerToysSetupVNext\WinUI3Applications.wxs.bk""""
|
||||
call cmd /C "copy ""$(ProjectDir)..\PowerToysSetupVNext\Workspaces.wxs"" ""$(ProjectDir)..\PowerToysSetupVNext\Workspaces.wxs.bk""""
|
||||
call powershell.exe -NonInteractive -executionpolicy Unrestricted -File ..\PowerToysSetupVNext\generateAllFileComponents.ps1 -platform $(Platform)
|
||||
if not "$(NormalizedPerUserValue)" == "true" call powershell.exe -NonInteractive -executionpolicy Unrestricted -File ..\PowerToysSetupVNext\generateAllFileComponents.ps1 -platform $(Platform)
|
||||
if "$(NormalizedPerUserValue)" == "true" call powershell.exe -NonInteractive -executionpolicy Unrestricted -File ..\PowerToysSetupVNext\generateAllFileComponents.ps1 -platform $(Platform) -installscopeperuser $(NormalizedPerUserValue)
|
||||
</Command>
|
||||
<Message>Backing up original files and populating .NET and WPF Runtime dependencies for WiX3 based installer</Message>
|
||||
</PreBuildEvent>
|
||||
@@ -173,4 +178,4 @@
|
||||
<Error Condition="!Exists('..\packages\WixToolset.DUtil.5.0.2\build\WixToolset.DUtil.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\WixToolset.DUtil.5.0.2\build\WixToolset.DUtil.props'))" />
|
||||
<Error Condition="!Exists('..\packages\WixToolset.WcaUtil.5.0.2\build\WixToolset.WcaUtil.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\WixToolset.WcaUtil.5.0.2\build\WixToolset.WcaUtil.props'))" />
|
||||
</Target>
|
||||
</Project>
|
||||
</Project>
|
||||
@@ -60,12 +60,6 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil
|
||||
call move /Y ..\..\..\Workspaces.wxs.bk ..\..\..\Workspaces.wxs
|
||||
</PostBuildEvent>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(RunBuildEvents)'=='false'">
|
||||
<PostBuildEvent></PostBuildEvent>
|
||||
<RunPostBuildEvent></RunPostBuildEvent>
|
||||
<PreBuildEventUseInBuild>false</PreBuildEventUseInBuild>
|
||||
<PostBuildEventUseInBuild>false</PostBuildEventUseInBuild>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Label="UserMacros" Condition=" '$(PerUser)' == 'true' ">
|
||||
<DefineConstants>$(DefineConstants);PerUser=true</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
[CmdletBinding()]
|
||||
Param(
|
||||
[Parameter(Mandatory = $True, Position = 1)]
|
||||
[string]$platform
|
||||
[string]$platform,
|
||||
[Parameter(Mandatory = $False, Position = 2)]
|
||||
[string]$installscopeperuser = "false"
|
||||
)
|
||||
|
||||
Function Generate-FileList() {
|
||||
@@ -75,7 +77,9 @@ Function Generate-FileComponents() {
|
||||
[Parameter(Mandatory = $True, Position = 1)]
|
||||
[string]$fileListName,
|
||||
[Parameter(Mandatory = $True, Position = 2)]
|
||||
[string]$wxsFilePath
|
||||
[string]$wxsFilePath,
|
||||
[Parameter(Mandatory = $True, Position = 3)]
|
||||
[string]$regroot
|
||||
)
|
||||
|
||||
$wxsFile = Get-Content $wxsFilePath;
|
||||
@@ -96,7 +100,7 @@ Function Generate-FileComponents() {
|
||||
$componentDefs +=
|
||||
@"
|
||||
<Component Id="$($componentId)" Guid="$((New-Guid).ToString().ToUpper())">
|
||||
<RegistryKey Root="`$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
|
||||
<RegistryKey Root="$($regroot)" Key="Software\Classes\powertoys\components">
|
||||
<RegistryValue Type="string" Name="$($componentId)" Value="" KeyPath="yes"/>
|
||||
</RegistryKey>`r`n
|
||||
"@
|
||||
@@ -130,188 +134,194 @@ if ($platform -ceq "arm64") {
|
||||
$platform = "ARM64"
|
||||
}
|
||||
|
||||
if ($installscopeperuser -eq "true") {
|
||||
$registryroot = "HKCU"
|
||||
} else {
|
||||
$registryroot = "HKLM"
|
||||
}
|
||||
|
||||
#BaseApplications
|
||||
Generate-FileList -fileDepsJson "" -fileListName BaseApplicationsFiles -wxsFilePath $PSScriptRoot\BaseApplications.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release"
|
||||
Generate-FileComponents -fileListName "BaseApplicationsFiles" -wxsFilePath $PSScriptRoot\BaseApplications.wxs
|
||||
Generate-FileComponents -fileListName "BaseApplicationsFiles" -wxsFilePath $PSScriptRoot\BaseApplications.wxs -regroot $registryroot
|
||||
|
||||
#WinUI3Applications
|
||||
Generate-FileList -fileDepsJson "" -fileListName WinUI3ApplicationsFiles -wxsFilePath $PSScriptRoot\WinUI3Applications.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps"
|
||||
Generate-FileComponents -fileListName "WinUI3ApplicationsFiles" -wxsFilePath $PSScriptRoot\WinUI3Applications.wxs
|
||||
Generate-FileComponents -fileListName "WinUI3ApplicationsFiles" -wxsFilePath $PSScriptRoot\WinUI3Applications.wxs -regroot $registryroot
|
||||
|
||||
#AdvancedPaste
|
||||
Generate-FileList -fileDepsJson "" -fileListName AdvancedPasteAssetsFiles -wxsFilePath $PSScriptRoot\AdvancedPaste.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\AdvancedPaste"
|
||||
Generate-FileComponents -fileListName "AdvancedPasteAssetsFiles" -wxsFilePath $PSScriptRoot\AdvancedPaste.wxs
|
||||
Generate-FileComponents -fileListName "AdvancedPasteAssetsFiles" -wxsFilePath $PSScriptRoot\AdvancedPaste.wxs -regroot $registryroot
|
||||
|
||||
#AwakeFiles
|
||||
Generate-FileList -fileDepsJson "" -fileListName AwakeImagesFiles -wxsFilePath $PSScriptRoot\Awake.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\Awake"
|
||||
Generate-FileComponents -fileListName "AwakeImagesFiles" -wxsFilePath $PSScriptRoot\Awake.wxs
|
||||
Generate-FileComponents -fileListName "AwakeImagesFiles" -wxsFilePath $PSScriptRoot\Awake.wxs -regroot $registryroot
|
||||
|
||||
#ColorPicker
|
||||
Generate-FileList -fileDepsJson "" -fileListName ColorPickerAssetsFiles -wxsFilePath $PSScriptRoot\ColorPicker.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\ColorPicker"
|
||||
Generate-FileComponents -fileListName "ColorPickerAssetsFiles" -wxsFilePath $PSScriptRoot\ColorPicker.wxs
|
||||
Generate-FileComponents -fileListName "ColorPickerAssetsFiles" -wxsFilePath $PSScriptRoot\ColorPicker.wxs -regroot $registryroot
|
||||
|
||||
#Environment Variables
|
||||
Generate-FileList -fileDepsJson "" -fileListName EnvironmentVariablesAssetsFiles -wxsFilePath $PSScriptRoot\EnvironmentVariables.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\EnvironmentVariables"
|
||||
Generate-FileComponents -fileListName "EnvironmentVariablesAssetsFiles" -wxsFilePath $PSScriptRoot\EnvironmentVariables.wxs
|
||||
Generate-FileComponents -fileListName "EnvironmentVariablesAssetsFiles" -wxsFilePath $PSScriptRoot\EnvironmentVariables.wxs -regroot $registryroot
|
||||
|
||||
#FileExplorerAdd-ons
|
||||
Generate-FileList -fileDepsJson "" -fileListName MonacoPreviewHandlerMonacoAssetsFiles -wxsFilePath $PSScriptRoot\FileExplorerPreview.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\Monaco"
|
||||
Generate-FileList -fileDepsJson "" -fileListName MonacoPreviewHandlerCustomLanguagesFiles -wxsFilePath $PSScriptRoot\FileExplorerPreview.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\Monaco\customLanguages"
|
||||
Generate-FileComponents -fileListName "MonacoPreviewHandlerMonacoAssetsFiles" -wxsFilePath $PSScriptRoot\FileExplorerPreview.wxs
|
||||
Generate-FileComponents -fileListName "MonacoPreviewHandlerCustomLanguagesFiles" -wxsFilePath $PSScriptRoot\FileExplorerPreview.wxs
|
||||
Generate-FileComponents -fileListName "MonacoPreviewHandlerMonacoAssetsFiles" -wxsFilePath $PSScriptRoot\FileExplorerPreview.wxs -regroot $registryroot
|
||||
Generate-FileComponents -fileListName "MonacoPreviewHandlerCustomLanguagesFiles" -wxsFilePath $PSScriptRoot\FileExplorerPreview.wxs -regroot $registryroot
|
||||
|
||||
#FileLocksmith
|
||||
Generate-FileList -fileDepsJson "" -fileListName FileLocksmithAssetsFiles -wxsFilePath $PSScriptRoot\FileLocksmith.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\FileLocksmith"
|
||||
Generate-FileComponents -fileListName "FileLocksmithAssetsFiles" -wxsFilePath $PSScriptRoot\FileLocksmith.wxs
|
||||
Generate-FileComponents -fileListName "FileLocksmithAssetsFiles" -wxsFilePath $PSScriptRoot\FileLocksmith.wxs -regroot $registryroot
|
||||
|
||||
#Hosts
|
||||
Generate-FileList -fileDepsJson "" -fileListName HostsAssetsFiles -wxsFilePath $PSScriptRoot\Hosts.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\Hosts"
|
||||
Generate-FileComponents -fileListName "HostsAssetsFiles" -wxsFilePath $PSScriptRoot\Hosts.wxs
|
||||
Generate-FileComponents -fileListName "HostsAssetsFiles" -wxsFilePath $PSScriptRoot\Hosts.wxs -regroot $registryroot
|
||||
|
||||
#ImageResizer
|
||||
Generate-FileList -fileDepsJson "" -fileListName ImageResizerAssetsFiles -wxsFilePath $PSScriptRoot\ImageResizer.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\ImageResizer"
|
||||
Generate-FileComponents -fileListName "ImageResizerAssetsFiles" -wxsFilePath $PSScriptRoot\ImageResizer.wxs
|
||||
Generate-FileComponents -fileListName "ImageResizerAssetsFiles" -wxsFilePath $PSScriptRoot\ImageResizer.wxs -regroot $registryroot
|
||||
|
||||
# Light Switch Service
|
||||
Generate-FileList -fileDepsJson "" -fileListName LightSwitchFiles -wxsFilePath $PSScriptRoot\LightSwitch.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\LightSwitchService"
|
||||
Generate-FileComponents -fileListName "LightSwitchFiles" -wxsFilePath $PSScriptRoot\LightSwitch.wxs
|
||||
Generate-FileComponents -fileListName "LightSwitchFiles" -wxsFilePath $PSScriptRoot\LightSwitch.wxs -regroot $registryroot
|
||||
|
||||
#New+
|
||||
Generate-FileList -fileDepsJson "" -fileListName NewPlusAssetsFiles -wxsFilePath $PSScriptRoot\NewPlus.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\NewPlus"
|
||||
Generate-FileComponents -fileListName "NewPlusAssetsFiles" -wxsFilePath $PSScriptRoot\NewPlus.wxs
|
||||
Generate-FileComponents -fileListName "NewPlusAssetsFiles" -wxsFilePath $PSScriptRoot\NewPlus.wxs -regroot $registryroot
|
||||
|
||||
#Peek
|
||||
Generate-FileList -fileDepsJson "" -fileListName PeekAssetsFiles -wxsFilePath $PSScriptRoot\Peek.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\Peek\"
|
||||
Generate-FileComponents -fileListName "PeekAssetsFiles" -wxsFilePath $PSScriptRoot\Peek.wxs
|
||||
Generate-FileComponents -fileListName "PeekAssetsFiles" -wxsFilePath $PSScriptRoot\Peek.wxs -regroot $registryroot
|
||||
|
||||
#PowerRename
|
||||
Generate-FileList -fileDepsJson "" -fileListName PowerRenameAssetsFiles -wxsFilePath $PSScriptRoot\PowerRename.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\PowerRename\"
|
||||
Generate-FileComponents -fileListName "PowerRenameAssetsFiles" -wxsFilePath $PSScriptRoot\PowerRename.wxs
|
||||
Generate-FileComponents -fileListName "PowerRenameAssetsFiles" -wxsFilePath $PSScriptRoot\PowerRename.wxs -regroot $registryroot
|
||||
|
||||
#RegistryPreview
|
||||
Generate-FileList -fileDepsJson "" -fileListName RegistryPreviewAssetsFiles -wxsFilePath $PSScriptRoot\RegistryPreview.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\RegistryPreview\"
|
||||
Generate-FileComponents -fileListName "RegistryPreviewAssetsFiles" -wxsFilePath $PSScriptRoot\RegistryPreview.wxs
|
||||
Generate-FileComponents -fileListName "RegistryPreviewAssetsFiles" -wxsFilePath $PSScriptRoot\RegistryPreview.wxs -regroot $registryroot
|
||||
|
||||
#Run
|
||||
Generate-FileList -fileDepsJson "" -fileListName launcherImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\PowerLauncher"
|
||||
Generate-FileComponents -fileListName "launcherImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
|
||||
Generate-FileComponents -fileListName "launcherImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
## Plugins
|
||||
###Calculator
|
||||
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Calculator\Microsoft.PowerToys.Run.Plugin.Calculator.deps.json" -fileListName calcComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
|
||||
Generate-FileList -fileDepsJson "" -fileListName calcImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Calculator\Images"
|
||||
Generate-FileComponents -fileListName "calcComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
|
||||
Generate-FileComponents -fileListName "calcImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
|
||||
Generate-FileComponents -fileListName "calcComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
Generate-FileComponents -fileListName "calcImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
###Folder
|
||||
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Folder\Microsoft.Plugin.Folder.deps.json" -fileListName FolderComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
|
||||
Generate-FileList -fileDepsJson "" -fileListName FolderImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Folder\Images"
|
||||
Generate-FileComponents -fileListName "FolderComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
|
||||
Generate-FileComponents -fileListName "FolderImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
|
||||
Generate-FileComponents -fileListName "FolderComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
Generate-FileComponents -fileListName "FolderImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
###Program
|
||||
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Program\Microsoft.Plugin.Program.deps.json" -fileListName ProgramComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
|
||||
Generate-FileList -fileDepsJson "" -fileListName ProgramImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Program\Images"
|
||||
Generate-FileComponents -fileListName "ProgramComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
|
||||
Generate-FileComponents -fileListName "ProgramImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
|
||||
Generate-FileComponents -fileListName "ProgramComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
Generate-FileComponents -fileListName "ProgramImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
###Shell
|
||||
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Shell\Microsoft.Plugin.Shell.deps.json" -fileListName ShellComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
|
||||
Generate-FileList -fileDepsJson "" -fileListName ShellImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Shell\Images"
|
||||
Generate-FileComponents -fileListName "ShellComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
|
||||
Generate-FileComponents -fileListName "ShellImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
|
||||
Generate-FileComponents -fileListName "ShellComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
Generate-FileComponents -fileListName "ShellImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
###Indexer
|
||||
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Indexer\Microsoft.Plugin.Indexer.deps.json" -fileListName IndexerComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
|
||||
Generate-FileList -fileDepsJson "" -fileListName IndexerImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Indexer\Images"
|
||||
Generate-FileComponents -fileListName "IndexerComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
|
||||
Generate-FileComponents -fileListName "IndexerImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
|
||||
Generate-FileComponents -fileListName "IndexerComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
Generate-FileComponents -fileListName "IndexerImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
###UnitConverter
|
||||
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\UnitConverter\Community.PowerToys.Run.Plugin.UnitConverter.deps.json" -fileListName UnitConvCompFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
|
||||
Generate-FileList -fileDepsJson "" -fileListName UnitConvImagesCompFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\UnitConverter\Images"
|
||||
Generate-FileComponents -fileListName "UnitConvCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs
|
||||
Generate-FileComponents -fileListName "UnitConvImagesCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs
|
||||
Generate-FileComponents -fileListName "UnitConvCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
Generate-FileComponents -fileListName "UnitConvImagesCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
###WebSearch
|
||||
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\WebSearch\Community.PowerToys.Run.Plugin.WebSearch.deps.json" -fileListName WebSrchCompFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
|
||||
Generate-FileList -fileDepsJson "" -fileListName WebSrchImagesCompFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\WebSearch\Images"
|
||||
Generate-FileComponents -fileListName "WebSrchCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs
|
||||
Generate-FileComponents -fileListName "WebSrchImagesCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs
|
||||
Generate-FileComponents -fileListName "WebSrchCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
Generate-FileComponents -fileListName "WebSrchImagesCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
###History
|
||||
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\History\Microsoft.PowerToys.Run.Plugin.History.deps.json" -fileListName HistoryPluginComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
|
||||
Generate-FileList -fileDepsJson "" -fileListName HistoryPluginImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\History\Images"
|
||||
Generate-FileComponents -fileListName "HistoryPluginComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
|
||||
Generate-FileComponents -fileListName "HistoryPluginImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
|
||||
Generate-FileComponents -fileListName "HistoryPluginComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
Generate-FileComponents -fileListName "HistoryPluginImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
###Uri
|
||||
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Uri\Microsoft.Plugin.Uri.deps.json" -fileListName UriComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
|
||||
Generate-FileList -fileDepsJson "" -fileListName UriImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Uri\Images"
|
||||
Generate-FileComponents -fileListName "UriComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
|
||||
Generate-FileComponents -fileListName "UriImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
|
||||
Generate-FileComponents -fileListName "UriComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
Generate-FileComponents -fileListName "UriImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
###VSCodeWorkspaces
|
||||
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\VSCodeWorkspaces\Community.PowerToys.Run.Plugin.VSCodeWorkspaces.deps.json" -fileListName VSCWrkCompFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
|
||||
Generate-FileList -fileDepsJson "" -fileListName VSCWrkImagesCompFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\VSCodeWorkspaces\Images"
|
||||
Generate-FileComponents -fileListName "VSCWrkCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs
|
||||
Generate-FileComponents -fileListName "VSCWrkImagesCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs
|
||||
Generate-FileComponents -fileListName "VSCWrkCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
Generate-FileComponents -fileListName "VSCWrkImagesCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
###WindowWalker
|
||||
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\WindowWalker\Microsoft.Plugin.WindowWalker.deps.json" -fileListName WindowWlkrCompFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
|
||||
Generate-FileList -fileDepsJson "" -fileListName WindowWlkrImagesCompFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\WindowWalker\Images"
|
||||
Generate-FileComponents -fileListName "WindowWlkrCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs
|
||||
Generate-FileComponents -fileListName "WindowWlkrImagesCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs
|
||||
Generate-FileComponents -fileListName "WindowWlkrCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
Generate-FileComponents -fileListName "WindowWlkrImagesCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
###OneNote
|
||||
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\OneNote\Microsoft.PowerToys.Run.Plugin.OneNote.deps.json" -fileListName OneNoteComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
|
||||
Generate-FileList -fileDepsJson "" -fileListName OneNoteImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\OneNote\Images"
|
||||
Generate-FileComponents -fileListName "OneNoteComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
|
||||
Generate-FileComponents -fileListName "OneNoteImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
|
||||
Generate-FileComponents -fileListName "OneNoteComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
Generate-FileComponents -fileListName "OneNoteImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
###Registry
|
||||
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Registry\Microsoft.PowerToys.Run.Plugin.Registry.deps.json" -fileListName RegistryComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
|
||||
Generate-FileList -fileDepsJson "" -fileListName RegistryImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Registry\Images"
|
||||
Generate-FileComponents -fileListName "RegistryComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
|
||||
Generate-FileComponents -fileListName "RegistryImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
|
||||
Generate-FileComponents -fileListName "RegistryComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
Generate-FileComponents -fileListName "RegistryImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
###Service
|
||||
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Service\Microsoft.PowerToys.Run.Plugin.Service.deps.json" -fileListName ServiceComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
|
||||
Generate-FileList -fileDepsJson "" -fileListName ServiceImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Service\Images"
|
||||
Generate-FileComponents -fileListName "ServiceComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
|
||||
Generate-FileComponents -fileListName "ServiceImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
|
||||
Generate-FileComponents -fileListName "ServiceComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
Generate-FileComponents -fileListName "ServiceImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
###System
|
||||
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\System\Microsoft.PowerToys.Run.Plugin.System.deps.json" -fileListName SystemComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
|
||||
Generate-FileList -fileDepsJson "" -fileListName SystemImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\System\Images"
|
||||
Generate-FileComponents -fileListName "SystemComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
|
||||
Generate-FileComponents -fileListName "SystemImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
|
||||
Generate-FileComponents -fileListName "SystemComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
Generate-FileComponents -fileListName "SystemImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
###TimeDate
|
||||
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\TimeDate\Microsoft.PowerToys.Run.Plugin.TimeDate.deps.json" -fileListName TimeDateComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
|
||||
Generate-FileList -fileDepsJson "" -fileListName TimeDateImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\TimeDate\Images"
|
||||
Generate-FileComponents -fileListName "TimeDateComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
|
||||
Generate-FileComponents -fileListName "TimeDateImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
|
||||
Generate-FileComponents -fileListName "TimeDateComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
Generate-FileComponents -fileListName "TimeDateImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
###WindowsSettings
|
||||
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\WindowsSettings\Microsoft.PowerToys.Run.Plugin.WindowsSettings.deps.json" -fileListName WinSetCmpFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
|
||||
Generate-FileList -fileDepsJson "" -fileListName WinSetImagesCmpFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\WindowsSettings\Images"
|
||||
Generate-FileComponents -fileListName "WinSetCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs
|
||||
Generate-FileComponents -fileListName "WinSetImagesCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs
|
||||
Generate-FileComponents -fileListName "WinSetCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
Generate-FileComponents -fileListName "WinSetImagesCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
###WindowsTerminal
|
||||
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\WindowsTerminal\Microsoft.PowerToys.Run.Plugin.WindowsTerminal.deps.json" -fileListName WinTermCmpFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
|
||||
Generate-FileList -fileDepsJson "" -fileListName WinTermImagesCmpFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\WindowsTerminal\Images"
|
||||
Generate-FileComponents -fileListName "WinTermCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs
|
||||
Generate-FileComponents -fileListName "WinTermImagesCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs
|
||||
Generate-FileComponents -fileListName "WinTermCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
Generate-FileComponents -fileListName "WinTermImagesCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
###PowerToys
|
||||
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\PowerToys\Microsoft.PowerToys.Run.Plugin.PowerToys.deps.json" -fileListName PowerToysCmpFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
|
||||
Generate-FileList -fileDepsJson "" -fileListName PowerToysImagesCmpFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\PowerToys\Images"
|
||||
Generate-FileComponents -fileListName "PowerToysCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs
|
||||
Generate-FileComponents -fileListName "PowerToysImagesCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs
|
||||
Generate-FileComponents -fileListName "PowerToysCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
Generate-FileComponents -fileListName "PowerToysImagesCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
###ValueGenerator
|
||||
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\ValueGenerator\Community.PowerToys.Run.Plugin.ValueGenerator.deps.json" -fileListName ValueGeneratorCmpFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
|
||||
Generate-FileList -fileDepsJson "" -fileListName ValueGeneratorImagesCmpFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\ValueGenerator\Images"
|
||||
Generate-FileComponents -fileListName "ValueGeneratorCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs
|
||||
Generate-FileComponents -fileListName "ValueGeneratorImagesCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs
|
||||
Generate-FileComponents -fileListName "ValueGeneratorCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
Generate-FileComponents -fileListName "ValueGeneratorImagesCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
|
||||
## Plugins
|
||||
|
||||
#ShortcutGuide
|
||||
Generate-FileList -fileDepsJson "" -fileListName ShortcutGuideSvgFiles -wxsFilePath $PSScriptRoot\ShortcutGuide.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\ShortcutGuide\"
|
||||
Generate-FileComponents -fileListName "ShortcutGuideSvgFiles" -wxsFilePath $PSScriptRoot\ShortcutGuide.wxs
|
||||
Generate-FileComponents -fileListName "ShortcutGuideSvgFiles" -wxsFilePath $PSScriptRoot\ShortcutGuide.wxs -regroot $registryroot
|
||||
|
||||
#Settings
|
||||
Generate-FileList -fileDepsJson "" -fileListName SettingsV2AssetsFiles -wxsFilePath $PSScriptRoot\Settings.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\Settings\"
|
||||
Generate-FileList -fileDepsJson "" -fileListName SettingsV2AssetsModulesFiles -wxsFilePath $PSScriptRoot\Settings.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\Settings\Modules\"
|
||||
Generate-FileList -fileDepsJson "" -fileListName SettingsV2OOBEAssetsModulesFiles -wxsFilePath $PSScriptRoot\Settings.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\Settings\Modules\OOBE\"
|
||||
Generate-FileList -fileDepsJson "" -fileListName SettingsV2OOBEAssetsFluentIconsFiles -wxsFilePath $PSScriptRoot\Settings.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\Settings\Icons\"
|
||||
Generate-FileComponents -fileListName "SettingsV2AssetsFiles" -wxsFilePath $PSScriptRoot\Settings.wxs
|
||||
Generate-FileComponents -fileListName "SettingsV2AssetsModulesFiles" -wxsFilePath $PSScriptRoot\Settings.wxs
|
||||
Generate-FileComponents -fileListName "SettingsV2OOBEAssetsModulesFiles" -wxsFilePath $PSScriptRoot\Settings.wxs
|
||||
Generate-FileComponents -fileListName "SettingsV2OOBEAssetsFluentIconsFiles" -wxsFilePath $PSScriptRoot\Settings.wxs
|
||||
Generate-FileComponents -fileListName "SettingsV2AssetsFiles" -wxsFilePath $PSScriptRoot\Settings.wxs -regroot $registryroot
|
||||
Generate-FileComponents -fileListName "SettingsV2AssetsModulesFiles" -wxsFilePath $PSScriptRoot\Settings.wxs -regroot $registryroot
|
||||
Generate-FileComponents -fileListName "SettingsV2OOBEAssetsModulesFiles" -wxsFilePath $PSScriptRoot\Settings.wxs -regroot $registryroot
|
||||
Generate-FileComponents -fileListName "SettingsV2OOBEAssetsFluentIconsFiles" -wxsFilePath $PSScriptRoot\Settings.wxs -regroot $registryroot
|
||||
|
||||
#Workspaces
|
||||
Generate-FileList -fileDepsJson "" -fileListName WorkspacesImagesComponentFiles -wxsFilePath $PSScriptRoot\Workspaces.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\Workspaces\"
|
||||
Generate-FileComponents -fileListName "WorkspacesImagesComponentFiles" -wxsFilePath $PSScriptRoot\Workspaces.wxs
|
||||
Generate-FileComponents -fileListName "WorkspacesImagesComponentFiles" -wxsFilePath $PSScriptRoot\Workspaces.wxs -regroot $registryroot
|
||||
|
||||
59
pt-cli-plan.md
Normal file
59
pt-cli-plan.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# PowerToys CLI Implementation Plan
|
||||
|
||||
## Goal
|
||||
- Deliver the `ptcli` command-line experience described in `pt-cli.md`, with `Runner` acting as the single broker for module commands.
|
||||
- Provide a maintainable architecture where modules self-describe commands, and CLI clients consume a uniform JSON/NamedPipe protocol.
|
||||
|
||||
## Workstreams
|
||||
|
||||
### 1. Broker Foundation (Runner)
|
||||
- **Command Registry**: Implement `IModuleCommandProvider` registration on module load and persist `CommandDescriptor` metadata (schema, elevation flag, long-running hints, docs).
|
||||
- **IPC Host**: Stand up the `\\.\pipe\PowerToys.Runner.CLI` NamedPipe server; define request/response DTOs with versioning (`v` field, correlation IDs).
|
||||
- **Dispatch Pipeline**: Validate module/action, apply schema validation, enforce elevation policy, and invoke `ExecuteAsync`.
|
||||
- **Response Envelope**: Normalize `status` (`ok|error|accepted`), payload, and error block (`code/message/details`). Emit diagnostic logging (caller, command, latency, result).
|
||||
|
||||
### 2. CLI Thin Client (`ptcli`)
|
||||
- **Argument Parsing**: Support `ptcli -m <module> <action> [--arg value]`, plus `--list-modules`, `--list-commands`.
|
||||
- **Transport**: Serialize requests to JSON, connect to the pipe with timeout handling, and deserialize responses.
|
||||
- **Output UX**: Map standard errors to friendly text, show structured results, and support optional `--json` passthrough.
|
||||
- **Async Jobs**: Handle `status=accepted` by printing job IDs, exposing `ptcli job status <id>` and `ptcli job cancel <id>` commands (polling via Runner endpoints).
|
||||
|
||||
### 3. Module Onboarding
|
||||
- **Awake**: Implement `IModuleCommandProvider` returning `set/start/stop/list` commands. Adapt current APIs or legacy triggers inside `ExecuteAsync`.
|
||||
- **Workspaces**: Provide `list/apply/delete` commands; wrap existing workspace manager calls. Ensure long-running operations flag `LongRunning=true`.
|
||||
- **Legacy Adapters**: For modules still using raw events/pipes, add Runner-side shims that translate command invocations while longer-term refactors are scheduled.
|
||||
|
||||
### 4. Capability Discovery & Help
|
||||
- **Describe APIs**: Expose Runner endpoints for `modules`, `commands`, parameter schemas, and elevation requirements.
|
||||
- **CLI Help**: Use discovery data to render `ptcli help`, module-specific usage, and argument hints without duplicating metadata.
|
||||
|
||||
### 5. Reliability, Security, Observability
|
||||
- **Security**: Configure pipe DACL to restrict access to the interactive user; enforce argument length/type limits.
|
||||
- **Concurrency**: Process each request on a dedicated task; delegate concurrency limits to modules. Provide cancellation tokens from Runner.
|
||||
- **Tracing**: Emit structured logs/ETW for requests, errors, and long-running progress notifications.
|
||||
- **Error Catalog**: Implement standardized error codes (`E_MODULE_NOT_FOUND`, `E_ARGS_INVALID`, `E_NEEDS_ELEVATION`, `E_TIMEOUT`, etc.) and map module exceptions accordingly.
|
||||
|
||||
### 6. Elevation & Policies
|
||||
- **Elevation Flow**: Detect when commands require elevation; if Runner is not elevated, return `E_NEEDS_ELEVATION` with actionable hints. Integrate with existing elevated Runner helper when available.
|
||||
- **Policy Hooks**: Add optional checks for policy/experiment gates before command execution.
|
||||
|
||||
### 7. Progress & Notifications
|
||||
- **Progress Channel**: Support incremental JSON progress messages over the same pipe or via job polling endpoints.
|
||||
- **Timeouts/Retry**: Implement configurable `timeoutMs` handling and `E_BUSY_RETRY` responses for transient module lock scenarios.
|
||||
|
||||
### 8. Incremental Rollout Strategy
|
||||
- **Phase 1**: Ship Runner pipe host + CLI client with two flagship commands (Awake.Set, Workspaces.List); document manual enablement.
|
||||
- **Phase 2**: Migrate additional modules through adapters; add help/describe surfaces and job management.
|
||||
- **Phase 3**: Enforce schema validation, finalize error catalog, and wire observability dashboards.
|
||||
- **Phase 4**: Deprecate direct module NamedPipe/event entry points once CLI parity is achieved.
|
||||
|
||||
### 9. Documentation & Maintenance
|
||||
- **User Docs**: Populate `pt-cli.md` with usage examples, elevation guidance, and troubleshooting mapped to error codes.
|
||||
- **Developer Guide**: Add module author instructions for implementing `IModuleCommandProvider`, including schema examples and best practices.
|
||||
- **Release Checklist**: Track new commands per release, update discovery metadata, and ensure CLI integration tests cover regression cases.
|
||||
|
||||
## Open Questions
|
||||
- What tooling will maintain JSON schemas (hand-authored vs. source-generated)?
|
||||
- Should progress streaming use duplex pipe messages or a per-job polling API?
|
||||
- How will elevated Runner lifecycle be managed (reuse existing helper vs. new broker)?
|
||||
- Which modules are in-scope for the first public preview, and what is the rollout schedule?
|
||||
358
pt-cli.md
Normal file
358
pt-cli.md
Normal file
@@ -0,0 +1,358 @@
|
||||
选 Runner 作为唯一的 Server/Broker,ptcli 只是瘦客户端。
|
||||
|
||||
|
||||
模块通过各自的 ModuleInterface 向 Runner 注册“可被调用的命令/参数模式”。
|
||||
|
||||
|
||||
ptcli → Runner 用 统一的 IPC(建议 NamedPipe + JSON-RPC/自定义轻量 JSON 协议)。
|
||||
|
||||
|
||||
Runner 再把请求转发到对应模块(可以是直接调用模块公开的接口,或转译为该模块现有的触发机制,如 Event/NamedPipe)。
|
||||
|
||||
|
||||
对“历史遗留的 event handle/pipe 触发点”,短期由 Runner 做兼容层;长期逐步统一为“命令接口”。
|
||||
|
||||
|
||||
这样你能得到:能力发现、参数校验、权限/提权、错误码一致、可观察性一致、向后兼容。
|
||||
|
||||
|
||||
|
||||
组件与职责
|
||||
|
||||
|
||||
ptcli(瘦客户端)
|
||||
|
||||
|
||||
|
||||
|
||||
解析命令行:ptcli -m awake set --duration 1h / ptcli -m workspace list
|
||||
|
||||
|
||||
将其映射为通用消息(JSON)发给 Runner。
|
||||
|
||||
|
||||
处理同步/异步返回、展示统一错误码与人类可读信息。
|
||||
|
||||
|
||||
最多内置“列出模块/命令的帮助”这类“离线功能”,但真正的能力发现来自 Runner。
|
||||
|
||||
|
||||
|
||||
|
||||
Runner(统一 Server/Broker)
|
||||
|
||||
|
||||
|
||||
|
||||
启动时建立 NamedPipe 服务端:\\.\pipe\PowerToys.Runner.CLI(示例)。
|
||||
|
||||
|
||||
维护 Command Registry:每个模块在加载/初始化时注册自己的命令(名称、参数 schema、是否需要提权、是否长任务、超时时间建议、描述文案等)。
|
||||
|
||||
|
||||
收到请求后:
|
||||
|
||||
|
||||
校验模块是否存在、命令是否存在、参数是否通过 schema 验证。
|
||||
|
||||
|
||||
如需提权且当前 Runner 权限不足:按策略返回“需要提权”的标准错误,或通过你们现有的提权助手启动“Elevated Runner”做代办。
|
||||
|
||||
|
||||
转发给目标模块(优先调用模块公开的“命令接口方法”;若模块尚未改造,由 Runner 适配为该模块现有触发(Event/NamedPipe))。
|
||||
|
||||
|
||||
汇总返回值,统一封装标准响应(状态、数据、错误码、诊断信息)。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Module(实现者)
|
||||
|
||||
|
||||
|
||||
|
||||
实现 IModuleCommandProvider(示例命名):
|
||||
|
||||
|
||||
IEnumerable<CommandDescriptor> DescribeCommands() 暴露命令元数据;
|
||||
|
||||
|
||||
Task<CommandResult> ExecuteAsync(CommandInvocation ctx) 执行命令;
|
||||
|
||||
|
||||
可标注“需要前台 UI”、“需要管理员”、“可能长时间运行(支持取消/进度)”等。
|
||||
|
||||
|
||||
|
||||
|
||||
现有“事件/NamedPipe 触发路径”的模块:短期由 Runner 适配;长期建议模块直接实现上面的 ExecuteAsync,统一语义与可观测性。
|
||||
|
||||
|
||||
|
||||
协议与数据结构(建议)
|
||||
请求(ptcli→Runner)
|
||||
{
|
||||
"v": 1,
|
||||
"correlationId": "uuid",
|
||||
"command": {
|
||||
"module": "awake",
|
||||
"action": "set", // 例如 set/start/stop/list 等
|
||||
"args": { "duration": "1h" } // 按模块定义的 schema
|
||||
},
|
||||
"options": {
|
||||
"timeoutMs": 20000,
|
||||
"wantProgress": false
|
||||
}
|
||||
}
|
||||
|
||||
响应(Runner→ptcli)
|
||||
{
|
||||
"v": 1,
|
||||
"correlationId": "uuid",
|
||||
"status": "ok", // ok | error | accepted (异步)
|
||||
"result": { /* 模块返回的结构化数据 */ },
|
||||
"error": { // 仅当 status=error
|
||||
"code": "E_NEEDS_ELEVATION", // 标准化错误码
|
||||
"message": "Awake requires elevation",
|
||||
"details": { "hint": "rerun with --elevated" }
|
||||
}
|
||||
}
|
||||
|
||||
进度/异步(可选)
|
||||
|
||||
|
||||
长任务时,status="accepted" 并返回 jobId;ptcli 可 ptcli job status <jobId> 轮询,或 Runner 通过同管道 增量推送 progress(JSON lines)。
|
||||
|
||||
|
||||
取消:ptcli 发送 { action: "cancel", jobId: "..." },Runner 调用模块 CancellationToken。
|
||||
|
||||
|
||||
|
||||
命令发现与帮助
|
||||
|
||||
|
||||
ptcli -m list:列出模块(Runner 直接返回 registry)。
|
||||
|
||||
|
||||
ptcli -m awake -h:DescribeCommands() 中的 Awake 条目返回所有 action、参数与示例。
|
||||
|
||||
|
||||
参数 schema:用简化 JSON Schema(或手写约束)即可,让 ptcli 能本地提示,也让 Runner 能服务器端校验。
|
||||
|
||||
|
||||
|
||||
示例映射
|
||||
Awake
|
||||
|
||||
|
||||
ptcli -m awake set --duration 1h
|
||||
→ Awake.Set(duration=1h)
|
||||
→ Runner 调用 AwakeModule.ExecuteAsync("set", args)
|
||||
→ 结果:{ "effectiveUntil": "2025-10-30T18:00:00+08:00" }
|
||||
|
||||
|
||||
ptcli -m awake stop
|
||||
→ Awake.Stop()(幂等)
|
||||
|
||||
|
||||
Workspaces
|
||||
|
||||
|
||||
ptcli -m workspace list
|
||||
→ Workspaces.List() 返回 { "items": [{ "id": "...", "name": "...", "monitors": 2, "windows": 14 }] }
|
||||
|
||||
|
||||
ptcli -m workspace apply --id 123 --strict
|
||||
→ Workspaces.Apply(id=123, strict=true) 支持进度与失败报告(缺失进程、权限不足等)。
|
||||
|
||||
|
||||
|
||||
Runner vs 直接敲模块事件/NamedPipe
|
||||
直接敲模块(如你举的 EventWaitHandle)优点
|
||||
|
||||
|
||||
省一跳,模块自己掌控。
|
||||
|
||||
|
||||
对少数“拍一下就够”的快捷触发点,写起来快。
|
||||
|
||||
|
||||
缺点(关键)
|
||||
|
||||
|
||||
入口分散:每个模块各有各的触发名、参数约定、错误语义。
|
||||
|
||||
|
||||
能力发现困难:ptcli 无法统一列出“模块能干啥、参数是什么”。
|
||||
|
||||
|
||||
权限与多实例问题:有的模块需要管理员/前台,有的在用户会话,有的在服务;直接对模块打洞容易踩坑。
|
||||
|
||||
|
||||
审计/可观察性差:难以统一日志/遥测/超时/取消。
|
||||
|
||||
|
||||
演进成本高:接口一旦铺散,很难回收。
|
||||
|
||||
|
||||
走 Runner Proxy(推荐)
|
||||
|
||||
|
||||
统一注册:模块只跟 Runner 说“我能做哪些命令、参数是什么”。
|
||||
|
||||
|
||||
统一协议:ptcli 只会说一种“通用 JSON 命令”。
|
||||
|
||||
|
||||
统一安全/提权/会话:Runner 最懂自己所在的权限/桌面会话,可决定是否需要跳 Elevation/切用户会话。
|
||||
|
||||
|
||||
兼容旧触发:Runner 内部去“Set 事件/写管道”,外部对 ptcli 完全透明。
|
||||
|
||||
|
||||
可测试/可监控:所有调用都经由同一 Broker,便于打点、限流、诊断。
|
||||
|
||||
|
||||
|
||||
结论:把直接事件/管道触发视为“模块侧 private API”,只由 Runner 调用。ptcli 与普通用户两边都只看得到 Runner 的“公共命令接口”。
|
||||
|
||||
|
||||
Runner 怎么“轻量 Server”
|
||||
|
||||
|
||||
进程:沿用现有 Runner,不另起新守护;新增一个 CommandRouter 子系统即可。
|
||||
|
||||
|
||||
IPC:NamedPipeServerStream + StreamJsonRpc(或你们已有的 JSON 框架);单管道多请求(长度前缀 + correlationId)。
|
||||
|
||||
|
||||
并发:每请求一个 Task,模块执行受自身并发控制。
|
||||
|
||||
|
||||
安全:给管道设定 DACL,仅允许同一交互式用户(或受信 SID)连接;参数白名单与长度限制防注入。
|
||||
|
||||
|
||||
错误码:统一枚举(像 HTTP 状态一样):
|
||||
|
||||
|
||||
E_MODULE_NOT_FOUND / E_COMMAND_NOT_FOUND / E_ARGS_INVALID
|
||||
|
||||
|
||||
E_NEEDS_ELEVATION / E_ACCESS_DENIED
|
||||
|
||||
|
||||
E_BUSY_RETRY / E_TIMEOUT / E_INTERNAL
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
最小可行落地(增量实施顺序)
|
||||
|
||||
|
||||
在 Runner 加一个 Pipe + CommandRouter,硬编码两个演示命令:
|
||||
|
||||
|
||||
Awake.Set(duration)(直接调用 Awake 的现有 API)
|
||||
|
||||
|
||||
Workspaces.List()(调用 Workspace 管理器)
|
||||
|
||||
|
||||
|
||||
|
||||
写 ptcli:只做 JSON 打包、发管道、打印结果。
|
||||
|
||||
|
||||
给两个模块各加 IModuleCommandProvider,从 Runner 注册。
|
||||
|
||||
|
||||
把 1~2 个“历史事件触发点”接入 Router(Runner 内部去 Set Event),对外暴露为 Module.Action。
|
||||
|
||||
|
||||
扩展:help/describe、Job/进度、取消、提权路径、返回码规范化。
|
||||
|
||||
|
||||
|
||||
简短示例(C#,仅示意;注释英文)
|
||||
Runner – 接口定义
|
||||
public record CommandDescriptor(
|
||||
string Module, string Action, string Description,
|
||||
IReadOnlyDictionary<string, ParamSpec> Params,
|
||||
bool RequiresElevation = false, bool LongRunning = false);
|
||||
|
||||
public interface IModuleCommandProvider
|
||||
{
|
||||
IEnumerable<CommandDescriptor> DescribeCommands();
|
||||
Task<CommandResult> ExecuteAsync(CommandInvocation ctx, CancellationToken ct);
|
||||
}
|
||||
|
||||
public record CommandInvocation(string Action, IReadOnlyDictionary<string, object?> Args);
|
||||
public record CommandResult(bool Ok, object? Data = null, string? ErrorCode = null, string? ErrorMessage = null);
|
||||
|
||||
Runner – 注册与路由(伪码)
|
||||
// On module load:
|
||||
registry.Register(provider.DescribeCommands(), provider);
|
||||
|
||||
// On request:
|
||||
var cmd = request.Command; // module, action, args
|
||||
var provider = registry.Resolve(cmd.Module, cmd.Action);
|
||||
ValidateArgs(cmd.Args, provider.Schema);
|
||||
if (provider.RequiresElevation && !IsElevated())
|
||||
return Error("E_NEEDS_ELEVATION", "Elevation required.");
|
||||
return await provider.ExecuteAsync(new CommandInvocation(cmd.Action, cmd.Args), ct);
|
||||
|
||||
ptcli – 调用(伪码)
|
||||
// Build request from CLI args
|
||||
var req = new { v = 1, correlationId = Guid.NewGuid(), command = new { module, action, args } };
|
||||
using var client = new NamedPipeClientStream(".", "PowerToys.Runner.CLI", PipeDirection.InOut);
|
||||
await client.ConnectAsync(timeout);
|
||||
await WriteJsonAsync(client, req);
|
||||
var resp = await ReadJsonAsync(client);
|
||||
Render(resp);
|
||||
return resp.status == "ok" ? 0 : 1;
|
||||
|
||||
|
||||
兼容性与演进策略
|
||||
|
||||
|
||||
不强行改造所有模块:Runner 先用“适配器”把旧事件/管道触发包起来,CLI 立即可用;后续模块逐步实现 IModuleCommandProvider。
|
||||
|
||||
|
||||
版本化:请求/响应带 v,便于以后扩展字段。
|
||||
|
||||
|
||||
可观测性:Runner 统一日志:调用者、命令、耗时、结果码;必要时加 ETW/Trace。
|
||||
|
||||
|
||||
权限:Runner 决策提权路径;ptcli 接到 E_NEEDS_ELEVATION 时,可提示 --elevated 或弹系统 UAC(按你们策略)。
|
||||
|
||||
|
||||
并行/队列:对长任务(如 workspace apply)可按模块内队列执行,避免竞态;CLI 侧提供 job status/cancel。
|
||||
|
||||
|
||||
|
||||
你关心的具体问题直答
|
||||
|
||||
|
||||
“由 moduleinterface 做统一入口管理是个好主意吗?”
|
||||
好,但入口管理应驻留在 Runner。模块通过 ModuleInterface 报告命令与执行入口,Runner 统一对外暴露。
|
||||
|
||||
|
||||
“ptcli 直接找模块执行,还是让 Runner 派发?”
|
||||
让 Runner 派发。这样获得统一发现、权限、错误、日志与演进空间。
|
||||
|
||||
|
||||
“现在很多模块用 named pipe/event 触发,这算绕过 Runner 吗?”
|
||||
目前你看到的例子确实是“模块直连触发”。建议以后仅 Runner 使用这些内部触发(做兼容/过渡),对外统一成命令。
|
||||
|
||||
|
||||
“Runner 起一个 light-weight server 吗?”
|
||||
对。就在 Runner 里加一个轻量 NamedPipe JSON-RPC 服务,不必单独再起一个服务进程。
|
||||
|
||||
|
||||
|
||||
要的话,我可以按上面的协议给你一版 ptcli(C#)与 Runner 侧 CommandRouter 的“最小可用代码骨架”,再加上 Awake/Workspaces 的两个真实命令适配示例。
|
||||
@@ -26,16 +26,6 @@ namespace ManagedCommon
|
||||
|
||||
private static readonly string Version = Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyFileVersionAttribute>()?.Version ?? "Unknown";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path to the log directory for the current version of the app.
|
||||
/// </summary>
|
||||
public static string CurrentVersionLogDirectoryPath { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path to the log directory for the app.
|
||||
/// </summary>
|
||||
public static string AppLogDirectoryPath { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the logger and sets the path for logging.
|
||||
/// </summary>
|
||||
@@ -52,9 +42,6 @@ namespace ManagedCommon
|
||||
Directory.CreateDirectory(versionedPath);
|
||||
}
|
||||
|
||||
AppLogDirectoryPath = basePath;
|
||||
CurrentVersionLogDirectoryPath = versionedPath;
|
||||
|
||||
var logFilePath = Path.Combine(versionedPath, "Log_" + DateTime.Now.ToString(@"yyyy-MM-dd", CultureInfo.InvariantCulture) + ".log");
|
||||
|
||||
Trace.Listeners.Add(new TextWriterTraceListener(logFilePath));
|
||||
|
||||
@@ -92,7 +92,6 @@
|
||||
<ClInclude Include="..\NewShellExtensionContextMenu\template_folder.h" />
|
||||
<ClInclude Include="..\NewShellExtensionContextMenu\template_item.h" />
|
||||
<ClInclude Include="..\NewShellExtensionContextMenu\trace.h" />
|
||||
<ClInclude Include="..\NewShellExtensionContextMenu\Helpers.h" />
|
||||
<ClInclude Include="dll_main.h" />
|
||||
<ClInclude Include="Generated Files\resource.h" />
|
||||
<ClInclude Include="pch.h" />
|
||||
@@ -100,7 +99,7 @@
|
||||
<ClInclude Include="shell_context_menu_win10.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="..\NewShellExtensionContextMenu\Helpers.cpp" />
|
||||
<ClCompile Include="..\..\powerrename\lib\Helpers.cpp" />
|
||||
<ClCompile Include="..\NewShellExtensionContextMenu\new_utilities.cpp" />
|
||||
<ClCompile Include="..\NewShellExtensionContextMenu\powertoys_module.cpp" />
|
||||
<ClCompile Include="..\NewShellExtensionContextMenu\settings.cpp" />
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#pragma once
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#define NOMINMAX
|
||||
#define NOMCX
|
||||
#define NOHELP
|
||||
#define NOCOMM
|
||||
|
||||
@@ -1,118 +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.
|
||||
|
||||
#include "pch.h"
|
||||
#include "Helpers.h"
|
||||
#include <regex>
|
||||
|
||||
// Minimal subset of PowerRename Helpers used by NewPlus
|
||||
// This is a copy from PowerRename main branch to avoid cross-module dependencies
|
||||
|
||||
HRESULT GetDatedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, SYSTEMTIME fileTime)
|
||||
{
|
||||
std::locale::global(std::locale(""));
|
||||
HRESULT hr = E_INVALIDARG;
|
||||
if (source && wcslen(source) > 0)
|
||||
{
|
||||
std::wstring res(source);
|
||||
wchar_t replaceTerm[MAX_PATH] = { 0 };
|
||||
wchar_t formattedDate[MAX_PATH] = { 0 };
|
||||
|
||||
wchar_t localeName[LOCALE_NAME_MAX_LENGTH];
|
||||
if (GetUserDefaultLocaleName(localeName, LOCALE_NAME_MAX_LENGTH) == 0)
|
||||
{
|
||||
StringCchCopy(localeName, LOCALE_NAME_MAX_LENGTH, L"en_US");
|
||||
}
|
||||
|
||||
int hour12 = (fileTime.wHour % 12);
|
||||
if (hour12 == 0)
|
||||
{
|
||||
hour12 = 12;
|
||||
}
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%04d"), L"$01", fileTime.wYear);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$YYYY"), replaceTerm);
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", (fileTime.wYear % 100));
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$YY"), replaceTerm);
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", (fileTime.wYear % 10));
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$Y"), replaceTerm);
|
||||
|
||||
GetDateFormatEx(localeName, NULL, &fileTime, L"MMMM", formattedDate, MAX_PATH, NULL);
|
||||
formattedDate[0] = towupper(formattedDate[0]);
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", formattedDate);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$MMMM"), replaceTerm);
|
||||
|
||||
GetDateFormatEx(localeName, NULL, &fileTime, L"MMM", formattedDate, MAX_PATH, NULL);
|
||||
formattedDate[0] = towupper(formattedDate[0]);
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", formattedDate);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$MMM"), replaceTerm);
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wMonth);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$MM"), replaceTerm);
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wMonth);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$M"), replaceTerm);
|
||||
|
||||
GetDateFormatEx(localeName, NULL, &fileTime, L"dddd", formattedDate, MAX_PATH, NULL);
|
||||
formattedDate[0] = towupper(formattedDate[0]);
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", formattedDate);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$DDDD"), replaceTerm);
|
||||
|
||||
GetDateFormatEx(localeName, NULL, &fileTime, L"ddd", formattedDate, MAX_PATH, NULL);
|
||||
formattedDate[0] = towupper(formattedDate[0]);
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", formattedDate);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$DDD"), replaceTerm);
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wDay);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$DD"), replaceTerm);
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wDay);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$D"), replaceTerm);
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", hour12);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$HH"), replaceTerm);
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", hour12);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$H"), replaceTerm);
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", (fileTime.wHour < 12) ? L"AM" : L"PM");
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$TT"), replaceTerm);
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", (fileTime.wHour < 12) ? L"am" : L"pm");
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$tt"), replaceTerm);
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wHour);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$hh"), replaceTerm);
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wHour);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$h"), replaceTerm);
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wMinute);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$mm"), replaceTerm);
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wMinute);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$m"), replaceTerm);
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wSecond);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$ss"), replaceTerm);
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wSecond);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$s"), replaceTerm);
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%03d"), L"$01", fileTime.wMilliseconds);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$fff"), replaceTerm);
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wMilliseconds / 10);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$ff"), replaceTerm);
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wMilliseconds / 100);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$f"), replaceTerm);
|
||||
|
||||
hr = StringCchCopy(result, cchMax, res.c_str());
|
||||
}
|
||||
|
||||
return hr;
|
||||
}
|
||||
@@ -1,9 +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.
|
||||
|
||||
#pragma once
|
||||
|
||||
// Minimal subset of PowerRename Helpers used by NewPlus
|
||||
// This is a copy from PowerRename's main branch to avoid cross-module dependencies
|
||||
HRESULT GetDatedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, SYSTEMTIME fileTime);
|
||||
@@ -110,7 +110,6 @@ MakeAppx.exe pack /d . /p $(OutDir)NewPlusPackage.msix /nv</Command>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="dll_main.h" />
|
||||
<ClInclude Include="Helpers.h" />
|
||||
<ClInclude Include="helpers_filesystem.h" />
|
||||
<ClInclude Include="helpers_variables.h" />
|
||||
<ClInclude Include="shell_context_menu.h" />
|
||||
@@ -128,7 +127,7 @@ MakeAppx.exe pack /d . /p $(OutDir)NewPlusPackage.msix /nv</Command>
|
||||
<ClInclude Include="template_item.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="Helpers.cpp" />
|
||||
<ClCompile Include="..\..\powerrename\lib\Helpers.cpp" />
|
||||
<ClCompile Include="new_utilities.cpp" />
|
||||
<ClCompile Include="shell_context_menu.cpp" />
|
||||
<ClCompile Include="shell_context_sub_menu.cpp" />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <regex>
|
||||
#include "Helpers.h"
|
||||
#include "..\..\powerrename\lib\Helpers.h"
|
||||
#include "helpers_filesystem.h"
|
||||
|
||||
#pragma comment(lib, "Pathcch.lib")
|
||||
|
||||
@@ -302,9 +302,9 @@ namespace newplus::utilities
|
||||
POINT mouse_position;
|
||||
GetCursorPos(&mouse_position);
|
||||
mouse_position.x -= GetSystemMetrics(SM_CXMENUSIZE);
|
||||
mouse_position.x = (std::max)(mouse_position.x, 20L);
|
||||
mouse_position.x = max(mouse_position.x, 20);
|
||||
mouse_position.y -= GetSystemMetrics(SM_CXMENUSIZE)/2;
|
||||
mouse_position.y = (std::max)(mouse_position.y, 20L);
|
||||
mouse_position.y = max(mouse_position.y, 20);
|
||||
POINT position[] = { mouse_position };
|
||||
folder_view->SelectAndPositionItems(1, shell_item_to_select_and_position, position, common_select_flags | SVSI_POSITIONITEM);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#pragma once
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#define NOMINMAX
|
||||
#define NOMCX
|
||||
#define NOHELP
|
||||
#define NOCOMM
|
||||
@@ -14,7 +13,6 @@
|
||||
#include <shellapi.h>
|
||||
#include <Windows.h>
|
||||
#include <shlobj.h>
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
#include <system_error>
|
||||
#include <memory>
|
||||
|
||||
@@ -60,8 +60,8 @@ std::wstring template_item::get_target_filename(const bool include_starting_digi
|
||||
|
||||
std::wstring template_item::remove_starting_digits_from_filename(std::wstring filename) const
|
||||
{
|
||||
filename.erase(0, std::min(filename.find_first_not_of(L"0123456789"), filename.size()));
|
||||
filename.erase(0, std::min(filename.find_first_not_of(L" ."), filename.size()));
|
||||
filename.erase(0, min(filename.find_first_not_of(L"0123456789"), filename.size()));
|
||||
filename.erase(0, min(filename.find_first_not_of(L" ."), filename.size()));
|
||||
|
||||
return filename;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <common/utils/logger_helper.h>
|
||||
#include <common/utils/resources.h>
|
||||
#include <common/utils/winapi_error.h>
|
||||
#include <common/utils/json.h>
|
||||
|
||||
#include <WorkspacesLib/trace.h>
|
||||
#include <WorkspacesLib/WorkspacesData.h>
|
||||
@@ -17,6 +18,9 @@
|
||||
|
||||
#include "resource.h"
|
||||
#include <common/utils/EventWaiter.h>
|
||||
#include <algorithm>
|
||||
#include <cwctype>
|
||||
#include <memory>
|
||||
|
||||
// Non-localizable
|
||||
const std::wstring workspacesLauncherPath = L"PowerToys.WorkspacesLauncher.exe";
|
||||
@@ -69,6 +73,8 @@ public:
|
||||
return app_key.c_str();
|
||||
}
|
||||
|
||||
pt::cli::IModuleCommandProvider* command_provider() override;
|
||||
|
||||
virtual std::optional<HotkeyEx> GetHotkeyEx() override
|
||||
{
|
||||
return m_hotkey;
|
||||
@@ -359,9 +365,85 @@ private:
|
||||
.modifiersMask = MOD_CONTROL | MOD_WIN,
|
||||
.vkCode = 0xC0, // VK_OEM_3 key; usually `~
|
||||
};
|
||||
|
||||
std::unique_ptr<pt::cli::IModuleCommandProvider> m_cliProvider;
|
||||
|
||||
pt::cli::CommandResult HandleList(const json::JsonObject& args) const;
|
||||
};
|
||||
|
||||
class WorkspacesCommandProvider final : public pt::cli::IModuleCommandProvider
|
||||
{
|
||||
public:
|
||||
explicit WorkspacesCommandProvider(const WorkspacesModuleInterface& owner) :
|
||||
m_owner(owner)
|
||||
{
|
||||
}
|
||||
|
||||
std::wstring ModuleKey() const override
|
||||
{
|
||||
return L"workspaces";
|
||||
}
|
||||
|
||||
std::vector<pt::cli::CommandDescriptor> DescribeCommands() const override
|
||||
{
|
||||
pt::cli::CommandDescriptor listDescriptor;
|
||||
listDescriptor.action = L"list";
|
||||
listDescriptor.description = L"List configured workspaces.";
|
||||
return { std::move(listDescriptor) };
|
||||
}
|
||||
|
||||
pt::cli::CommandResult Execute(const pt::cli::CommandInvocation& invocation) override
|
||||
{
|
||||
std::wstring action = invocation.action;
|
||||
std::transform(action.begin(), action.end(), action.begin(), [](wchar_t ch) {
|
||||
return static_cast<wchar_t>(std::towlower(ch));
|
||||
});
|
||||
|
||||
if (action == L"list")
|
||||
{
|
||||
return m_owner.HandleList(invocation.args);
|
||||
}
|
||||
|
||||
return pt::cli::CommandResult::Error(L"E_COMMAND_NOT_FOUND", L"Unsupported Workspaces command.");
|
||||
}
|
||||
|
||||
private:
|
||||
const WorkspacesModuleInterface& m_owner;
|
||||
};
|
||||
|
||||
pt::cli::IModuleCommandProvider* WorkspacesModuleInterface::command_provider()
|
||||
{
|
||||
if (!m_cliProvider)
|
||||
{
|
||||
m_cliProvider = std::make_unique<WorkspacesCommandProvider>(*this);
|
||||
}
|
||||
|
||||
return m_cliProvider.get();
|
||||
}
|
||||
|
||||
pt::cli::CommandResult WorkspacesModuleInterface::HandleList(const json::JsonObject& args) const
|
||||
{
|
||||
UNREFERENCED_PARAMETER(args);
|
||||
|
||||
json::JsonObject payload = json::JsonObject();
|
||||
auto workspacesPath = WorkspacesData::WorkspacesFile();
|
||||
payload.SetNamedValue(L"path", json::value(workspacesPath));
|
||||
|
||||
auto stored = json::from_file(workspacesPath);
|
||||
if (stored.has_value())
|
||||
{
|
||||
payload.SetNamedValue(L"data", json::value(*stored));
|
||||
}
|
||||
else
|
||||
{
|
||||
payload.SetNamedValue(L"data", json::value(json::JsonObject()));
|
||||
}
|
||||
|
||||
return pt::cli::CommandResult::Success(std::move(payload));
|
||||
}
|
||||
|
||||
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
|
||||
{
|
||||
return new WorkspacesModuleInterface();
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,13 @@
|
||||
#include <common/utils/resources.h>
|
||||
#include <common/utils/os-detect.h>
|
||||
#include <common/utils/winapi_error.h>
|
||||
#include <common/utils/json.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <cwctype>
|
||||
#include <optional>
|
||||
#include <filesystem>
|
||||
#include <set>
|
||||
|
||||
@@ -37,6 +43,105 @@ BOOL APIENTRY DllMain(HMODULE /*hModule*/, DWORD ul_reason_for_call, LPVOID /*lp
|
||||
const static wchar_t* MODULE_NAME = L"Awake";
|
||||
const static wchar_t* MODULE_DESC = L"A module that keeps your computer awake on-demand.";
|
||||
|
||||
namespace
|
||||
{
|
||||
std::wstring to_lower_copy(std::wstring value)
|
||||
{
|
||||
std::transform(value.begin(), value.end(), value.begin(), [](wchar_t ch) {
|
||||
return static_cast<wchar_t>(std::towlower(ch));
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
std::wstring mode_to_string(int mode)
|
||||
{
|
||||
switch (mode)
|
||||
{
|
||||
case 0:
|
||||
return L"passive";
|
||||
case 1:
|
||||
return L"indefinite";
|
||||
case 2:
|
||||
return L"timed";
|
||||
case 3:
|
||||
return L"expirable";
|
||||
default:
|
||||
return L"unknown";
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<uint32_t> parse_duration_string(const std::wstring& raw)
|
||||
{
|
||||
if (raw.empty())
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::wstring value = raw;
|
||||
double multiplier = 1.0;
|
||||
|
||||
wchar_t suffix = value.back();
|
||||
if (!iswdigit(suffix))
|
||||
{
|
||||
value.pop_back();
|
||||
if (suffix == L'h' || suffix == L'H')
|
||||
{
|
||||
multiplier = 60.0;
|
||||
}
|
||||
else if (suffix == L'm' || suffix == L'M')
|
||||
{
|
||||
multiplier = 1.0;
|
||||
}
|
||||
else
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
double numeric = std::stod(value);
|
||||
if (numeric < 0)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
double totalMinutes = numeric * multiplier;
|
||||
if (totalMinutes < 0 || totalMinutes > static_cast<double>(std::numeric_limits<uint32_t>::max()))
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return static_cast<uint32_t>(totalMinutes);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<uint32_t> extract_duration_minutes(const json::JsonObject& args)
|
||||
{
|
||||
if (args.HasKey(L"durationMinutes"))
|
||||
{
|
||||
auto value = args.GetNamedNumber(L"durationMinutes");
|
||||
if (value < 0)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
return static_cast<uint32_t>(value);
|
||||
}
|
||||
|
||||
if (args.HasKey(L"duration"))
|
||||
{
|
||||
auto asString = args.GetNamedString(L"duration");
|
||||
return parse_duration_string(asString.c_str());
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
class Awake : public PowertoyModuleIface
|
||||
{
|
||||
std::wstring app_name;
|
||||
@@ -45,6 +150,7 @@ class Awake : public PowertoyModuleIface
|
||||
private:
|
||||
bool m_enabled = false;
|
||||
PROCESS_INFORMATION p_info = {};
|
||||
std::unique_ptr<pt::cli::IModuleCommandProvider> m_cliProvider;
|
||||
|
||||
bool is_process_running()
|
||||
{
|
||||
@@ -176,9 +282,167 @@ public:
|
||||
{
|
||||
return m_enabled;
|
||||
}
|
||||
|
||||
pt::cli::IModuleCommandProvider* command_provider() override;
|
||||
|
||||
pt::cli::CommandResult HandleStatus() const;
|
||||
pt::cli::CommandResult HandleSet(const json::JsonObject& args);
|
||||
};
|
||||
|
||||
class AwakeCommandProvider final : public pt::cli::IModuleCommandProvider
|
||||
{
|
||||
public:
|
||||
explicit AwakeCommandProvider(Awake& owner) :
|
||||
m_owner(owner)
|
||||
{
|
||||
}
|
||||
|
||||
std::wstring ModuleKey() const override
|
||||
{
|
||||
return L"awake";
|
||||
}
|
||||
|
||||
std::vector<pt::cli::CommandDescriptor> DescribeCommands() const override
|
||||
{
|
||||
std::vector<pt::cli::CommandParameter> setParameters{
|
||||
{ L"mode", false, L"Awake mode: passive | indefinite | timed." },
|
||||
{ L"durationMinutes", false, L"Total duration in minutes for timed mode." },
|
||||
{ L"duration", false, L"Duration with unit (e.g. 30m, 2h) for timed mode." },
|
||||
{ L"displayOn", false, L"Whether to keep the display active (true/false)." },
|
||||
};
|
||||
|
||||
pt::cli::CommandDescriptor setDescriptor;
|
||||
setDescriptor.action = L"set";
|
||||
setDescriptor.description = L"Configure the Awake module.";
|
||||
setDescriptor.parameters = std::move(setParameters);
|
||||
|
||||
pt::cli::CommandDescriptor statusDescriptor;
|
||||
statusDescriptor.action = L"status";
|
||||
statusDescriptor.description = L"Inspect the current Awake mode.";
|
||||
|
||||
return { std::move(setDescriptor), std::move(statusDescriptor) };
|
||||
}
|
||||
|
||||
pt::cli::CommandResult Execute(const pt::cli::CommandInvocation& invocation) override
|
||||
{
|
||||
auto action = to_lower_copy(invocation.action);
|
||||
if (action == L"set")
|
||||
{
|
||||
return m_owner.HandleSet(invocation.args);
|
||||
}
|
||||
|
||||
if (action == L"status")
|
||||
{
|
||||
return m_owner.HandleStatus();
|
||||
}
|
||||
|
||||
return pt::cli::CommandResult::Error(L"E_COMMAND_NOT_FOUND", L"Unsupported Awake action.");
|
||||
}
|
||||
|
||||
private:
|
||||
Awake& m_owner;
|
||||
};
|
||||
|
||||
pt::cli::IModuleCommandProvider* Awake::command_provider()
|
||||
{
|
||||
if (!m_cliProvider)
|
||||
{
|
||||
m_cliProvider = std::make_unique<AwakeCommandProvider>(*this);
|
||||
}
|
||||
|
||||
return m_cliProvider.get();
|
||||
}
|
||||
|
||||
pt::cli::CommandResult Awake::HandleStatus() const
|
||||
{
|
||||
auto settings = PTSettingsHelper::load_module_settings(app_key);
|
||||
json::JsonObject payload = json::JsonObject();
|
||||
|
||||
if (!settings.HasKey(L"properties"))
|
||||
{
|
||||
payload.SetNamedValue(L"mode", json::value(L"unknown"));
|
||||
payload.SetNamedValue(L"keepDisplayOn", json::value(false));
|
||||
return pt::cli::CommandResult::Success(std::move(payload));
|
||||
}
|
||||
|
||||
auto properties = settings.GetNamedObject(L"properties");
|
||||
|
||||
const auto modeValue = static_cast<int>(properties.GetNamedNumber(L"mode", 0));
|
||||
payload.SetNamedValue(L"mode", json::value(mode_to_string(modeValue)));
|
||||
payload.SetNamedValue(L"modeValue", json::value(modeValue));
|
||||
payload.SetNamedValue(L"keepDisplayOn", json::value(properties.GetNamedBoolean(L"keepDisplayOn", false)));
|
||||
payload.SetNamedValue(L"intervalHours", json::value(static_cast<uint32_t>(properties.GetNamedNumber(L"intervalHours", 0))));
|
||||
payload.SetNamedValue(L"intervalMinutes", json::value(static_cast<uint32_t>(properties.GetNamedNumber(L"intervalMinutes", 0))));
|
||||
|
||||
if (properties.HasKey(L"expirationDateTime"))
|
||||
{
|
||||
payload.SetNamedValue(L"expirationDateTime", json::value(properties.GetNamedString(L"expirationDateTime")));
|
||||
}
|
||||
|
||||
return pt::cli::CommandResult::Success(std::move(payload));
|
||||
}
|
||||
|
||||
pt::cli::CommandResult Awake::HandleSet(const json::JsonObject& args)
|
||||
{
|
||||
std::wstring requestedMode = L"indefinite";
|
||||
if (args.HasKey(L"mode"))
|
||||
{
|
||||
requestedMode = to_lower_copy(std::wstring(args.GetNamedString(L"mode").c_str()));
|
||||
}
|
||||
|
||||
auto settings = PTSettingsHelper::load_module_settings(app_key);
|
||||
json::JsonObject properties = settings.HasKey(L"properties") ? settings.GetNamedObject(L"properties") : json::JsonObject();
|
||||
|
||||
const bool keepDisplayOn = args.GetNamedBoolean(L"displayOn", properties.GetNamedBoolean(L"keepDisplayOn", false));
|
||||
|
||||
int modeValue = 1; // default to indefinite
|
||||
if (requestedMode == L"passive")
|
||||
{
|
||||
modeValue = 0;
|
||||
}
|
||||
else if (requestedMode == L"indefinite" || requestedMode.empty())
|
||||
{
|
||||
modeValue = 1;
|
||||
}
|
||||
else if (requestedMode == L"timed")
|
||||
{
|
||||
modeValue = 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
return pt::cli::CommandResult::Error(L"E_ARGS_INVALID", L"Unsupported mode. Use passive, indefinite, or timed.");
|
||||
}
|
||||
|
||||
properties.SetNamedValue(L"keepDisplayOn", json::value(keepDisplayOn));
|
||||
properties.SetNamedValue(L"mode", json::value(modeValue));
|
||||
|
||||
if (modeValue == 2)
|
||||
{
|
||||
auto durationMinutes = extract_duration_minutes(args);
|
||||
if (!durationMinutes.has_value() || durationMinutes.value() == 0)
|
||||
{
|
||||
return pt::cli::CommandResult::Error(L"E_ARGS_INVALID", L"Timed mode requires a non-zero duration.");
|
||||
}
|
||||
|
||||
const uint32_t totalMinutes = durationMinutes.value();
|
||||
const uint32_t hours = totalMinutes / 60;
|
||||
const uint32_t minutes = totalMinutes % 60;
|
||||
properties.SetNamedValue(L"intervalHours", json::value(hours));
|
||||
properties.SetNamedValue(L"intervalMinutes", json::value(minutes));
|
||||
}
|
||||
else
|
||||
{
|
||||
properties.SetNamedValue(L"intervalHours", json::value(0));
|
||||
properties.SetNamedValue(L"intervalMinutes", json::value(0));
|
||||
}
|
||||
|
||||
settings.SetNamedValue(L"properties", json::value(properties));
|
||||
PTSettingsHelper::save_module_settings(app_key, settings);
|
||||
|
||||
return HandleStatus();
|
||||
}
|
||||
|
||||
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
|
||||
{
|
||||
return new Awake();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,8 +40,6 @@ namespace Microsoft.CmdPal.UI;
|
||||
/// </summary>
|
||||
public partial class App : Application
|
||||
{
|
||||
private readonly GlobalErrorHandler _globalErrorHandler = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current <see cref="App"/> instance in use.
|
||||
/// </summary>
|
||||
@@ -63,10 +61,6 @@ public partial class App : Application
|
||||
/// </summary>
|
||||
public App()
|
||||
{
|
||||
#if !CMDPAL_DISABLE_GLOBAL_ERROR_HANDLER
|
||||
_globalErrorHandler.Register(this);
|
||||
#endif
|
||||
|
||||
Services = ConfigureServices();
|
||||
|
||||
this.InitializeComponent();
|
||||
|
||||
@@ -1,134 +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 ManagedCommon;
|
||||
using Microsoft.CmdPal.Core.Common.Helpers;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
using SystemUnhandledExceptionEventArgs = System.UnhandledExceptionEventArgs;
|
||||
using XamlUnhandledExceptionEventArgs = Microsoft.UI.Xaml.UnhandledExceptionEventArgs;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Global error handler for Command Palette.
|
||||
/// </summary>
|
||||
internal sealed partial class GlobalErrorHandler
|
||||
{
|
||||
// GlobalErrorHandler is designed to be self-contained; it can be registered and invoked before a service provider is available.
|
||||
internal void Register(App app)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(app);
|
||||
|
||||
app.UnhandledException += App_UnhandledException;
|
||||
TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
|
||||
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
|
||||
}
|
||||
|
||||
private void App_UnhandledException(object sender, XamlUnhandledExceptionEventArgs e)
|
||||
{
|
||||
// Exceptions thrown on the main UI thread are handled here.
|
||||
if (e.Exception != null)
|
||||
{
|
||||
HandleException(e.Exception, Context.MainThreadException);
|
||||
}
|
||||
}
|
||||
|
||||
private void CurrentDomain_UnhandledException(object sender, SystemUnhandledExceptionEventArgs e)
|
||||
{
|
||||
// Exceptions thrown on background threads are handled here.
|
||||
if (e.ExceptionObject is Exception ex)
|
||||
{
|
||||
HandleException(ex, Context.AppDomainUnhandledException);
|
||||
}
|
||||
}
|
||||
|
||||
private void TaskScheduler_UnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e)
|
||||
{
|
||||
// This event is raised only when a faulted Task is garbage-collected
|
||||
// without its exception being observed. It is NOT raised immediately
|
||||
// when the Task faults; timing depends on GC finalization.
|
||||
e.SetObserved();
|
||||
HandleException(e.Exception, Context.UnobservedTaskException, isRecoverable: true);
|
||||
}
|
||||
|
||||
private void HandleException(Exception ex, Context context, bool isRecoverable = false)
|
||||
{
|
||||
Logger.LogError($"Unhandled exception detected ({context})", ex);
|
||||
|
||||
if (context == Context.MainThreadException)
|
||||
{
|
||||
var error = DiagnosticsHelper.BuildExceptionMessage(ex, null);
|
||||
var report = $"""
|
||||
This is an error report generated by Windows Command Palette.
|
||||
If you are seeing this message, it means the application has encountered an unexpected issue.
|
||||
You can help us fix it by filing a report at https://aka.ms/powerToysReportBug.
|
||||
{error}
|
||||
""";
|
||||
|
||||
StoreReport(report, storeOnDesktop: false);
|
||||
|
||||
PInvoke.MessageBox(
|
||||
HWND.Null,
|
||||
"Command Palette has encountered a fatal error and must close.\n\nAn error report has been saved to your desktop.",
|
||||
"Unhandled Error",
|
||||
MESSAGEBOX_STYLE.MB_ICONERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private static string? StoreReport(string report, bool storeOnDesktop)
|
||||
{
|
||||
// Generate a unique name for the report file; include timestamp and a random zero-padded number to avoid collisions
|
||||
// in case of crash storm.
|
||||
var name = FormattableString.Invariant($"CmdPal_ErrorReport_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{Random.Shared.Next(100000):D5}.log");
|
||||
|
||||
// Always store a copy in log directory, this way it is available for Bug Report Tool
|
||||
string? reportPath = null;
|
||||
if (Logger.CurrentVersionLogDirectoryPath != null)
|
||||
{
|
||||
reportPath = Save(report, name, static () => Logger.CurrentVersionLogDirectoryPath);
|
||||
}
|
||||
|
||||
// Optionally store a copy on the desktop for user (in)convenience
|
||||
if (storeOnDesktop)
|
||||
{
|
||||
var path = Save(report, name, static () => Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory));
|
||||
|
||||
// show the desktop copy if both succeeded
|
||||
if (path != null)
|
||||
{
|
||||
reportPath = path;
|
||||
}
|
||||
}
|
||||
|
||||
return reportPath;
|
||||
|
||||
static string? Save(string reportContent, string reportFileName, Func<string> directory)
|
||||
{
|
||||
try
|
||||
{
|
||||
var logDirectory = directory();
|
||||
Directory.CreateDirectory(logDirectory);
|
||||
var reportFilePath = Path.Combine(logDirectory, reportFileName);
|
||||
File.WriteAllText(reportFilePath, reportContent);
|
||||
return reportFilePath;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to store exception report", ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum Context
|
||||
{
|
||||
Unknown = 0,
|
||||
MainThreadException,
|
||||
BackgroundThreadException,
|
||||
UnobservedTaskException,
|
||||
AppDomainUnhandledException,
|
||||
}
|
||||
}
|
||||
@@ -110,7 +110,7 @@ public partial class BookmarkResolverTests
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Drive",
|
||||
Input: "C:\\.",
|
||||
Input: "C:",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.Directory,
|
||||
ExpectedTarget: "C:\\",
|
||||
|
||||
@@ -8,6 +8,7 @@ using System;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
|
||||
using ImageResizer.Models;
|
||||
using ImageResizer.Properties;
|
||||
using ImageResizer.Utilities;
|
||||
@@ -19,32 +20,8 @@ namespace ImageResizer
|
||||
{
|
||||
public partial class App : Application, IDisposable
|
||||
{
|
||||
private const string LogSubFolder = "\\ImageResizer\\Logs";
|
||||
|
||||
/// <summary>
|
||||
/// Gets cached AI availability state, checked at app startup.
|
||||
/// Can be updated after model download completes or background initialization.
|
||||
/// </summary>
|
||||
public static AiAvailabilityState AiAvailabilityState { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Event fired when AI initialization completes in background.
|
||||
/// Allows UI to refresh state when initialization finishes.
|
||||
/// </summary>
|
||||
public static event EventHandler<AiAvailabilityState> AiInitializationCompleted;
|
||||
|
||||
static App()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Initialize logger early (mirroring PowerOCR pattern)
|
||||
Logger.InitializeLogger(LogSubFolder);
|
||||
}
|
||||
catch
|
||||
{
|
||||
/* swallow logger init issues silently */
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string appLanguage = LanguageHelper.LoadLanguage();
|
||||
@@ -53,9 +30,9 @@ namespace ImageResizer
|
||||
System.Threading.Thread.CurrentThread.CurrentUICulture = new CultureInfo(appLanguage);
|
||||
}
|
||||
}
|
||||
catch (CultureNotFoundException ex)
|
||||
catch (CultureNotFoundException)
|
||||
{
|
||||
Logger.LogError("CultureNotFoundException: " + ex.Message);
|
||||
// error
|
||||
}
|
||||
|
||||
Console.InputEncoding = Encoding.Unicode;
|
||||
@@ -71,115 +48,23 @@ namespace ImageResizer
|
||||
/* TODO: Add logs to ImageResizer.
|
||||
* Logger.LogWarning("Tried to start with a GPO policy setting the utility to always be disabled. Please contact your systems administrator.");
|
||||
*/
|
||||
Logger.LogWarning("GPO policy disables ImageResizer. Exiting.");
|
||||
Environment.Exit(0); // Current.Exit won't work until there's a window opened.
|
||||
return;
|
||||
}
|
||||
|
||||
// Check AI availability at startup (not relying on cached settings)
|
||||
AiAvailabilityState = CheckAiAvailability();
|
||||
Logger.LogInfo($"AI availability checked at startup: {AiAvailabilityState}");
|
||||
|
||||
// If AI is potentially available, start background initialization (non-blocking)
|
||||
if (AiAvailabilityState == AiAvailabilityState.Ready)
|
||||
{
|
||||
_ = InitializeAiServiceAsync(); // Fire and forget - don't block UI
|
||||
}
|
||||
else
|
||||
{
|
||||
// AI not available - set NoOp service immediately
|
||||
ResizeBatch.SetAiSuperResolutionService(Services.NoOpAiSuperResolutionService.Instance);
|
||||
}
|
||||
|
||||
var batch = ResizeBatch.FromCommandLine(Console.In, e?.Args);
|
||||
|
||||
// TODO: Add command-line parameters that can be used in lieu of the input page (issue #14)
|
||||
var mainWindow = new MainWindow(new MainViewModel(batch, Settings.Default));
|
||||
mainWindow.Show();
|
||||
Logger.LogInfo("MainWindow shown (unpackaged or activation fallback path).");
|
||||
|
||||
// Temporary workaround for issue #1273
|
||||
WindowHelpers.BringToForeground(new System.Windows.Interop.WindowInteropHelper(mainWindow).Handle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check AI Super Resolution availability on this system.
|
||||
/// Performs architecture check and model availability check.
|
||||
/// </summary>
|
||||
private static AiAvailabilityState CheckAiAvailability()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Check Windows AI service model ready state
|
||||
// it's so slow, why?
|
||||
var readyState = Services.WinAiSuperResolutionService.GetModelReadyState();
|
||||
|
||||
// Map AI service state to our availability state
|
||||
switch (readyState)
|
||||
{
|
||||
case Microsoft.Windows.AI.AIFeatureReadyState.Ready:
|
||||
return AiAvailabilityState.Ready;
|
||||
|
||||
case Microsoft.Windows.AI.AIFeatureReadyState.NotReady:
|
||||
return AiAvailabilityState.ModelNotReady;
|
||||
|
||||
case Microsoft.Windows.AI.AIFeatureReadyState.DisabledByUser:
|
||||
case Microsoft.Windows.AI.AIFeatureReadyState.NotSupportedOnCurrentSystem:
|
||||
default:
|
||||
return AiAvailabilityState.NotSupported;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return AiAvailabilityState.NotSupported;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize AI Super Resolution service asynchronously in background.
|
||||
/// Runs without blocking UI startup - state change event notifies completion.
|
||||
/// </summary>
|
||||
private static async System.Threading.Tasks.Task InitializeAiServiceAsync()
|
||||
{
|
||||
AiAvailabilityState finalState;
|
||||
|
||||
try
|
||||
{
|
||||
// Create and initialize AI service using async factory
|
||||
var aiService = await Services.WinAiSuperResolutionService.CreateAsync();
|
||||
|
||||
if (aiService != null)
|
||||
{
|
||||
ResizeBatch.SetAiSuperResolutionService(aiService);
|
||||
Logger.LogInfo("AI Super Resolution service initialized successfully.");
|
||||
finalState = AiAvailabilityState.Ready;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Initialization failed - fallback to NoOp
|
||||
ResizeBatch.SetAiSuperResolutionService(Services.NoOpAiSuperResolutionService.Instance);
|
||||
Logger.LogWarning("AI Super Resolution service initialization failed. Using fallback.");
|
||||
finalState = AiAvailabilityState.NotSupported;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Log error and use NoOp service as fallback
|
||||
ResizeBatch.SetAiSuperResolutionService(Services.NoOpAiSuperResolutionService.Instance);
|
||||
Logger.LogError($"Exception during AI service initialization: {ex.Message}");
|
||||
finalState = AiAvailabilityState.NotSupported;
|
||||
}
|
||||
|
||||
// Update cached state and notify listeners
|
||||
AiAvailabilityState = finalState;
|
||||
AiInitializationCompleted?.Invoke(null, finalState);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Dispose AI Super Resolution service
|
||||
ResizeBatch.DisposeAiSuperResolutionService();
|
||||
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
|
||||
<UseWPF>true</UseWPF>
|
||||
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
@@ -19,7 +18,6 @@
|
||||
<RootNamespace>ImageResizer</RootNamespace>
|
||||
<AssemblyName>PowerToys.ImageResizer</AssemblyName>
|
||||
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
@@ -48,7 +46,6 @@
|
||||
<Resource Include="Resources\ImageResizer.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK.AI" />
|
||||
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" />
|
||||
<PackageReference Include="System.IO.Abstractions" />
|
||||
<PackageReference Include="WPF-UI" />
|
||||
|
||||
@@ -1,48 +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.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
using ImageResizer.Properties;
|
||||
|
||||
namespace ImageResizer.Models
|
||||
{
|
||||
public class AiSize : ResizeSize
|
||||
{
|
||||
private static readonly CompositeFormat ScaleFormat = CompositeFormat.Parse(Resources.Input_AiScaleFormat);
|
||||
private int _scale = 2;
|
||||
|
||||
[JsonIgnore]
|
||||
public override string Name
|
||||
{
|
||||
get => Resources.Input_AiSuperResolution;
|
||||
set { /* no-op */ }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the formatted scale display string (e.g., "2×").
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public string ScaleDisplay => string.Format(CultureInfo.CurrentCulture, ScaleFormat, _scale);
|
||||
|
||||
[JsonPropertyName("scale")]
|
||||
public int Scale
|
||||
{
|
||||
get => _scale;
|
||||
set => Set(ref _scale, value);
|
||||
}
|
||||
|
||||
[JsonConstructor]
|
||||
public AiSize(int scale)
|
||||
{
|
||||
Scale = scale;
|
||||
}
|
||||
|
||||
public AiSize()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,30 +15,17 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using ImageResizer.Properties;
|
||||
using ImageResizer.Services;
|
||||
|
||||
namespace ImageResizer.Models
|
||||
{
|
||||
public class ResizeBatch
|
||||
{
|
||||
private readonly IFileSystem _fileSystem = new FileSystem();
|
||||
private static IAISuperResolutionService _aiSuperResolutionService;
|
||||
|
||||
public string DestinationDirectory { get; set; }
|
||||
|
||||
public ICollection<string> Files { get; } = new List<string>();
|
||||
|
||||
public static void SetAiSuperResolutionService(IAISuperResolutionService service)
|
||||
{
|
||||
_aiSuperResolutionService = service;
|
||||
}
|
||||
|
||||
public static void DisposeAiSuperResolutionService()
|
||||
{
|
||||
_aiSuperResolutionService?.Dispose();
|
||||
_aiSuperResolutionService = null;
|
||||
}
|
||||
|
||||
public static ResizeBatch FromCommandLine(TextReader standardInput, string[] args)
|
||||
{
|
||||
var batch = new ResizeBatch();
|
||||
@@ -132,9 +119,6 @@ namespace ImageResizer.Models
|
||||
}
|
||||
|
||||
protected virtual void Execute(string file)
|
||||
{
|
||||
var aiService = _aiSuperResolutionService ?? NoOpAiSuperResolutionService.Instance;
|
||||
new ResizeOperation(file, DestinationDirectory, Settings.Default, aiService).Execute();
|
||||
}
|
||||
=> new ResizeOperation(file, DestinationDirectory, Settings.Default).Execute();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,14 +10,12 @@ using System.Globalization;
|
||||
using System.IO;
|
||||
using System.IO.Abstractions;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
using ImageResizer.Extensions;
|
||||
using ImageResizer.Properties;
|
||||
using ImageResizer.Services;
|
||||
using ImageResizer.Utilities;
|
||||
using Microsoft.VisualBasic.FileIO;
|
||||
|
||||
@@ -32,10 +30,6 @@ namespace ImageResizer.Models
|
||||
private readonly string _file;
|
||||
private readonly string _destinationDirectory;
|
||||
private readonly Settings _settings;
|
||||
private readonly IAISuperResolutionService _aiSuperResolutionService;
|
||||
|
||||
// Cache CompositeFormat for AI error message formatting (CA1863)
|
||||
private static readonly CompositeFormat _aiErrorFormat = CompositeFormat.Parse(Resources.Error_AiProcessingFailed);
|
||||
|
||||
// Filenames to avoid according to https://learn.microsoft.com/windows/win32/fileio/naming-a-file#file-and-directory-names
|
||||
private static readonly string[] _avoidFilenames =
|
||||
@@ -45,12 +39,11 @@ namespace ImageResizer.Models
|
||||
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
|
||||
};
|
||||
|
||||
public ResizeOperation(string file, string destinationDirectory, Settings settings, IAISuperResolutionService aiSuperResolutionService = null)
|
||||
public ResizeOperation(string file, string destinationDirectory, Settings settings)
|
||||
{
|
||||
_file = file;
|
||||
_destinationDirectory = destinationDirectory;
|
||||
_settings = settings;
|
||||
_aiSuperResolutionService = aiSuperResolutionService ?? NoOpAiSuperResolutionService.Instance;
|
||||
}
|
||||
|
||||
public void Execute()
|
||||
@@ -174,11 +167,6 @@ namespace ImageResizer.Models
|
||||
|
||||
private BitmapSource Transform(BitmapSource source)
|
||||
{
|
||||
if (_settings.SelectedSize is AiSize)
|
||||
{
|
||||
return TransformWithAi(source);
|
||||
}
|
||||
|
||||
var originalWidth = source.PixelWidth;
|
||||
var originalHeight = source.PixelHeight;
|
||||
var width = _settings.SelectedSize.GetPixelWidth(originalWidth, source.DpiX);
|
||||
@@ -229,31 +217,6 @@ namespace ImageResizer.Models
|
||||
return scaledBitmap;
|
||||
}
|
||||
|
||||
private BitmapSource TransformWithAi(BitmapSource source)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = _aiSuperResolutionService.ApplySuperResolution(
|
||||
source,
|
||||
_settings.AiSize.Scale,
|
||||
_file);
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
throw new InvalidOperationException(Properties.Resources.Error_AiConversionFailed);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Wrap the exception with a localized message
|
||||
// This will be caught by ResizeBatch.Process() and displayed to the user
|
||||
var errorMessage = string.Format(CultureInfo.CurrentCulture, _aiErrorFormat, ex.Message);
|
||||
throw new InvalidOperationException(errorMessage, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks original metadata by writing an image containing the given metadata into a memory stream.
|
||||
/// In case of errors, we try to rebuild the metadata object and check again.
|
||||
@@ -360,24 +323,19 @@ namespace ImageResizer.Models
|
||||
}
|
||||
|
||||
// Remove directory characters from the size's name.
|
||||
// For AI Size, use the scale display (e.g., "2×") instead of the full name
|
||||
string sizeName = _settings.SelectedSize is AiSize aiSize
|
||||
? aiSize.ScaleDisplay
|
||||
: _settings.SelectedSize.Name;
|
||||
string sizeNameSanitized = sizeName
|
||||
string sizeNameSanitized = _settings.SelectedSize.Name;
|
||||
sizeNameSanitized = sizeNameSanitized
|
||||
.Replace('\\', '_')
|
||||
.Replace('/', '_');
|
||||
|
||||
// Using CurrentCulture since this is user facing
|
||||
var selectedWidth = _settings.SelectedSize is AiSize ? encoder.Frames[0].PixelWidth : _settings.SelectedSize.Width;
|
||||
var selectedHeight = _settings.SelectedSize is AiSize ? encoder.Frames[0].PixelHeight : _settings.SelectedSize.Height;
|
||||
var fileName = string.Format(
|
||||
CultureInfo.CurrentCulture,
|
||||
_settings.FileNameFormat,
|
||||
originalFileName,
|
||||
sizeNameSanitized,
|
||||
selectedWidth,
|
||||
selectedHeight,
|
||||
_settings.SelectedSize.Width,
|
||||
_settings.SelectedSize.Height,
|
||||
encoder.Frames[0].PixelWidth,
|
||||
encoder.Frames[0].PixelHeight);
|
||||
|
||||
|
||||
@@ -194,187 +194,7 @@ namespace ImageResizer.Properties {
|
||||
return ResourceManager.GetString("Input_ShrinkOnly.Content", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to AI Super Resolution.
|
||||
/// </summary>
|
||||
public static string Input_AiSuperResolution {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiSuperResolution", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Original size.
|
||||
/// </summary>
|
||||
public static string Input_AiOriginalSizeLabel {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiOriginalSizeLabel", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Enhanced size.
|
||||
/// </summary>
|
||||
public static string Input_AiEnhancedSizeLabel {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiEnhancedSizeLabel", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Unavailable.
|
||||
/// </summary>
|
||||
public static string Input_AiUnknownSize {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiUnknownSize", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Multiple files selected.
|
||||
/// </summary>
|
||||
public static string Input_AiMultipleFiles {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiMultipleFiles", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to {0}×.
|
||||
/// </summary>
|
||||
public static string Input_AiScaleFormat {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiScaleFormat", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Upscale to {0} times the original size..
|
||||
/// </summary>
|
||||
public static string Input_AiScaleHelp {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiScaleHelp", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Scale.
|
||||
/// </summary>
|
||||
public static string Input_AiScaleLabel {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiScaleLabel", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Current.
|
||||
/// </summary>
|
||||
public static string Input_AiCurrentLabel {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiCurrentLabel", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to New.
|
||||
/// </summary>
|
||||
public static string Input_AiNewLabel {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiNewLabel", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Checking AI model availability...
|
||||
/// </summary>
|
||||
public static string Input_AiModelChecking {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiModelChecking", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to AI model not downloaded. Click Download to get started.
|
||||
/// </summary>
|
||||
public static string Input_AiModelNotAvailable {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiModelNotAvailable", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to AI feature is disabled by system settings.
|
||||
/// </summary>
|
||||
public static string Input_AiModelDisabledByUser {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiModelDisabledByUser", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to AI feature is not supported on this system.
|
||||
/// </summary>
|
||||
public static string Input_AiModelNotSupported {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiModelNotSupported", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Downloading AI model...
|
||||
/// </summary>
|
||||
public static string Input_AiModelDownloading {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiModelDownloading", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Failed to download AI model. Please try again.
|
||||
/// </summary>
|
||||
public static string Input_AiModelDownloadFailed {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiModelDownloadFailed", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Download AI Model.
|
||||
/// </summary>
|
||||
public static string Input_AiModelDownloadButton {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiModelDownloadButton", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to AI super resolution processing failed: {0}.
|
||||
/// </summary>
|
||||
public static string Error_AiProcessingFailed {
|
||||
get {
|
||||
return ResourceManager.GetString("Error_AiProcessingFailed", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Failed to convert image format for AI processing..
|
||||
/// </summary>
|
||||
public static string Error_AiConversionFailed {
|
||||
get {
|
||||
return ResourceManager.GetString("Error_AiConversionFailed", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to AI scaling operation failed..
|
||||
/// </summary>
|
||||
public static string Error_AiScalingFailed {
|
||||
get {
|
||||
return ResourceManager.GetString("Error_AiScalingFailed", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Large.
|
||||
/// </summary>
|
||||
|
||||
@@ -296,52 +296,4 @@
|
||||
<data name="Input_ShrinkOnly.Content" xml:space="preserve">
|
||||
<value>_Make pictures smaller but not larger</value>
|
||||
</data>
|
||||
<data name="Input_AiSuperResolution" xml:space="preserve">
|
||||
<value>AI Super Resolution</value>
|
||||
</data>
|
||||
<data name="Input_AiUnknownSize" xml:space="preserve">
|
||||
<value>Unavailable</value>
|
||||
</data>
|
||||
<data name="Input_AiScaleFormat" xml:space="preserve">
|
||||
<value>{0}×</value>
|
||||
</data>
|
||||
<data name="Input_AiScaleLabel" xml:space="preserve">
|
||||
<value>Scale</value>
|
||||
</data>
|
||||
<data name="Input_AiCurrentLabel" xml:space="preserve">
|
||||
<value>Current</value>
|
||||
</data>
|
||||
<data name="Input_AiNewLabel" xml:space="preserve">
|
||||
<value>New</value>
|
||||
</data>
|
||||
<data name="Input_AiModelChecking" xml:space="preserve">
|
||||
<value>Checking AI model availability...</value>
|
||||
</data>
|
||||
<data name="Input_AiModelNotAvailable" xml:space="preserve">
|
||||
<value>AI model not downloaded. Click Download to get started.</value>
|
||||
</data>
|
||||
<data name="Input_AiModelDisabledByUser" xml:space="preserve">
|
||||
<value>AI feature is disabled by system settings.</value>
|
||||
</data>
|
||||
<data name="Input_AiModelNotSupported" xml:space="preserve">
|
||||
<value>AI feature is not supported on this system.</value>
|
||||
</data>
|
||||
<data name="Input_AiModelDownloading" xml:space="preserve">
|
||||
<value>Downloading AI model...</value>
|
||||
</data>
|
||||
<data name="Input_AiModelDownloadFailed" xml:space="preserve">
|
||||
<value>Failed to download AI model. Please try again.</value>
|
||||
</data>
|
||||
<data name="Input_AiModelDownloadButton" xml:space="preserve">
|
||||
<value>Download AI Model</value>
|
||||
</data>
|
||||
<data name="Error_AiProcessingFailed" xml:space="preserve">
|
||||
<value>AI super resolution processing failed: {0}</value>
|
||||
</data>
|
||||
<data name="Error_AiConversionFailed" xml:space="preserve">
|
||||
<value>Failed to convert image format for AI processing.</value>
|
||||
</data>
|
||||
<data name="Error_AiScalingFailed" xml:space="preserve">
|
||||
<value>AI scaling operation failed.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -19,22 +19,10 @@ using System.Threading;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
using ImageResizer.Models;
|
||||
using ImageResizer.Services;
|
||||
using ImageResizer.ViewModels;
|
||||
using ManagedCommon;
|
||||
|
||||
namespace ImageResizer.Properties
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the availability state of AI Super Resolution feature.
|
||||
/// </summary>
|
||||
public enum AiAvailabilityState
|
||||
{
|
||||
NotSupported, // System doesn't support AI (architecture issue or policy disabled)
|
||||
ModelNotReady, // AI supported but model not downloaded
|
||||
Ready, // AI fully ready to use
|
||||
}
|
||||
|
||||
public sealed partial class Settings : IDataErrorInfo, INotifyPropertyChanged
|
||||
{
|
||||
private static readonly IFileSystem _fileSystem = new FileSystem();
|
||||
@@ -62,7 +50,6 @@ namespace ImageResizer.Properties
|
||||
private bool _keepDateModified;
|
||||
private System.Guid _fallbackEncoder;
|
||||
private CustomSize _customSize;
|
||||
private AiSize _aiSize;
|
||||
|
||||
public Settings()
|
||||
{
|
||||
@@ -85,28 +72,9 @@ namespace ImageResizer.Properties
|
||||
KeepDateModified = false;
|
||||
FallbackEncoder = new System.Guid("19e4a5aa-5662-4fc5-a0c0-1758028e1057");
|
||||
CustomSize = new CustomSize(ResizeFit.Fit, 1024, 640, ResizeUnit.Pixel);
|
||||
AiSize = new AiSize(2); // Initialize with default scale of 2
|
||||
AllSizes = new AllSizesCollection(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the SelectedSizeIndex to ensure it's within the valid range.
|
||||
/// This handles cross-device migration where settings saved on ARM64 with AI selected
|
||||
/// are loaded on non-ARM64 devices.
|
||||
/// </summary>
|
||||
private void ValidateSelectedSizeIndex()
|
||||
{
|
||||
// Index structure: 0 to Sizes.Count-1 (regular), Sizes.Count (CustomSize), Sizes.Count+1 (AiSize)
|
||||
var maxIndex = ImageResizer.App.AiAvailabilityState == AiAvailabilityState.NotSupported
|
||||
? Sizes.Count // CustomSize only
|
||||
: Sizes.Count + 1; // CustomSize + AiSize
|
||||
|
||||
if (_selectedSizeIndex > maxIndex)
|
||||
{
|
||||
_selectedSizeIndex = 0; // Reset to first size
|
||||
}
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public IEnumerable<ResizeSize> AllSizes { get; set; }
|
||||
|
||||
@@ -126,35 +94,15 @@ namespace ImageResizer.Properties
|
||||
[JsonIgnore]
|
||||
public ResizeSize SelectedSize
|
||||
{
|
||||
get
|
||||
{
|
||||
if (SelectedSizeIndex >= 0 && SelectedSizeIndex < Sizes.Count)
|
||||
{
|
||||
return Sizes[SelectedSizeIndex];
|
||||
}
|
||||
else if (SelectedSizeIndex == Sizes.Count)
|
||||
{
|
||||
return CustomSize;
|
||||
}
|
||||
else
|
||||
{
|
||||
return AiSize;
|
||||
}
|
||||
}
|
||||
|
||||
get => SelectedSizeIndex >= 0 && SelectedSizeIndex < Sizes.Count
|
||||
? Sizes[SelectedSizeIndex]
|
||||
: CustomSize;
|
||||
set
|
||||
{
|
||||
var index = Sizes.IndexOf(value);
|
||||
if (index == -1)
|
||||
{
|
||||
if (value is AiSize)
|
||||
{
|
||||
index = Sizes.Count + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
index = Sizes.Count;
|
||||
}
|
||||
index = Sizes.Count;
|
||||
}
|
||||
|
||||
SelectedSizeIndex = index;
|
||||
@@ -190,17 +138,13 @@ namespace ImageResizer.Properties
|
||||
|
||||
private class AllSizesCollection : IEnumerable<ResizeSize>, INotifyCollectionChanged, INotifyPropertyChanged
|
||||
{
|
||||
private readonly Settings _settings;
|
||||
private ObservableCollection<ResizeSize> _sizes;
|
||||
private CustomSize _customSize;
|
||||
private AiSize _aiSize;
|
||||
|
||||
public AllSizesCollection(Settings settings)
|
||||
{
|
||||
_settings = settings;
|
||||
_sizes = settings.Sizes;
|
||||
_customSize = settings.CustomSize;
|
||||
_aiSize = settings.AiSize;
|
||||
|
||||
_sizes.CollectionChanged += HandleCollectionChanged;
|
||||
((INotifyPropertyChanged)_sizes).PropertyChanged += HandlePropertyChanged;
|
||||
@@ -219,18 +163,6 @@ namespace ImageResizer.Properties
|
||||
oldCustomSize,
|
||||
_sizes.Count));
|
||||
}
|
||||
else if (e.PropertyName == nameof(Models.AiSize))
|
||||
{
|
||||
var oldAiSize = _aiSize;
|
||||
_aiSize = settings.AiSize;
|
||||
|
||||
OnCollectionChanged(
|
||||
new NotifyCollectionChangedEventArgs(
|
||||
NotifyCollectionChangedAction.Replace,
|
||||
_aiSize,
|
||||
oldAiSize,
|
||||
_sizes.Count + 1));
|
||||
}
|
||||
else if (e.PropertyName == nameof(Sizes))
|
||||
{
|
||||
var oldSizes = _sizes;
|
||||
@@ -253,30 +185,12 @@ namespace ImageResizer.Properties
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
public int Count
|
||||
=> _sizes.Count + 1 + (ImageResizer.App.AiAvailabilityState != AiAvailabilityState.NotSupported ? 1 : 0);
|
||||
=> _sizes.Count + 1;
|
||||
|
||||
public ResizeSize this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
if (index < _sizes.Count)
|
||||
{
|
||||
return _sizes[index];
|
||||
}
|
||||
else if (index == _sizes.Count)
|
||||
{
|
||||
return _customSize;
|
||||
}
|
||||
else if (ImageResizer.App.AiAvailabilityState != AiAvailabilityState.NotSupported && index == _sizes.Count + 1)
|
||||
{
|
||||
return _aiSize;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(index), index, $"Index {index} is out of range for AllSizesCollection.");
|
||||
}
|
||||
}
|
||||
}
|
||||
=> index == _sizes.Count
|
||||
? _customSize
|
||||
: _sizes[index];
|
||||
|
||||
public IEnumerator<ResizeSize> GetEnumerator()
|
||||
=> new AllSizesEnumerator(this);
|
||||
@@ -496,18 +410,6 @@ namespace ImageResizer.Properties
|
||||
}
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(WrappedJsonValueConverter))]
|
||||
[JsonPropertyName("imageresizer_aiSize")]
|
||||
public AiSize AiSize
|
||||
{
|
||||
get => _aiSize;
|
||||
set
|
||||
{
|
||||
_aiSize = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public static string SettingsPath { get => _settingsPath; set => _settingsPath = value; }
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
@@ -573,7 +475,6 @@ namespace ImageResizer.Properties
|
||||
KeepDateModified = jsonSettings.KeepDateModified;
|
||||
FallbackEncoder = jsonSettings.FallbackEncoder;
|
||||
CustomSize = jsonSettings.CustomSize;
|
||||
AiSize = jsonSettings.AiSize ?? new AiSize(InputViewModel.DefaultAiScale);
|
||||
SelectedSizeIndex = jsonSettings.SelectedSizeIndex;
|
||||
|
||||
if (jsonSettings.Sizes.Count > 0)
|
||||
@@ -584,10 +485,6 @@ namespace ImageResizer.Properties
|
||||
// Ensure Ids are unique and handle missing Ids
|
||||
IdRecoveryHelper.RecoverInvalidIds(Sizes);
|
||||
}
|
||||
|
||||
// Validate SelectedSizeIndex after Sizes collection has been updated
|
||||
// This handles cross-device migration (e.g., ARM64 -> non-ARM64)
|
||||
ValidateSelectedSizeIndex();
|
||||
});
|
||||
|
||||
_jsonMutex.ReleaseMutex();
|
||||
|
||||
@@ -1,14 +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.Windows.Media.Imaging;
|
||||
|
||||
namespace ImageResizer.Services
|
||||
{
|
||||
public interface IAISuperResolutionService : IDisposable
|
||||
{
|
||||
BitmapSource ApplySuperResolution(BitmapSource source, int scale, string filePath);
|
||||
}
|
||||
}
|
||||
@@ -1,27 +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.Windows.Media.Imaging;
|
||||
|
||||
namespace ImageResizer.Services
|
||||
{
|
||||
public sealed class NoOpAiSuperResolutionService : IAISuperResolutionService
|
||||
{
|
||||
public static NoOpAiSuperResolutionService Instance { get; } = new NoOpAiSuperResolutionService();
|
||||
|
||||
private NoOpAiSuperResolutionService()
|
||||
{
|
||||
}
|
||||
|
||||
public BitmapSource ApplySuperResolution(BitmapSource source, int scale, string filePath)
|
||||
{
|
||||
return source;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// No resources to dispose in no-op implementation
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,261 +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.Runtime.InteropServices;
|
||||
using System.Runtime.InteropServices.WindowsRuntime;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using Microsoft.Windows.AI;
|
||||
using Microsoft.Windows.AI.Imaging;
|
||||
using Windows.Graphics.Imaging;
|
||||
|
||||
namespace ImageResizer.Services
|
||||
{
|
||||
public sealed class WinAiSuperResolutionService : IAISuperResolutionService
|
||||
{
|
||||
private readonly ImageScaler _imageScaler;
|
||||
private readonly object _usageLock = new object();
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WinAiSuperResolutionService"/> class.
|
||||
/// Private constructor. Use CreateAsync() factory method to create instances.
|
||||
/// </summary>
|
||||
private WinAiSuperResolutionService(ImageScaler imageScaler)
|
||||
{
|
||||
_imageScaler = imageScaler ?? throw new ArgumentNullException(nameof(imageScaler));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Async factory method to create and initialize WinAiSuperResolutionService.
|
||||
/// Returns null if initialization fails.
|
||||
/// </summary>
|
||||
public static async Task<WinAiSuperResolutionService> CreateAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var imageScaler = await ImageScaler.CreateAsync();
|
||||
if (imageScaler == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new WinAiSuperResolutionService(imageScaler);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static AIFeatureReadyState GetModelReadyState()
|
||||
{
|
||||
try
|
||||
{
|
||||
return ImageScaler.GetReadyState();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// If we can't get the state, treat it as disabled by user
|
||||
// The caller should check if it's Ready or NotReady
|
||||
return AIFeatureReadyState.DisabledByUser;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<AIFeatureReadyResult> EnsureModelReadyAsync(IProgress<double> progress = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var operation = ImageScaler.EnsureReadyAsync();
|
||||
|
||||
// Register progress handler if provided
|
||||
if (progress != null)
|
||||
{
|
||||
operation.Progress = (asyncInfo, progressValue) =>
|
||||
{
|
||||
// progressValue is a double representing completion percentage (0.0 to 1.0 or 0 to 100)
|
||||
progress.Report(progressValue);
|
||||
};
|
||||
}
|
||||
|
||||
return await operation;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public BitmapSource ApplySuperResolution(BitmapSource source, int scale, string filePath)
|
||||
{
|
||||
if (source == null || _disposed)
|
||||
{
|
||||
return source;
|
||||
}
|
||||
|
||||
// Note: filePath parameter reserved for future use (e.g., logging, caching)
|
||||
// Currently not used by the ImageScaler API
|
||||
try
|
||||
{
|
||||
// Convert WPF BitmapSource to WinRT SoftwareBitmap
|
||||
var softwareBitmap = ConvertBitmapSourceToSoftwareBitmap(source);
|
||||
if (softwareBitmap == null)
|
||||
{
|
||||
return source;
|
||||
}
|
||||
|
||||
// Calculate target dimensions
|
||||
var newWidth = softwareBitmap.PixelWidth * scale;
|
||||
var newHeight = softwareBitmap.PixelHeight * scale;
|
||||
|
||||
// Apply super resolution with thread-safe access
|
||||
// _usageLock protects concurrent access from Parallel.ForEach threads
|
||||
SoftwareBitmap scaledBitmap;
|
||||
lock (_usageLock)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return source;
|
||||
}
|
||||
|
||||
scaledBitmap = _imageScaler.ScaleSoftwareBitmap(softwareBitmap, newWidth, newHeight);
|
||||
}
|
||||
|
||||
if (scaledBitmap == null)
|
||||
{
|
||||
return source;
|
||||
}
|
||||
|
||||
// Convert back to WPF BitmapSource
|
||||
return ConvertSoftwareBitmapToBitmapSource(scaledBitmap);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Any error, return original image gracefully
|
||||
return source;
|
||||
}
|
||||
}
|
||||
|
||||
private static SoftwareBitmap ConvertBitmapSourceToSoftwareBitmap(BitmapSource bitmapSource)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Ensure the bitmap is in a compatible format
|
||||
var convertedBitmap = new FormatConvertedBitmap();
|
||||
convertedBitmap.BeginInit();
|
||||
convertedBitmap.Source = bitmapSource;
|
||||
convertedBitmap.DestinationFormat = PixelFormats.Bgra32;
|
||||
convertedBitmap.EndInit();
|
||||
|
||||
int width = convertedBitmap.PixelWidth;
|
||||
int height = convertedBitmap.PixelHeight;
|
||||
int stride = width * 4; // 4 bytes per pixel for Bgra32
|
||||
byte[] pixels = new byte[height * stride];
|
||||
|
||||
convertedBitmap.CopyPixels(pixels, stride, 0);
|
||||
|
||||
// Create SoftwareBitmap from pixel data
|
||||
var softwareBitmap = new SoftwareBitmap(
|
||||
BitmapPixelFormat.Bgra8,
|
||||
width,
|
||||
height,
|
||||
BitmapAlphaMode.Premultiplied);
|
||||
|
||||
using (var buffer = softwareBitmap.LockBuffer(BitmapBufferAccessMode.Write))
|
||||
using (var reference = buffer.CreateReference())
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
((IMemoryBufferByteAccess)reference).GetBuffer(out byte* dataInBytes, out uint capacity);
|
||||
System.Runtime.InteropServices.Marshal.Copy(pixels, 0, (IntPtr)dataInBytes, pixels.Length);
|
||||
}
|
||||
}
|
||||
|
||||
return softwareBitmap;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static BitmapSource ConvertSoftwareBitmapToBitmapSource(SoftwareBitmap softwareBitmap)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Convert to Bgra8 format if needed
|
||||
var convertedBitmap = SoftwareBitmap.Convert(
|
||||
softwareBitmap,
|
||||
BitmapPixelFormat.Bgra8,
|
||||
BitmapAlphaMode.Premultiplied);
|
||||
|
||||
int width = convertedBitmap.PixelWidth;
|
||||
int height = convertedBitmap.PixelHeight;
|
||||
int stride = width * 4; // 4 bytes per pixel for Bgra8
|
||||
byte[] pixels = new byte[height * stride];
|
||||
|
||||
using (var buffer = convertedBitmap.LockBuffer(BitmapBufferAccessMode.Read))
|
||||
using (var reference = buffer.CreateReference())
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
((IMemoryBufferByteAccess)reference).GetBuffer(out byte* dataInBytes, out uint capacity);
|
||||
System.Runtime.InteropServices.Marshal.Copy((IntPtr)dataInBytes, pixels, 0, pixels.Length);
|
||||
}
|
||||
}
|
||||
|
||||
// Create WPF BitmapSource from pixel data
|
||||
var wpfBitmap = BitmapSource.Create(
|
||||
width,
|
||||
height,
|
||||
96, // DPI X
|
||||
96, // DPI Y
|
||||
PixelFormats.Bgra32,
|
||||
null,
|
||||
pixels,
|
||||
stride);
|
||||
|
||||
wpfBitmap.Freeze(); // Make it thread-safe
|
||||
return wpfBitmap;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
[ComImport]
|
||||
[Guid("5B0D3235-4DBA-4D44-865E-8F1D0E4FD04D")]
|
||||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
private interface IMemoryBufferByteAccess
|
||||
{
|
||||
unsafe void GetBuffer(out byte* buffer, out uint capacity);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_usageLock)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// ImageScaler implements IDisposable
|
||||
(_imageScaler as IDisposable)?.Dispose();
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,41 +6,22 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
using Common.UI;
|
||||
using ImageResizer.Helpers;
|
||||
using ImageResizer.Models;
|
||||
using ImageResizer.Properties;
|
||||
using ImageResizer.Services;
|
||||
using ImageResizer.Views;
|
||||
|
||||
namespace ImageResizer.ViewModels
|
||||
{
|
||||
public class InputViewModel : Observable
|
||||
{
|
||||
public const int DefaultAiScale = 2;
|
||||
private const int MinAiScale = 1;
|
||||
private const int MaxAiScale = 8;
|
||||
|
||||
private readonly ResizeBatch _batch;
|
||||
private readonly MainViewModel _mainViewModel;
|
||||
private readonly IMainView _mainView;
|
||||
private readonly bool _hasMultipleFiles;
|
||||
private bool _originalDimensionsLoaded;
|
||||
private int? _originalWidth;
|
||||
private int? _originalHeight;
|
||||
private string _currentResolutionDescription;
|
||||
private string _newResolutionDescription;
|
||||
private bool _isDownloadingModel;
|
||||
private string _modelStatusMessage;
|
||||
private double _modelDownloadProgress;
|
||||
|
||||
public enum Dimension
|
||||
{
|
||||
@@ -64,116 +45,24 @@ namespace ImageResizer.ViewModels
|
||||
_batch = batch;
|
||||
_mainViewModel = mainViewModel;
|
||||
_mainView = mainView;
|
||||
_hasMultipleFiles = _batch?.Files.Count > 1;
|
||||
|
||||
Settings = settings;
|
||||
if (settings != null)
|
||||
{
|
||||
settings.CustomSize.PropertyChanged += (sender, e) => settings.SelectedSize = (CustomSize)sender;
|
||||
settings.AiSize.PropertyChanged += (sender, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(AiSize.Scale))
|
||||
{
|
||||
NotifyAiScaleChanged();
|
||||
}
|
||||
};
|
||||
settings.PropertyChanged += HandleSettingsPropertyChanged;
|
||||
}
|
||||
|
||||
ResizeCommand = new RelayCommand(Resize, () => CanResize);
|
||||
ResizeCommand = new RelayCommand(Resize);
|
||||
CancelCommand = new RelayCommand(Cancel);
|
||||
OpenSettingsCommand = new RelayCommand(OpenSettings);
|
||||
EnterKeyPressedCommand = new RelayCommand<KeyPressParams>(HandleEnterKeyPress);
|
||||
DownloadModelCommand = new RelayCommand(async () => await DownloadModelAsync());
|
||||
|
||||
// Initialize AI UI state based on Settings availability
|
||||
InitializeAiState();
|
||||
}
|
||||
|
||||
public Settings Settings { get; }
|
||||
|
||||
public IEnumerable<ResizeFit> ResizeFitValues => Enum.GetValues<ResizeFit>();
|
||||
public IEnumerable<ResizeFit> ResizeFitValues => Enum.GetValues(typeof(ResizeFit)).Cast<ResizeFit>();
|
||||
|
||||
public IEnumerable<ResizeUnit> ResizeUnitValues => Enum.GetValues<ResizeUnit>();
|
||||
|
||||
public int AiSuperResolutionScale
|
||||
{
|
||||
get => Settings?.AiSize?.Scale ?? DefaultAiScale;
|
||||
set
|
||||
{
|
||||
if (Settings?.AiSize != null && Settings.AiSize.Scale != value)
|
||||
{
|
||||
Settings.AiSize.Scale = value;
|
||||
NotifyAiScaleChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string AiScaleDisplay => Settings?.AiSize?.ScaleDisplay ?? string.Empty;
|
||||
|
||||
public string AiScaleDescription => FormatLabeledSize(Resources.Input_AiScaleLabel, AiScaleDisplay);
|
||||
|
||||
public string CurrentResolutionDescription
|
||||
{
|
||||
get => _currentResolutionDescription;
|
||||
private set => Set(ref _currentResolutionDescription, value);
|
||||
}
|
||||
|
||||
public string NewResolutionDescription
|
||||
{
|
||||
get => _newResolutionDescription;
|
||||
private set => Set(ref _newResolutionDescription, value);
|
||||
}
|
||||
|
||||
// ==================== UI State Properties ====================
|
||||
|
||||
// Show AI size descriptions only when AI size is selected and not multiple files
|
||||
public bool ShowAiSizeDescriptions => Settings?.SelectedSize is AiSize && !_hasMultipleFiles;
|
||||
|
||||
// Helper property: Is model currently being downloaded?
|
||||
public bool IsModelDownloading => _isDownloadingModel;
|
||||
|
||||
public string ModelStatusMessage
|
||||
{
|
||||
get => _modelStatusMessage;
|
||||
private set => Set(ref _modelStatusMessage, value);
|
||||
}
|
||||
|
||||
public double ModelDownloadProgress
|
||||
{
|
||||
get => _modelDownloadProgress;
|
||||
private set => Set(ref _modelDownloadProgress, value);
|
||||
}
|
||||
|
||||
// Show download prompt when: AI size is selected and model is not ready (including downloading)
|
||||
public bool ShowModelDownloadPrompt =>
|
||||
Settings?.SelectedSize is AiSize &&
|
||||
(App.AiAvailabilityState == Properties.AiAvailabilityState.ModelNotReady || _isDownloadingModel);
|
||||
|
||||
// Show AI controls when: AI size is selected and AI is ready
|
||||
public bool ShowAiControls =>
|
||||
Settings?.SelectedSize is AiSize &&
|
||||
App.AiAvailabilityState == Properties.AiAvailabilityState.Ready;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the resize operation can proceed.
|
||||
/// For AI resize: only enabled when AI is fully ready.
|
||||
/// For non-AI resize: always enabled.
|
||||
/// </summary>
|
||||
public bool CanResize
|
||||
{
|
||||
get
|
||||
{
|
||||
// If AI size is selected, only allow resize when AI is fully ready
|
||||
if (Settings?.SelectedSize is AiSize)
|
||||
{
|
||||
return App.AiAvailabilityState == Properties.AiAvailabilityState.Ready;
|
||||
}
|
||||
|
||||
// Non-AI resize can always proceed
|
||||
return true;
|
||||
}
|
||||
}
|
||||
public IEnumerable<ResizeUnit> ResizeUnitValues => Enum.GetValues(typeof(ResizeUnit)).Cast<ResizeUnit>();
|
||||
|
||||
public ICommand ResizeCommand { get; }
|
||||
|
||||
@@ -183,11 +72,9 @@ namespace ImageResizer.ViewModels
|
||||
|
||||
public ICommand EnterKeyPressedCommand { get; private set; }
|
||||
|
||||
public ICommand DownloadModelCommand { get; private set; }
|
||||
|
||||
// Any of the files is a gif
|
||||
public bool TryingToResizeGifFiles =>
|
||||
_batch?.Files.Any(filename => filename.EndsWith(".gif", System.StringComparison.InvariantCultureIgnoreCase)) == true;
|
||||
_batch.Files.Any(filename => filename.EndsWith(".gif", System.StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
public void Resize()
|
||||
{
|
||||
@@ -215,242 +102,5 @@ namespace ImageResizer.ViewModels
|
||||
|
||||
public void Cancel()
|
||||
=> _mainView.Close();
|
||||
|
||||
private void HandleSettingsPropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
switch (e.PropertyName)
|
||||
{
|
||||
case nameof(Settings.SelectedSizeIndex):
|
||||
case nameof(Settings.SelectedSize):
|
||||
// Notify UI state properties that depend on SelectedSize
|
||||
NotifyAiStateChanged();
|
||||
UpdateAiDetails();
|
||||
|
||||
// Trigger CanExecuteChanged for ResizeCommand
|
||||
if (ResizeCommand is RelayCommand cmd)
|
||||
{
|
||||
cmd.OnCanExecuteChanged();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureAiScaleWithinRange()
|
||||
{
|
||||
if (Settings?.AiSize != null)
|
||||
{
|
||||
Settings.AiSize.Scale = Math.Clamp(
|
||||
Settings.AiSize.Scale,
|
||||
MinAiScale,
|
||||
MaxAiScale);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateAiDetails()
|
||||
{
|
||||
// Clear AI details if AI size not selected
|
||||
if (Settings == null || Settings.SelectedSize is not AiSize)
|
||||
{
|
||||
CurrentResolutionDescription = string.Empty;
|
||||
NewResolutionDescription = string.Empty;
|
||||
return;
|
||||
}
|
||||
|
||||
EnsureAiScaleWithinRange();
|
||||
|
||||
if (_hasMultipleFiles)
|
||||
{
|
||||
CurrentResolutionDescription = string.Empty;
|
||||
NewResolutionDescription = string.Empty;
|
||||
return;
|
||||
}
|
||||
|
||||
EnsureOriginalDimensionsLoaded();
|
||||
|
||||
var hasConcreteSize = _originalWidth.HasValue && _originalHeight.HasValue;
|
||||
var currentValue = hasConcreteSize
|
||||
? FormatDimensions(_originalWidth!.Value, _originalHeight!.Value)
|
||||
: Resources.Input_AiUnknownSize;
|
||||
CurrentResolutionDescription = FormatLabeledSize(Resources.Input_AiCurrentLabel, currentValue);
|
||||
|
||||
var scale = Settings.AiSize.Scale;
|
||||
var newValue = hasConcreteSize
|
||||
? FormatDimensions((long)_originalWidth!.Value * scale, (long)_originalHeight!.Value * scale)
|
||||
: Resources.Input_AiUnknownSize;
|
||||
NewResolutionDescription = FormatLabeledSize(Resources.Input_AiNewLabel, newValue);
|
||||
}
|
||||
|
||||
private static string FormatDimensions(long width, long height)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, "{0} × {1}", width, height);
|
||||
}
|
||||
|
||||
private static string FormatLabeledSize(string label, string value)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, "{0}: {1}", label, value);
|
||||
}
|
||||
|
||||
private void EnsureOriginalDimensionsLoaded()
|
||||
{
|
||||
if (_originalDimensionsLoaded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var file = _batch?.Files.FirstOrDefault();
|
||||
if (string.IsNullOrEmpty(file))
|
||||
{
|
||||
_originalDimensionsLoaded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var stream = File.OpenRead(file);
|
||||
var decoder = BitmapDecoder.Create(stream, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.None);
|
||||
var frame = decoder.Frames.FirstOrDefault();
|
||||
if (frame != null)
|
||||
{
|
||||
_originalWidth = frame.PixelWidth;
|
||||
_originalHeight = frame.PixelHeight;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Failed to load image dimensions - clear values
|
||||
_originalWidth = null;
|
||||
_originalHeight = null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_originalDimensionsLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes AI UI state based on App's cached availability state.
|
||||
/// Subscribe to state change event to update UI when background initialization completes.
|
||||
/// </summary>
|
||||
private void InitializeAiState()
|
||||
{
|
||||
// Subscribe to initialization completion event to refresh UI
|
||||
App.AiInitializationCompleted += OnAiInitializationCompleted;
|
||||
|
||||
// Set initial status message based on current state
|
||||
UpdateStatusMessage();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles AI initialization completion event from App.
|
||||
/// Refreshes UI when background initialization finishes.
|
||||
/// </summary>
|
||||
private void OnAiInitializationCompleted(object sender, Properties.AiAvailabilityState finalState)
|
||||
{
|
||||
UpdateStatusMessage();
|
||||
NotifyAiStateChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates status message based on current App availability state.
|
||||
/// </summary>
|
||||
private void UpdateStatusMessage()
|
||||
{
|
||||
ModelStatusMessage = App.AiAvailabilityState switch
|
||||
{
|
||||
Properties.AiAvailabilityState.Ready => string.Empty,
|
||||
Properties.AiAvailabilityState.ModelNotReady => Resources.Input_AiModelNotAvailable,
|
||||
Properties.AiAvailabilityState.NotSupported => Resources.Input_AiModelNotSupported,
|
||||
_ => string.Empty,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Notifies UI when AI state changes (model availability, download status).
|
||||
/// </summary>
|
||||
private void NotifyAiStateChanged()
|
||||
{
|
||||
OnPropertyChanged(nameof(IsModelDownloading));
|
||||
OnPropertyChanged(nameof(ShowModelDownloadPrompt));
|
||||
OnPropertyChanged(nameof(ShowAiControls));
|
||||
OnPropertyChanged(nameof(ShowAiSizeDescriptions));
|
||||
OnPropertyChanged(nameof(CanResize));
|
||||
|
||||
// Trigger CanExecuteChanged for ResizeCommand
|
||||
if (ResizeCommand is RelayCommand resizeCommand)
|
||||
{
|
||||
resizeCommand.OnCanExecuteChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Notifies UI when AI scale changes (slider value).
|
||||
/// </summary>
|
||||
private void NotifyAiScaleChanged()
|
||||
{
|
||||
OnPropertyChanged(nameof(AiSuperResolutionScale));
|
||||
OnPropertyChanged(nameof(AiScaleDisplay));
|
||||
OnPropertyChanged(nameof(AiScaleDescription));
|
||||
UpdateAiDetails();
|
||||
}
|
||||
|
||||
private async Task DownloadModelAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Set downloading flag and show progress
|
||||
_isDownloadingModel = true;
|
||||
ModelStatusMessage = Resources.Input_AiModelDownloading;
|
||||
ModelDownloadProgress = 0;
|
||||
NotifyAiStateChanged();
|
||||
|
||||
// Create progress reporter to update UI
|
||||
var progress = new Progress<double>(value =>
|
||||
{
|
||||
// progressValue could be 0-1 or 0-100, normalize to 0-100
|
||||
ModelDownloadProgress = value > 1 ? value : value * 100;
|
||||
});
|
||||
|
||||
// Call EnsureReadyAsync to download and prepare the AI model
|
||||
var result = await WinAiSuperResolutionService.EnsureModelReadyAsync(progress);
|
||||
|
||||
if (result?.Status == Microsoft.Windows.AI.AIFeatureReadyResultState.Success)
|
||||
{
|
||||
// Model successfully downloaded and ready
|
||||
ModelDownloadProgress = 100;
|
||||
|
||||
// Update App's cached state
|
||||
App.AiAvailabilityState = Properties.AiAvailabilityState.Ready;
|
||||
UpdateStatusMessage();
|
||||
|
||||
// Initialize the AI service now that model is ready
|
||||
var aiService = await WinAiSuperResolutionService.CreateAsync();
|
||||
ResizeBatch.SetAiSuperResolutionService(aiService ?? (Services.IAISuperResolutionService)NoOpAiSuperResolutionService.Instance);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Download failed
|
||||
ModelStatusMessage = Resources.Input_AiModelDownloadFailed;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Exception during download
|
||||
ModelStatusMessage = Resources.Input_AiModelDownloadFailed;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Clear downloading flag
|
||||
_isDownloadingModel = false;
|
||||
|
||||
// Reset progress if not successful
|
||||
if (App.AiAvailabilityState != Properties.AiAvailabilityState.Ready)
|
||||
{
|
||||
ModelDownloadProgress = 0;
|
||||
}
|
||||
|
||||
NotifyAiStateChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,21 +11,11 @@ using System.Windows.Data;
|
||||
|
||||
namespace ImageResizer.Views
|
||||
{
|
||||
[ValueConversion(typeof(bool), typeof(Visibility))]
|
||||
[ValueConversion(typeof(Enum), typeof(string))]
|
||||
internal class BoolValueConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
bool boolValue = (bool)value;
|
||||
bool invert = parameter is string param && param.Equals("Inverted", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (invert)
|
||||
{
|
||||
boolValue = !boolValue;
|
||||
}
|
||||
|
||||
return boolValue ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
=> (bool)value ? Visibility.Visible : Visibility.Collapsed;
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
=> (Visibility)value == Visibility.Visible;
|
||||
|
||||
@@ -7,23 +7,6 @@
|
||||
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
|
||||
xmlns:v="clr-namespace:ImageResizer.Views">
|
||||
|
||||
<UserControl.Resources>
|
||||
<Style
|
||||
x:Key="ReadableDisabledButtonStyle"
|
||||
BasedOn="{StaticResource {x:Type ui:Button}}"
|
||||
TargetType="ui:Button">
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<!-- Improved disabled state: keep readable but clearly disabled -->
|
||||
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" />
|
||||
<Setter Property="Background" Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource {x:Static SystemColors.ControlDarkBrushKey}}" />
|
||||
<Setter Property="Opacity" Value="0.75" />
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</UserControl.Resources>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
@@ -32,140 +15,60 @@
|
||||
<!-- other controls -->
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<StackPanel Grid.Row="0" Margin="16">
|
||||
<ComboBox
|
||||
Name="SizeComboBox"
|
||||
Height="64"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
VerticalContentAlignment="Stretch"
|
||||
AutomationProperties.HelpText="{Binding Settings.SelectedSize, Converter={StaticResource SizeTypeToHelpTextConverter}}"
|
||||
AutomationProperties.Name="{x:Static p:Resources.Image_Sizes}"
|
||||
ItemsSource="{Binding Settings.AllSizes}"
|
||||
SelectedItem="{Binding Settings.SelectedSize, Mode=TwoWay}">
|
||||
<ComboBox.ItemContainerStyle>
|
||||
<Style BasedOn="{StaticResource {x:Type ComboBoxItem}}" TargetType="ComboBoxItem">
|
||||
<Setter Property="AutomationProperties.Name" Value="{Binding Name}" />
|
||||
<Setter Property="AutomationProperties.HelpText" Value="{Binding ., Converter={StaticResource SizeTypeToHelpTextConverter}}" />
|
||||
</Style>
|
||||
</ComboBox.ItemContainerStyle>
|
||||
<ComboBox.Resources>
|
||||
<DataTemplate DataType="{x:Type m:ResizeSize}">
|
||||
<Grid VerticalAlignment="Center">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Style="{StaticResource BodyStrongTextBlockStyle}" Text="{Binding Name}" />
|
||||
<StackPanel Grid.Row="1" Orientation="Horizontal">
|
||||
<TextBlock Text="{Binding Fit, Converter={StaticResource EnumValueConverter}, ConverterParameter=ThirdPersonSingular}" />
|
||||
<TextBlock
|
||||
Margin="4,0,0,0"
|
||||
Style="{StaticResource BodyStrongTextBlockStyle}"
|
||||
Text="{Binding Width, Converter={StaticResource AutoDoubleConverter}, ConverterParameter=Auto}" />
|
||||
<TextBlock
|
||||
Margin="4,0,0,0"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
|
||||
Text="×"
|
||||
Visibility="{Binding ShowHeight, Converter={StaticResource BoolValueConverter}}" />
|
||||
<TextBlock
|
||||
Margin="4,0,0,0"
|
||||
Style="{StaticResource BodyStrongTextBlockStyle}"
|
||||
Text="{Binding Height, Converter={StaticResource AutoDoubleConverter}, ConverterParameter=Auto}"
|
||||
Visibility="{Binding ShowHeight, Converter={StaticResource BoolValueConverter}}" />
|
||||
<TextBlock Margin="4,0,0,0" Text="{Binding Unit, Converter={StaticResource EnumValueConverter}, ConverterParameter=ToLower}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
<ComboBox
|
||||
Name="SizeComboBox"
|
||||
Grid.Row="0"
|
||||
Height="64"
|
||||
Margin="16"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
VerticalContentAlignment="Stretch"
|
||||
AutomationProperties.HelpText="{Binding Settings.SelectedSize, Converter={StaticResource SizeTypeToHelpTextConverter}}"
|
||||
AutomationProperties.Name="{x:Static p:Resources.Image_Sizes}"
|
||||
ItemsSource="{Binding Settings.AllSizes}"
|
||||
SelectedIndex="{Binding Settings.SelectedSizeIndex}">
|
||||
<ComboBox.ItemContainerStyle>
|
||||
<Style BasedOn="{StaticResource {x:Type ComboBoxItem}}" TargetType="ComboBoxItem">
|
||||
<Setter Property="AutomationProperties.Name" Value="{Binding Name}" />
|
||||
<Setter Property="AutomationProperties.HelpText" Value="{Binding ., Converter={StaticResource SizeTypeToHelpTextConverter}}" />
|
||||
</Style>
|
||||
</ComboBox.ItemContainerStyle>
|
||||
<ComboBox.Resources>
|
||||
<DataTemplate DataType="{x:Type m:ResizeSize}">
|
||||
<Grid VerticalAlignment="Center">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Style="{StaticResource BodyStrongTextBlockStyle}" Text="{Binding Name}" />
|
||||
<StackPanel Grid.Row="1" Orientation="Horizontal">
|
||||
<TextBlock Text="{Binding Fit, Converter={StaticResource EnumValueConverter}, ConverterParameter=ThirdPersonSingular}" />
|
||||
<TextBlock
|
||||
Margin="4,0,0,0"
|
||||
Style="{StaticResource BodyStrongTextBlockStyle}"
|
||||
Text="{Binding Width, Converter={StaticResource AutoDoubleConverter}, ConverterParameter=Auto}" />
|
||||
<TextBlock
|
||||
Margin="4,0,0,0"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
|
||||
Text="×"
|
||||
Visibility="{Binding ShowHeight, Converter={StaticResource BoolValueConverter}}" />
|
||||
<TextBlock
|
||||
Margin="4,0,0,0"
|
||||
Style="{StaticResource BodyStrongTextBlockStyle}"
|
||||
Text="{Binding Height, Converter={StaticResource AutoDoubleConverter}, ConverterParameter=Auto}"
|
||||
Visibility="{Binding ShowHeight, Converter={StaticResource BoolValueConverter}}" />
|
||||
<TextBlock Margin="4,0,0,0" Text="{Binding Unit, Converter={StaticResource EnumValueConverter}, ConverterParameter=ToLower}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate DataType="{x:Type m:CustomSize}">
|
||||
<Grid VerticalAlignment="Center">
|
||||
<TextBlock FontWeight="SemiBold" Text="{Binding Name}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate DataType="{x:Type m:AiSize}">
|
||||
<Grid VerticalAlignment="Center">
|
||||
<TextBlock FontWeight="SemiBold" Text="{Binding Name}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ComboBox.Resources>
|
||||
</ComboBox>
|
||||
|
||||
<!-- AI Configuration Panel -->
|
||||
<Grid Margin="0,8,0,0">
|
||||
<!-- AI Model Download Prompt -->
|
||||
<StackPanel>
|
||||
<StackPanel.Style>
|
||||
<Style TargetType="StackPanel">
|
||||
<Setter Property="Visibility" Value="Collapsed" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding ShowModelDownloadPrompt}" Value="True">
|
||||
<Setter Property="Visibility" Value="Visible" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</StackPanel.Style>
|
||||
|
||||
<ui:InfoBar
|
||||
IsClosable="False"
|
||||
IsOpen="True"
|
||||
Message="{Binding ModelStatusMessage}"
|
||||
Severity="Informational" />
|
||||
|
||||
<ui:Button
|
||||
Margin="0,8,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Appearance="Primary"
|
||||
Command="{Binding DownloadModelCommand}"
|
||||
Content="{x:Static p:Resources.Input_AiModelDownloadButton}"
|
||||
Visibility="{Binding IsModelDownloading, Converter={StaticResource BoolValueConverter}, ConverterParameter=Inverted}" />
|
||||
|
||||
<StackPanel Margin="0,8,0,0" Visibility="{Binding IsModelDownloading, Converter={StaticResource BoolValueConverter}}">
|
||||
<ui:ProgressRing IsIndeterminate="True" />
|
||||
<TextBlock
|
||||
Margin="0,8,0,0"
|
||||
HorizontalAlignment="Center"
|
||||
Text="{Binding ModelStatusMessage}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- AI Scale Controls -->
|
||||
<StackPanel>
|
||||
<StackPanel.Style>
|
||||
<Style TargetType="StackPanel">
|
||||
<Setter Property="Visibility" Value="Collapsed" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding ShowAiControls}" Value="True">
|
||||
<Setter Property="Visibility" Value="Visible" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</StackPanel.Style>
|
||||
|
||||
<TextBlock Style="{StaticResource BodyStrongTextBlockStyle}" Text="{Binding AiScaleDescription}" />
|
||||
<Slider
|
||||
Margin="0,8,0,0"
|
||||
AutomationProperties.Name="{x:Static p:Resources.Input_AiScaleLabel}"
|
||||
IsSelectionRangeEnabled="False"
|
||||
IsSnapToTickEnabled="True"
|
||||
Maximum="8"
|
||||
Minimum="1"
|
||||
TickFrequency="1"
|
||||
TickPlacement="BottomRight"
|
||||
Ticks="1,2,3,4,5,6,7,8"
|
||||
Value="{Binding AiSuperResolutionScale, Mode=TwoWay}" />
|
||||
|
||||
<StackPanel Margin="0,8,0,0" Visibility="{Binding ShowAiSizeDescriptions, Converter={StaticResource BoolValueConverter}}">
|
||||
<TextBlock Style="{StaticResource BodyTextBlockStyle}" Text="{Binding CurrentResolutionDescription}" />
|
||||
<TextBlock
|
||||
Margin="0,4,0,0"
|
||||
Style="{StaticResource BodyTextBlockStyle}"
|
||||
Text="{Binding NewResolutionDescription}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
<DataTemplate DataType="{x:Type m:CustomSize}">
|
||||
<Grid VerticalAlignment="Center">
|
||||
<TextBlock FontWeight="SemiBold" Text="{Binding Name}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ComboBox.Resources>
|
||||
</ComboBox>
|
||||
|
||||
<Grid Grid.Row="1">
|
||||
<Grid.RowDefinitions>
|
||||
@@ -377,8 +280,7 @@
|
||||
Appearance="Primary"
|
||||
AutomationProperties.Name="{x:Static p:Resources.Resize_Tooltip}"
|
||||
Command="{Binding ResizeCommand}"
|
||||
IsDefault="True"
|
||||
Style="{StaticResource ReadableDisabledButtonStyle}">
|
||||
IsDefault="True">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<ui:SymbolIcon FontSize="16" Symbol="ResizeLarge16" />
|
||||
<TextBlock Margin="8,0,0,0" Text="{x:Static p:Resources.Input_Resize}" />
|
||||
|
||||
68
src/modules/interface/powertoy_cli.h
Normal file
68
src/modules/interface/powertoy_cli.h
Normal file
@@ -0,0 +1,68 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <utility>
|
||||
#include <common/utils/json.h>
|
||||
|
||||
namespace pt::cli
|
||||
{
|
||||
struct CommandParameter
|
||||
{
|
||||
std::wstring name;
|
||||
bool required = false;
|
||||
std::wstring description;
|
||||
};
|
||||
|
||||
struct CommandDescriptor
|
||||
{
|
||||
std::wstring action;
|
||||
std::wstring description;
|
||||
std::vector<CommandParameter> parameters;
|
||||
bool requiresElevation = false;
|
||||
bool longRunning = false;
|
||||
};
|
||||
|
||||
struct CommandInvocation
|
||||
{
|
||||
std::wstring action;
|
||||
json::JsonObject args;
|
||||
};
|
||||
|
||||
struct CommandResult
|
||||
{
|
||||
bool ok = false;
|
||||
json::JsonObject data;
|
||||
std::wstring errorCode;
|
||||
std::wstring errorMessage;
|
||||
|
||||
static CommandResult Success(json::JsonObject data = {});
|
||||
static CommandResult Error(std::wstring code, std::wstring message);
|
||||
};
|
||||
|
||||
inline CommandResult CommandResult::Success(json::JsonObject data)
|
||||
{
|
||||
CommandResult result;
|
||||
result.ok = true;
|
||||
result.data = std::move(data);
|
||||
return result;
|
||||
}
|
||||
|
||||
inline CommandResult CommandResult::Error(std::wstring code, std::wstring message)
|
||||
{
|
||||
CommandResult result;
|
||||
result.ok = false;
|
||||
result.errorCode = std::move(code);
|
||||
result.errorMessage = std::move(message);
|
||||
return result;
|
||||
}
|
||||
|
||||
class IModuleCommandProvider
|
||||
{
|
||||
public:
|
||||
virtual ~IModuleCommandProvider() = default;
|
||||
virtual std::wstring ModuleKey() const = 0;
|
||||
virtual std::vector<CommandDescriptor> DescribeCommands() const = 0;
|
||||
virtual CommandResult Execute(const CommandInvocation& invocation) = 0;
|
||||
};
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <compare>
|
||||
#include <common/utils/gpo.h>
|
||||
#include <compare>
|
||||
#include <common/utils/gpo.h>
|
||||
#include "powertoy_cli.h"
|
||||
|
||||
/*
|
||||
DLL Interface for PowerToys. The powertoy_create() (see below) must return
|
||||
@@ -140,20 +141,25 @@ public:
|
||||
* milliseconds_win_key_must_be_pressed returns the number of milliseconds the win key should be pressed before triggering the module.
|
||||
* Don't use these for new modules.
|
||||
*/
|
||||
virtual bool keep_track_of_pressed_win_key() { return false; }
|
||||
virtual UINT milliseconds_win_key_must_be_pressed() { return 0; }
|
||||
|
||||
virtual void send_settings_telemetry()
|
||||
{
|
||||
}
|
||||
|
||||
virtual bool is_enabled_by_default() const { return true; }
|
||||
|
||||
/* Provides the GPO configuration value for the module. This should be overridden by the module interface to get the proper gpo policy setting. */
|
||||
virtual powertoys_gpo::gpo_rule_configured_t gpo_policy_enabled_configuration()
|
||||
{
|
||||
return powertoys_gpo::gpo_rule_configured_not_configured;
|
||||
}
|
||||
virtual bool keep_track_of_pressed_win_key() { return false; }
|
||||
virtual UINT milliseconds_win_key_must_be_pressed() { return 0; }
|
||||
|
||||
virtual void send_settings_telemetry()
|
||||
{
|
||||
}
|
||||
|
||||
virtual bool is_enabled_by_default() const { return true; }
|
||||
|
||||
/* Provides the GPO configuration value for the module. This should be overridden by the module interface to get the proper gpo policy setting. */
|
||||
virtual powertoys_gpo::gpo_rule_configured_t gpo_policy_enabled_configuration()
|
||||
{
|
||||
return powertoys_gpo::gpo_rule_configured_not_configured;
|
||||
}
|
||||
|
||||
virtual pt::cli::IModuleCommandProvider* command_provider()
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Some actions like AdvancedPaste generate new inputs, which we don't want to catch again.
|
||||
// The flag was purposefully chose to not collide with other keyboard manager flags.
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<AdditionalDependencies>legacy_stdio_definitions.lib;windowscodecs.lib;$(VCToolsInstallDir)lib\$(Platform)\libsancov.lib;$(CoreLibraryDependencies);%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalDependencies>legacy_stdio_definitions.lib;$(VCToolsInstallDir)lib\$(Platform)\libsancov.lib;$(CoreLibraryDependencies);%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
<PostBuildEvent>
|
||||
<Command>xcopy /y "$(VCToolsInstallDir)bin\Hostx64\x64\clang_rt.asan_dynamic-x86_64.dll" "$(OutDir)"</Command>
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<EnableUAC>false</EnableUAC>
|
||||
<AdditionalDependencies>runtimeobject.lib;windowscodecs.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalDependencies>runtimeobject.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<ModuleDefinitionFile>Source.def</ModuleDefinitionFile>
|
||||
</Link>
|
||||
<PreBuildEvent>
|
||||
@@ -75,7 +75,7 @@ MakeAppx.exe pack /d . /p $(OutDir)PowerRenameContextMenuPackage.msix /nv</Comma
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<EnableUAC>false</EnableUAC>
|
||||
<AdditionalDependencies>runtimeobject.lib;windowscodecs.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalDependencies>runtimeobject.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<ModuleDefinitionFile>Source.def</ModuleDefinitionFile>
|
||||
</Link>
|
||||
<PreBuildEvent>
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<AdditionalDependencies>kernel32.lib;user32.lib;dwmapi.lib;Shcore.lib;windowscodecs.lib;propsys.lib;ole32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalDependencies>kernel32.lib;user32.lib;dwmapi.lib;Shcore.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
|
||||
@@ -89,7 +89,7 @@
|
||||
<Link>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<AdditionalDependencies>kernel32.lib;user32.lib;dwmapi.lib;Shcore.lib;windowscodecs.lib;propsys.lib;ole32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalDependencies>kernel32.lib;user32.lib;dwmapi.lib;Shcore.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "pch.h"
|
||||
#include "pch.h"
|
||||
|
||||
#include "App.xaml.h"
|
||||
#include "MainWindow.xaml.h"
|
||||
@@ -117,9 +117,6 @@ App::App()
|
||||
/// <param name="e">Details about the launch request and process.</param>
|
||||
void App::OnLaunched(LaunchActivatedEventArgs const&)
|
||||
{
|
||||
// WinUI3 framework automatically initializes COM as STA on the main thread
|
||||
// No manual initialization needed for WIC operations
|
||||
|
||||
LoggerHelpers::init_logger(moduleName, L"", LogSettings::powerRenameLoggerName);
|
||||
|
||||
if (powertoys_gpo::getConfiguredPowerRenameEnabledValue() == powertoys_gpo::gpo_rule_configured_disabled)
|
||||
@@ -240,6 +237,7 @@ void App::OnLaunched(LaunchActivatedEventArgs const&)
|
||||
}
|
||||
#else
|
||||
#define BUFSIZE 4096 * 4
|
||||
|
||||
BOOL bSuccess;
|
||||
WCHAR chBuf[BUFSIZE];
|
||||
DWORD dwRead;
|
||||
@@ -271,4 +269,4 @@ void App::OnLaunched(LaunchActivatedEventArgs const&)
|
||||
|
||||
window = make<MainWindow>();
|
||||
window.Activate();
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,6 @@ namespace PowerRenameUI
|
||||
Windows.Foundation.Collections.IObservableVector<PatternSnippet> DateTimeShortcuts { get; };
|
||||
Windows.Foundation.Collections.IObservableVector<PatternSnippet> CounterShortcuts { get; };
|
||||
Windows.Foundation.Collections.IObservableVector<PatternSnippet> RandomizerShortcuts { get; };
|
||||
Windows.Foundation.Collections.IObservableVector<PatternSnippet> MetadataShortcuts { get; };
|
||||
|
||||
String OriginalCount;
|
||||
String RenamedCount;
|
||||
|
||||
@@ -330,8 +330,6 @@
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="28" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="28" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock x:Uid="DateTimeCheatSheet_Title" FontWeight="SemiBold" />
|
||||
<ListView
|
||||
@@ -453,48 +451,6 @@
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
<!-- Media Metadata -->
|
||||
<TextBlock
|
||||
x:Uid="MetadataCheatSheet_Title"
|
||||
Grid.Row="6"
|
||||
Margin="0,10,0,0"
|
||||
FontWeight="SemiBold" />
|
||||
<ListView
|
||||
Grid.Row="7"
|
||||
Margin="-4,12,0,0"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="MetadataItemClick"
|
||||
ItemsSource="{x:Bind MetadataShortcuts}"
|
||||
SelectionMode="None">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate x:DataType="local:PatternSnippet">
|
||||
<Grid Margin="-10,0,0,0" ColumnSpacing="8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Border
|
||||
Padding="8"
|
||||
HorizontalAlignment="Left"
|
||||
Background="{ThemeResource ButtonBackground}"
|
||||
BorderBrush="{ThemeResource ButtonBorderBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="4">
|
||||
<TextBlock
|
||||
FontFamily="Consolas"
|
||||
Foreground="{ThemeResource ButtonForeground}"
|
||||
Text="{x:Bind Code}" />
|
||||
</Border>
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="12"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="{x:Bind Description}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
</Grid>
|
||||
</Flyout>
|
||||
</Button.Flyout>
|
||||
@@ -604,61 +560,31 @@
|
||||
FontFamily="{ThemeResource SymbolThemeFontFamily}" />
|
||||
</StackPanel>
|
||||
|
||||
<Grid Margin="0,16,0,0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="12" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- File Time Section -->
|
||||
<TextBlock
|
||||
x:Name="FileTimeLabel"
|
||||
x:Uid="TextBlock_FileTime"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
Margin="0,0,0,8"
|
||||
FontSize="12"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
|
||||
<TextBlock
|
||||
x:Name="FileTimeLabel"
|
||||
x:Uid="TextBlock_FileTime"
|
||||
Margin="0,16,0,8"
|
||||
FontSize="12"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
|
||||
|
||||
<StackPanel
|
||||
Grid.Row="2"
|
||||
Grid.Column="1"
|
||||
Orientation="Horizontal"
|
||||
Spacing="4">
|
||||
<ComboBox
|
||||
x:Name="comboBox_fileTimeParts"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
Width="200"
|
||||
HorizontalAlignment="Stretch"
|
||||
AutomationProperties.LabeledBy="{Binding ElementName=FileTimeLabel}"
|
||||
AutomationProperties.LabeledBy="{Binding ElementName=FileDateLabel}"
|
||||
SelectedIndex="0">
|
||||
<ComboBoxItem x:Uid="FileTimeParts_CreationTime" />
|
||||
<ComboBoxItem x:Uid="FileTimeParts_ModificationTime" />
|
||||
<ComboBoxItem x:Uid="FileTimeParts_AccessTime" />
|
||||
</ComboBox>
|
||||
|
||||
<!-- Metadata Source Section -->
|
||||
<TextBlock
|
||||
x:Name="MetadataSourceLabel"
|
||||
x:Uid="TextBlock_MetadataSource"
|
||||
Grid.Row="0"
|
||||
Grid.Column="2"
|
||||
Margin="0,0,0,8"
|
||||
FontSize="12"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
|
||||
|
||||
<ComboBox
|
||||
x:Name="comboBox_metadataSource"
|
||||
Grid.Row="1"
|
||||
Grid.Column="2"
|
||||
HorizontalAlignment="Stretch"
|
||||
AutomationProperties.LabeledBy="{Binding ElementName=MetadataSourceLabel}"
|
||||
SelectedIndex="0"
|
||||
SelectionChanged="MetadataSourceComboBox_SelectionChanged">
|
||||
<ComboBoxItem x:Uid="MetadataSource_EXIF" />
|
||||
<ComboBoxItem x:Uid="MetadataSource_XMP" />
|
||||
</ComboBox>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "pch.h"
|
||||
#include "pch.h"
|
||||
#include "MainWindow.xaml.h"
|
||||
#if __has_include("MainWindow.g.cpp")
|
||||
#include "MainWindow.g.cpp"
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
#include <settings.h>
|
||||
#include <trace.h>
|
||||
#include <Helpers.h>
|
||||
|
||||
#include <common/logger/call_tracer.h>
|
||||
#include <common/logger/logger.h>
|
||||
@@ -226,11 +225,6 @@ namespace winrt::PowerRenameUI::implementation
|
||||
m_RandomizerShortcuts.Append(winrt::make<PatternSnippet>(L"${rstringdigit=36}", manager.MainResourceMap().GetValue(L"Resources/RandomizerCheatSheet_Digit").ValueAsString()));
|
||||
m_RandomizerShortcuts.Append(winrt::make<PatternSnippet>(L"${ruuidv4}", manager.MainResourceMap().GetValue(L"Resources/RandomizerCheatSheet_Uuid").ValueAsString()));
|
||||
|
||||
// Initialize metadata shortcuts - will be populated based on selected metadata type
|
||||
m_metadataShortcuts = winrt::single_threaded_observable_vector<PowerRenameUI::PatternSnippet>();
|
||||
// Initialize with EXIF patterns (default)
|
||||
UpdateMetadataShortcuts(PowerRenameLib::MetadataType::EXIF);
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
m_etwTrace.UpdateState(true);
|
||||
@@ -362,10 +356,7 @@ namespace winrt::PowerRenameUI::implementation
|
||||
hstring MainWindow::OriginalCount()
|
||||
{
|
||||
UINT count = 0;
|
||||
if (m_prManager)
|
||||
{
|
||||
m_prManager->GetItemCount(&count);
|
||||
}
|
||||
m_prManager->GetItemCount(&count);
|
||||
return hstring{ std::to_wstring(count) };
|
||||
}
|
||||
|
||||
@@ -403,16 +394,13 @@ namespace winrt::PowerRenameUI::implementation
|
||||
button_showAll().IsChecked(true);
|
||||
button_showRenamed().IsChecked(false);
|
||||
|
||||
if (m_prManager)
|
||||
DWORD filter = 0;
|
||||
m_prManager->GetFilter(&filter);
|
||||
if (filter != PowerRenameFilters::None)
|
||||
{
|
||||
DWORD filter = 0;
|
||||
m_prManager->GetFilter(&filter);
|
||||
if (filter != PowerRenameFilters::None)
|
||||
{
|
||||
m_prManager->SwitchFilter(0);
|
||||
get_self<ExplorerItemsSource>(m_explorerItems)->SetIsFiltered(false);
|
||||
InvalidateItemListViewState();
|
||||
}
|
||||
m_prManager->SwitchFilter(0);
|
||||
get_self<ExplorerItemsSource>(m_explorerItems)->SetIsFiltered(false);
|
||||
InvalidateItemListViewState();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -421,17 +409,14 @@ namespace winrt::PowerRenameUI::implementation
|
||||
button_showRenamed().IsChecked(true);
|
||||
button_showAll().IsChecked(false);
|
||||
|
||||
if (m_prManager)
|
||||
DWORD filter = 0;
|
||||
m_prManager->GetFilter(&filter);
|
||||
if (filter != PowerRenameFilters::ShouldRename)
|
||||
{
|
||||
DWORD filter = 0;
|
||||
m_prManager->GetFilter(&filter);
|
||||
if (filter != PowerRenameFilters::ShouldRename)
|
||||
{
|
||||
m_prManager->SwitchFilter(0);
|
||||
UpdateCounts();
|
||||
get_self<ExplorerItemsSource>(m_explorerItems)->SetIsFiltered(true);
|
||||
InvalidateItemListViewState();
|
||||
}
|
||||
m_prManager->SwitchFilter(0);
|
||||
UpdateCounts();
|
||||
get_self<ExplorerItemsSource>(m_explorerItems)->SetIsFiltered(true);
|
||||
InvalidateItemListViewState();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -449,27 +434,6 @@ namespace winrt::PowerRenameUI::implementation
|
||||
textBox_replace().Text(textBox_replace().Text() + s->Code());
|
||||
}
|
||||
|
||||
void MainWindow::MetadataItemClick(winrt::Windows::Foundation::IInspectable const&, winrt::Microsoft::UI::Xaml::Controls::ItemClickEventArgs const& e)
|
||||
{
|
||||
auto s = e.ClickedItem().try_as<PatternSnippet>();
|
||||
DateTimeFlyout().Hide();
|
||||
textBox_replace().Text(textBox_replace().Text() + s->Code());
|
||||
}
|
||||
|
||||
void MainWindow::MetadataSourceComboBox_SelectionChanged(winrt::Windows::Foundation::IInspectable const&, winrt::Microsoft::UI::Xaml::Controls::SelectionChangedEventArgs const&)
|
||||
{
|
||||
int selectedIndex = comboBox_metadataSource().SelectedIndex();
|
||||
|
||||
// Get the selected metadata type based on ComboBox selection
|
||||
PowerRenameLib::MetadataType metadataType = static_cast<PowerRenameLib::MetadataType>(selectedIndex);
|
||||
|
||||
// Update the metadata shortcuts list
|
||||
UpdateMetadataShortcuts(metadataType);
|
||||
|
||||
// Update the metadata source flags
|
||||
UpdateMetadataSourceFlags(selectedIndex);
|
||||
}
|
||||
|
||||
void MainWindow::button_rename_Click(winrt::Microsoft::UI::Xaml::Controls::SplitButton const&, winrt::Microsoft::UI::Xaml::Controls::SplitButtonClickEventArgs const&)
|
||||
{
|
||||
Rename(false);
|
||||
@@ -657,12 +621,6 @@ namespace winrt::PowerRenameUI::implementation
|
||||
{
|
||||
_TRACER_;
|
||||
|
||||
if (!m_prManager)
|
||||
{
|
||||
// Manager not initialized yet, ignore flag updates
|
||||
return;
|
||||
}
|
||||
|
||||
DWORD flags{};
|
||||
m_prManager->GetFlags(&flags);
|
||||
|
||||
@@ -860,7 +818,6 @@ namespace winrt::PowerRenameUI::implementation
|
||||
UpdateFlag(ModificationTime, UpdateFlagCommand::Reset);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
void MainWindow::ToggleItem(int32_t id, bool checked)
|
||||
@@ -1092,15 +1049,6 @@ namespace winrt::PowerRenameUI::implementation
|
||||
{
|
||||
toggleButton_capitalize().IsChecked(true);
|
||||
}
|
||||
|
||||
int metadataIndex = (flags & MetadataSourceXMP) ? 1 : 0;
|
||||
if (comboBox_metadataSource().SelectedIndex() != metadataIndex)
|
||||
{
|
||||
comboBox_metadataSource().SelectedIndex(metadataIndex);
|
||||
}
|
||||
|
||||
auto metadataType = metadataIndex == 1 ? PowerRenameLib::MetadataType::XMP : PowerRenameLib::MetadataType::EXIF;
|
||||
UpdateMetadataShortcuts(metadataType);
|
||||
}
|
||||
|
||||
void MainWindow::UpdateCounts()
|
||||
@@ -1133,220 +1081,6 @@ namespace winrt::PowerRenameUI::implementation
|
||||
RenamedCount(hstring{ std::to_wstring(m_renamingCount) });
|
||||
}
|
||||
|
||||
void MainWindow::UpdateMetadataShortcuts(PowerRenameLib::MetadataType metadataType)
|
||||
{
|
||||
// Clear existing list
|
||||
m_metadataShortcuts.Clear();
|
||||
|
||||
// Get supported patterns for the selected metadata type
|
||||
auto supportedPatterns = PowerRenameLib::MetadataPatternExtractor::GetSupportedPatterns(metadataType);
|
||||
|
||||
auto factory = winrt::get_activation_factory<ResourceManager, IResourceManagerFactory>();
|
||||
ResourceManager manager = factory.CreateInstance(L"PowerToys.PowerRename.pri");
|
||||
|
||||
// Add each supported pattern to the list
|
||||
for (const auto& pattern : supportedPatterns)
|
||||
{
|
||||
std::wstring resourceKey = L"Resources/MetadataCheatSheet_" + ConvertPatternToResourceKey(pattern);
|
||||
winrt::hstring patternWithDollar = winrt::hstring(L"$" + pattern);
|
||||
|
||||
try {
|
||||
auto description = manager.MainResourceMap().GetValue(resourceKey).ValueAsString();
|
||||
m_metadataShortcuts.Append(winrt::make<PatternSnippet>(patternWithDollar, description));
|
||||
}
|
||||
catch (...) {
|
||||
// If resource doesn't exist, use the pattern name as description
|
||||
m_metadataShortcuts.Append(winrt::make<PatternSnippet>(patternWithDollar, winrt::hstring(pattern)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::wstring MainWindow::ConvertPatternToResourceKey(const std::wstring& pattern)
|
||||
{
|
||||
// Special cases for patterns that don't follow the standard naming convention
|
||||
if (pattern == L"TITLE")
|
||||
{
|
||||
return L"DocTitle";
|
||||
}
|
||||
else if (pattern == L"DATE_TAKEN_YYYY")
|
||||
{
|
||||
return L"DateTakenYear4";
|
||||
}
|
||||
else if (pattern == L"DATE_TAKEN_YY")
|
||||
{
|
||||
return L"DateTakenYear2";
|
||||
}
|
||||
else if (pattern == L"DATE_TAKEN_MM")
|
||||
{
|
||||
return L"DateTakenMonth";
|
||||
}
|
||||
else if (pattern == L"DATE_TAKEN_DD")
|
||||
{
|
||||
return L"DateTakenDay";
|
||||
}
|
||||
else if (pattern == L"DATE_TAKEN_HH")
|
||||
{
|
||||
return L"DateTakenHour";
|
||||
}
|
||||
else if (pattern == L"DATE_TAKEN_mm")
|
||||
{
|
||||
return L"DateTakenMinute";
|
||||
}
|
||||
else if (pattern == L"DATE_TAKEN_SS")
|
||||
{
|
||||
return L"DateTakenSecond";
|
||||
}
|
||||
else if (pattern == L"CREATE_DATE_YYYY")
|
||||
{
|
||||
return L"CreateDateYear4";
|
||||
}
|
||||
else if (pattern == L"CREATE_DATE_YY")
|
||||
{
|
||||
return L"CreateDateYear2";
|
||||
}
|
||||
else if (pattern == L"CREATE_DATE_MM")
|
||||
{
|
||||
return L"CreateDateMonth";
|
||||
}
|
||||
else if (pattern == L"CREATE_DATE_DD")
|
||||
{
|
||||
return L"CreateDateDay";
|
||||
}
|
||||
else if (pattern == L"CREATE_DATE_HH")
|
||||
{
|
||||
return L"CreateDateHour";
|
||||
}
|
||||
else if (pattern == L"CREATE_DATE_mm")
|
||||
{
|
||||
return L"CreateDateMinute";
|
||||
}
|
||||
else if (pattern == L"CREATE_DATE_SS")
|
||||
{
|
||||
return L"CreateDateSecond";
|
||||
}
|
||||
else if (pattern == L"MODIFY_DATE_YYYY")
|
||||
{
|
||||
return L"ModifyDateYear4";
|
||||
}
|
||||
else if (pattern == L"MODIFY_DATE_YY")
|
||||
{
|
||||
return L"ModifyDateYear2";
|
||||
}
|
||||
else if (pattern == L"MODIFY_DATE_MM")
|
||||
{
|
||||
return L"ModifyDateMonth";
|
||||
}
|
||||
else if (pattern == L"MODIFY_DATE_DD")
|
||||
{
|
||||
return L"ModifyDateDay";
|
||||
}
|
||||
else if (pattern == L"MODIFY_DATE_HH")
|
||||
{
|
||||
return L"ModifyDateHour";
|
||||
}
|
||||
else if (pattern == L"MODIFY_DATE_mm")
|
||||
{
|
||||
return L"ModifyDateMinute";
|
||||
}
|
||||
else if (pattern == L"MODIFY_DATE_SS")
|
||||
{
|
||||
return L"ModifyDateSecond";
|
||||
}
|
||||
else if (pattern == L"METADATA_DATE_YYYY")
|
||||
{
|
||||
return L"MetadataDateYear4";
|
||||
}
|
||||
else if (pattern == L"METADATA_DATE_YY")
|
||||
{
|
||||
return L"MetadataDateYear2";
|
||||
}
|
||||
else if (pattern == L"METADATA_DATE_MM")
|
||||
{
|
||||
return L"MetadataDateMonth";
|
||||
}
|
||||
else if (pattern == L"METADATA_DATE_DD")
|
||||
{
|
||||
return L"MetadataDateDay";
|
||||
}
|
||||
else if (pattern == L"METADATA_DATE_HH")
|
||||
{
|
||||
return L"MetadataDateHour";
|
||||
}
|
||||
else if (pattern == L"METADATA_DATE_mm")
|
||||
{
|
||||
return L"MetadataDateMinute";
|
||||
}
|
||||
else if (pattern == L"METADATA_DATE_SS")
|
||||
{
|
||||
return L"MetadataDateSecond";
|
||||
}
|
||||
else if (pattern == L"ISO")
|
||||
{
|
||||
return L"ISO";
|
||||
}
|
||||
else if (pattern == L"TITLE")
|
||||
{
|
||||
return L"DocTitle";
|
||||
}
|
||||
else if (pattern == L"DESCRIPTION")
|
||||
{
|
||||
return L"DocDescription";
|
||||
}
|
||||
else if (pattern == L"CREATOR")
|
||||
{
|
||||
return L"DocCreator";
|
||||
}
|
||||
else if (pattern == L"SUBJECT")
|
||||
{
|
||||
return L"DocSubject";
|
||||
}
|
||||
else if (pattern == L"RIGHTS")
|
||||
{
|
||||
return L"Rights";
|
||||
}
|
||||
|
||||
// Convert pattern name to resource key format
|
||||
// e.g., "CAMERA_MAKE" -> "CameraMake"
|
||||
std::wstring result;
|
||||
bool capitalizeNext = true;
|
||||
|
||||
for (wchar_t ch : pattern)
|
||||
{
|
||||
if (ch == L'_')
|
||||
{
|
||||
capitalizeNext = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (capitalizeNext)
|
||||
{
|
||||
result += static_cast<wchar_t>(std::toupper(ch));
|
||||
capitalizeNext = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
result += static_cast<wchar_t>(std::tolower(ch));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void MainWindow::UpdateMetadataSourceFlags(int selectedIndex)
|
||||
{
|
||||
// Clear all metadata source flags first
|
||||
UpdateFlag(MetadataSourceEXIF, UpdateFlagCommand::Reset);
|
||||
UpdateFlag(MetadataSourceXMP, UpdateFlagCommand::Reset);
|
||||
|
||||
// Set the appropriate metadata source flag based on selection
|
||||
switch(selectedIndex) {
|
||||
case 0: UpdateFlag(MetadataSourceEXIF, UpdateFlagCommand::Set); break;
|
||||
case 1: UpdateFlag(MetadataSourceXMP, UpdateFlagCommand::Set); break;
|
||||
default: UpdateFlag(MetadataSourceEXIF, UpdateFlagCommand::Set); break; // Default to EXIF
|
||||
}
|
||||
}
|
||||
|
||||
HRESULT MainWindow::OnRename(_In_ IPowerRenameItem* /*renameItem*/)
|
||||
{
|
||||
UpdateCounts();
|
||||
@@ -1388,6 +1122,3 @@ namespace winrt::PowerRenameUI::implementation
|
||||
return S_OK;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#include "winrt/Windows.UI.Xaml.h"
|
||||
#include "winrt/Windows.UI.Xaml.Markup.h"
|
||||
@@ -20,8 +20,6 @@
|
||||
#include <PowerRenameManager.h>
|
||||
#include <PowerRenameInterfaces.h>
|
||||
#include <PowerRenameMRU.h>
|
||||
#include <MetadataTypes.h>
|
||||
#include <MetadataPatternExtractor.h>
|
||||
|
||||
namespace winrt::PowerRenameUI::implementation
|
||||
{
|
||||
@@ -90,7 +88,6 @@ namespace winrt::PowerRenameUI::implementation
|
||||
winrt::Windows::Foundation::Collections::IObservableVector<PowerRenameUI::PatternSnippet> DateTimeShortcuts() { return m_dateTimeShortcuts; }
|
||||
winrt::Windows::Foundation::Collections::IObservableVector<PowerRenameUI::PatternSnippet> CounterShortcuts() { return m_CounterShortcuts; }
|
||||
winrt::Windows::Foundation::Collections::IObservableVector<PowerRenameUI::PatternSnippet> RandomizerShortcuts() { return m_RandomizerShortcuts; }
|
||||
winrt::Windows::Foundation::Collections::IObservableVector<PowerRenameUI::PatternSnippet> MetadataShortcuts() { return m_metadataShortcuts; }
|
||||
|
||||
hstring OriginalCount();
|
||||
void OriginalCount(hstring value);
|
||||
@@ -114,7 +111,6 @@ namespace winrt::PowerRenameUI::implementation
|
||||
winrt::Windows::Foundation::Collections::IObservableVector<PowerRenameUI::PatternSnippet> m_dateTimeShortcuts;
|
||||
winrt::Windows::Foundation::Collections::IObservableVector<PowerRenameUI::PatternSnippet> m_CounterShortcuts;
|
||||
winrt::Windows::Foundation::Collections::IObservableVector<PowerRenameUI::PatternSnippet> m_RandomizerShortcuts;
|
||||
winrt::Windows::Foundation::Collections::IObservableVector<PowerRenameUI::PatternSnippet> m_metadataShortcuts;
|
||||
|
||||
// Used by PowerRenameManagerEvents
|
||||
HRESULT OnRename(_In_ IPowerRenameItem* renameItem);
|
||||
@@ -148,9 +144,6 @@ namespace winrt::PowerRenameUI::implementation
|
||||
HRESULT OpenSettingsApp();
|
||||
void SetCheckboxesFromFlags(DWORD flags);
|
||||
void UpdateCounts();
|
||||
void UpdateMetadataShortcuts(PowerRenameLib::MetadataType metadataType);
|
||||
std::wstring ConvertPatternToResourceKey(const std::wstring& pattern);
|
||||
void UpdateMetadataSourceFlags(int selectedIndex);
|
||||
|
||||
Shared::Trace::ETWTrace m_etwTrace{};
|
||||
|
||||
@@ -174,8 +167,6 @@ namespace winrt::PowerRenameUI::implementation
|
||||
public:
|
||||
void RegExItemClick(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::Controls::ItemClickEventArgs const& e);
|
||||
void DateTimeItemClick(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::Controls::ItemClickEventArgs const& e);
|
||||
void MetadataItemClick(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::Controls::ItemClickEventArgs const& e);
|
||||
void MetadataSourceComboBox_SelectionChanged(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::Controls::SelectionChangedEventArgs const& e);
|
||||
void button_rename_Click(winrt::Microsoft::UI::Xaml::Controls::SplitButton const& sender, winrt::Microsoft::UI::Xaml::Controls::SplitButtonClickEventArgs const& args);
|
||||
void MenuFlyoutItem_Click(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e);
|
||||
void OpenDocs(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e);
|
||||
@@ -188,4 +179,3 @@ namespace winrt::PowerRenameUI::factory_implementation
|
||||
{
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -414,9 +414,6 @@
|
||||
<data name="TextBlock_FileTime.Text" xml:space="preserve">
|
||||
<value>Time used for replacement</value>
|
||||
</data>
|
||||
<data name="TextBlock_MetadataSource.Text" xml:space="preserve">
|
||||
<value>Metadata source for replacement</value>
|
||||
</data>
|
||||
<data name="FileTimeParts_CreationTime.Content" xml:space="preserve">
|
||||
<value>Creation Time</value>
|
||||
</data>
|
||||
@@ -426,149 +423,4 @@
|
||||
<data name="FileTimeParts_AccessTime.Content" xml:space="preserve">
|
||||
<value>Access Time</value>
|
||||
</data>
|
||||
|
||||
<data name="MetadataSource_EXIF.Content" xml:space="preserve">
|
||||
<value>EXIF Metadata</value>
|
||||
</data>
|
||||
<data name="MetadataSource_XMP.Content" xml:space="preserve">
|
||||
<value>XMP Metadata</value>
|
||||
</data>
|
||||
|
||||
<data name="MetadataCheatSheet_Title.Text" xml:space="preserve">
|
||||
<value>Replace with media metadata</value>
|
||||
</data>
|
||||
<data name="MetadataCheatSheet_CameraMake" xml:space="preserve">
|
||||
<value>Camera manufacturer name</value>
|
||||
</data>
|
||||
<data name="MetadataCheatSheet_CameraModel" xml:space="preserve">
|
||||
<value>Camera model name</value>
|
||||
</data>
|
||||
<data name="MetadataCheatSheet_Lens" xml:space="preserve">
|
||||
<value>Lens model name</value>
|
||||
</data>
|
||||
<data name="MetadataCheatSheet_ISO" xml:space="preserve">
|
||||
<value>ISO sensitivity value</value>
|
||||
</data>
|
||||
<data name="MetadataCheatSheet_Aperture" xml:space="preserve">
|
||||
<value>F-number aperture value</value>
|
||||
</data>
|
||||
<data name="MetadataCheatSheet_Shutter" xml:space="preserve">
|
||||
<value>Shutter speed value</value>
|
||||
</data>
|
||||
<data name="MetadataCheatSheet_Focal" xml:space="preserve">
|
||||
<value>Focal length in millimeters</value>
|
||||
</data>
|
||||
<data name="MetadataCheatSheet_Flash" xml:space="preserve">
|
||||
<value>Flash status (On/Off)</value>
|
||||
</data>
|
||||
<data name="MetadataCheatSheet_Width" xml:space="preserve">
|
||||
<value>Image width in pixels</value>
|
||||
</data>
|
||||
<data name="MetadataCheatSheet_Height" xml:space="preserve">
|
||||
<value>Image height in pixels</value>
|
||||
</data>
|
||||
<data name="MetadataCheatSheet_Author" xml:space="preserve">
|
||||
<value>Image author/artist</value>
|
||||
</data>
|
||||
<data name="MetadataCheatSheet_Copyright" xml:space="preserve">
|
||||
<value>Copyright information</value>
|
||||
</data>
|
||||
<data name="MetadataCheatSheet_Latitude" xml:space="preserve">
|
||||
<value>GPS latitude coordinate</value>
|
||||
</data>
|
||||
<data name="MetadataCheatSheet_Longitude" xml:space="preserve">
|
||||
<value>GPS longitude coordinate</value>
|
||||
</data>
|
||||
<data name="MetadataCheatSheet_Altitude" xml:space="preserve">
|
||||
<value>GPS altitude in meters</value>
|
||||
</data>
|
||||
<data name="MetadataCheatSheet_ExposureBias" xml:space="preserve">
|
||||
<value>Exposure compensation value</value>
|
||||
</data>
|
||||
<data name="MetadataCheatSheet_Orientation" xml:space="preserve">
|
||||
<value>Image orientation</value>
|
||||
</data>
|
||||
<data name="MetadataCheatSheet_ColorSpace" xml:space="preserve">
|
||||
<value>Color space information</value>
|
||||
</data>
|
||||
<data name="MetadataCheatSheet_DateTakenYear4" xml:space="preserve">
|
||||
<value>Year photo was taken (4 digits)</value>
|
||||
</data>
|
||||
<data name="MetadataCheatSheet_DateTakenYear2" xml:space="preserve">
|
||||
<value>Year photo was taken (2 digits)</value>
|
||||
</data>
|
||||
<data name="MetadataCheatSheet_DateTakenMonth" xml:space="preserve">
|
||||
<value>Month photo was taken (01-12)</value>
|
||||
</data>
|
||||
<data name="MetadataCheatSheet_DateTakenDay" xml:space="preserve">
|
||||
<value>Day photo was taken (01-31)</value>
|
||||
</data>
|
||||
<data name="MetadataCheatSheet_DateTakenHour" xml:space="preserve">
|
||||
<value>Hour photo was taken (00-23)</value>
|
||||
</data>
|
||||
<data name="MetadataCheatSheet_DateTakenMinute" xml:space="preserve">
|
||||
<value>Minute photo was taken (00-59)</value>
|
||||
</data>
|
||||
<data name="MetadataCheatSheet_DateTakenSecond" xml:space="preserve">
|
||||
<value>Second photo was taken (00-59)</value>
|
||||
</data>
|
||||
<data name="MetadataCheatSheet_CreateDateYear4" xml:space="preserve">
|
||||
<value>Year from XMP create date (4 digits)</value>
|
||||
</data>
|
||||
<data name="MetadataCheatSheet_CreateDateYear2" xml:space="preserve">
|
||||
<value>Year from XMP create date (2 digits)</value>
|
||||
</data>
|
||||
<data name="MetadataCheatSheet_CreateDateMonth" xml:space="preserve">
|
||||
<value>Month from XMP create date (01-12)</value>
|
||||
</data>
|
||||
<data name="MetadataCheatSheet_CreateDateDay" xml:space="preserve">
|
||||
<value>Day from XMP create date (01-31)</value>
|
||||
</data>
|
||||
<data name="MetadataCheatSheet_CreateDateHour" xml:space="preserve">
|
||||
<value>Hour from XMP create date (00-23)</value>
|
||||
</data>
|
||||
<data name="MetadataCheatSheet_CreateDateMinute" xml:space="preserve">
|
||||
<value>Minute from XMP create date (00-59)</value>
|
||||
</data>
|
||||
<data name="MetadataCheatSheet_CreateDateSecond" xml:space="preserve">
|
||||
<value>Second from XMP create date (00-59)</value>
|
||||
</data>
|
||||
|
||||
<!-- XMP patterns -->
|
||||
<data name="MetadataCheatSheet_CreatorTool" xml:space="preserve">
|
||||
<value>Software used to create/edit</value>
|
||||
</data>
|
||||
|
||||
<!-- Dublin Core patterns -->
|
||||
<data name="MetadataCheatSheet_DocTitle" xml:space="preserve">
|
||||
<value>Document title</value>
|
||||
</data>
|
||||
<data name="MetadataCheatSheet_DocDescription" xml:space="preserve">
|
||||
<value>Document description</value>
|
||||
</data>
|
||||
<data name="MetadataCheatSheet_DocCreator" xml:space="preserve">
|
||||
<value>Document creator/author</value>
|
||||
</data>
|
||||
<data name="MetadataCheatSheet_DocSubject" xml:space="preserve">
|
||||
<value>Keywords/tags</value>
|
||||
</data>
|
||||
|
||||
<!-- XMP Rights pattern -->
|
||||
<data name="MetadataCheatSheet_Rights" xml:space="preserve">
|
||||
<value>Copyright/rights information</value>
|
||||
</data>
|
||||
|
||||
<!-- XMP Media Management schema patterns -->
|
||||
<data name="MetadataCheatSheet_DocumentId" xml:space="preserve">
|
||||
<value>Document unique identifier</value>
|
||||
</data>
|
||||
<data name="MetadataCheatSheet_InstanceId" xml:space="preserve">
|
||||
<value>Instance unique identifier</value>
|
||||
</data>
|
||||
<data name="MetadataCheatSheet_OriginalDocumentId" xml:space="preserve">
|
||||
<value>Original document identifier</value>
|
||||
</data>
|
||||
<data name="MetadataCheatSheet_VersionId" xml:space="preserve">
|
||||
<value>Version identifier</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -24,7 +24,7 @@
|
||||
<AdditionalIncludeDirectories>..\lib\;..\PowerRenameUILib\;..\;..\..\..\;..\..\..\common\telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<AdditionalDependencies>Pathcch.lib;comctl32.lib;shcore.lib;windowscodecs.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalDependencies>Pathcch.lib;comctl32.lib;shcore.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<ModuleDefinitionFile>PowerRenameExt.def</ModuleDefinitionFile>
|
||||
<DelayLoadDLLs>gdi32.dll;shell32.dll;ole32.dll;shlwapi.dll;oleaut32.dll;%(DelayLoadDLLs)</DelayLoadDLLs>
|
||||
</Link>
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
#include "pch.h"
|
||||
#include "Helpers.h"
|
||||
#include "MetadataTypes.h"
|
||||
#include <regex>
|
||||
#include <ShlGuid.h>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <algorithm>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
@@ -16,50 +12,6 @@ namespace
|
||||
const int MAX_INPUT_STRING_LEN = 1024;
|
||||
|
||||
const wchar_t c_rootRegPath[] = L"Software\\Microsoft\\PowerRename";
|
||||
|
||||
// Helper function: Find the longest matching pattern starting at the given position
|
||||
// Returns the matched pattern name, or empty string if no match found
|
||||
std::wstring FindLongestPattern(
|
||||
const std::wstring& input,
|
||||
size_t startPos,
|
||||
size_t maxPatternLength,
|
||||
const std::unordered_set<std::wstring>& validPatterns)
|
||||
{
|
||||
const size_t remaining = input.length() - startPos;
|
||||
const size_t searchLength = std::min(maxPatternLength, remaining);
|
||||
|
||||
// Try to match from longest to shortest to ensure greedy matching
|
||||
// e.g., DATE_TAKEN_YYYY should be matched before DATE_TAKEN_YY
|
||||
for (size_t len = searchLength; len > 0; --len)
|
||||
{
|
||||
std::wstring candidate = input.substr(startPos, len);
|
||||
if (validPatterns.find(candidate) != validPatterns.end())
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return L"";
|
||||
}
|
||||
|
||||
// Helper function: Get the replacement value for a pattern
|
||||
// Returns the actual metadata value if available; if not, returns the pattern name with $ prefix
|
||||
std::wstring GetPatternValue(
|
||||
const std::wstring& patternName,
|
||||
const PowerRenameLib::MetadataPatternMap& patterns)
|
||||
{
|
||||
auto it = patterns.find(patternName);
|
||||
|
||||
// Return actual value if found and valid (non-empty)
|
||||
if (it != patterns.end() && !it->second.empty())
|
||||
{
|
||||
return it->second;
|
||||
}
|
||||
|
||||
// Return pattern name with $ prefix if value is unavailable
|
||||
// This provides visual feedback that the field exists but has no data
|
||||
return L"$" + patternName;
|
||||
}
|
||||
}
|
||||
|
||||
HRESULT GetTrimmedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source)
|
||||
@@ -319,72 +271,6 @@ bool isFileTimeUsed(_In_ PCWSTR source)
|
||||
return used;
|
||||
}
|
||||
|
||||
bool isMetadataUsed(_In_ PCWSTR source, PowerRenameLib::MetadataType metadataType, _In_opt_ PCWSTR filePath, bool isFolder)
|
||||
{
|
||||
if (!source) return false;
|
||||
|
||||
// Early exit: If file path is provided, check file type first (fastest checks)
|
||||
// This avoids expensive pattern matching for files that don't support metadata
|
||||
if (filePath != nullptr)
|
||||
{
|
||||
// Folders don't support metadata extraction
|
||||
if (isFolder)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if file path is valid
|
||||
if (wcslen(filePath) == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get file extension
|
||||
std::wstring extension = fs::path(filePath).extension().wstring();
|
||||
|
||||
// Convert to lowercase for case-insensitive comparison
|
||||
std::transform(extension.begin(), extension.end(), extension.begin(), ::towlower);
|
||||
|
||||
// According to the metadata support table, only these formats support metadata extraction:
|
||||
// - JPEG (IFD, Exif, XMP, GPS, IPTC) - supports fast metadata encoding
|
||||
// - TIFF (IFD, Exif, XMP, GPS, IPTC) - supports fast metadata encoding
|
||||
// - PNG (text chunks)
|
||||
static const std::unordered_set<std::wstring> supportedExtensions = {
|
||||
L".jpg",
|
||||
L".jpeg",
|
||||
L".png",
|
||||
L".tif",
|
||||
L".tiff"
|
||||
};
|
||||
|
||||
// If file type doesn't support metadata, no need to check patterns
|
||||
if (supportedExtensions.find(extension) == supportedExtensions.end())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Now check if any metadata pattern exists in the source string
|
||||
// This is the most expensive check, so we do it last
|
||||
std::wstring str(source);
|
||||
|
||||
// Get supported patterns for the specified metadata type
|
||||
auto supportedPatterns = PowerRenameLib::MetadataPatternExtractor::GetSupportedPatterns(metadataType);
|
||||
|
||||
// Check if any metadata pattern exists in the source string
|
||||
for (const auto& pattern : supportedPatterns)
|
||||
{
|
||||
std::wstring searchPattern = L"$" + pattern;
|
||||
if (str.find(searchPattern) != std::wstring::npos)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// No metadata pattern found
|
||||
return false;
|
||||
}
|
||||
|
||||
HRESULT GetDatedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, SYSTEMTIME fileTime)
|
||||
{
|
||||
std::locale::global(std::locale(""));
|
||||
@@ -411,10 +297,10 @@ HRESULT GetDatedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, SY
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$YYYY"), replaceTerm);
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", (fileTime.wYear % 100));
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$YY(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $YYY, $YYYY, or metadata patterns
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$YY"), replaceTerm);
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", (fileTime.wYear % 10));
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$Y(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $YY, $YYYY, or metadata patterns
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$Y"), replaceTerm);
|
||||
|
||||
GetDateFormatEx(localeName, NULL, &fileTime, L"MMMM", formattedDate, MAX_PATH, NULL);
|
||||
formattedDate[0] = towupper(formattedDate[0]);
|
||||
@@ -424,13 +310,13 @@ HRESULT GetDatedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, SY
|
||||
GetDateFormatEx(localeName, NULL, &fileTime, L"MMM", formattedDate, MAX_PATH, NULL);
|
||||
formattedDate[0] = towupper(formattedDate[0]);
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", formattedDate);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$MMM(?!M)"), replaceTerm); // Negative lookahead prevents matching $MMMM
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$MMM"), replaceTerm);
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wMonth);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$MM(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $MMM, $MMMM, or metadata patterns
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$MM"), replaceTerm);
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wMonth);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$M(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $MM, $MMM, $MMMM, or metadata patterns
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$M"), replaceTerm);
|
||||
|
||||
GetDateFormatEx(localeName, NULL, &fileTime, L"dddd", formattedDate, MAX_PATH, NULL);
|
||||
formattedDate[0] = towupper(formattedDate[0]);
|
||||
@@ -440,19 +326,19 @@ HRESULT GetDatedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, SY
|
||||
GetDateFormatEx(localeName, NULL, &fileTime, L"ddd", formattedDate, MAX_PATH, NULL);
|
||||
formattedDate[0] = towupper(formattedDate[0]);
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", formattedDate);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$DDD(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $DDDD or metadata patterns
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$DDD"), replaceTerm);
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wDay);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$DD(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $DDD, $DDDD, or metadata patterns
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$DD"), replaceTerm);
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wDay);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$D(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $DD, $DDD, $DDDD, or metadata patterns like $DATE_TAKEN_YYYY
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$D"), replaceTerm);
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", hour12);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$HH(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $HHH or metadata patterns
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$HH"), replaceTerm);
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", hour12);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$H(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $HH or metadata patterns
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$H"), replaceTerm);
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", (fileTime.wHour < 12) ? L"AM" : L"PM");
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$TT"), replaceTerm);
|
||||
@@ -461,31 +347,31 @@ HRESULT GetDatedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, SY
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$tt"), replaceTerm);
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wHour);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$hh(?!h)"), replaceTerm); // Negative lookahead prevents matching $hhh
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$hh"), replaceTerm);
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wHour);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$h(?!h)"), replaceTerm); // Negative lookahead prevents matching $hh
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$h"), replaceTerm);
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wMinute);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$mm(?!m)"), replaceTerm); // Negative lookahead prevents matching $mmm
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$mm"), replaceTerm);
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wMinute);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$m(?!m)"), replaceTerm); // Negative lookahead prevents matching $mm
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$m"), replaceTerm);
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wSecond);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$ss(?!s)"), replaceTerm); // Negative lookahead prevents matching $sss
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$ss"), replaceTerm);
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wSecond);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$s(?!s)"), replaceTerm); // Negative lookahead prevents matching $ss
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$s"), replaceTerm);
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%03d"), L"$01", fileTime.wMilliseconds);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$fff(?!f)"), replaceTerm); // Negative lookahead prevents matching $ffff
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$fff"), replaceTerm);
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wMilliseconds / 10);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$ff(?!f)"), replaceTerm); // Negative lookahead prevents matching $fff
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$ff"), replaceTerm);
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wMilliseconds / 100);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$f(?!f)"), replaceTerm); // Negative lookahead prevents matching $ff or $fff
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$f"), replaceTerm);
|
||||
|
||||
hr = StringCchCopy(result, cchMax, res.c_str());
|
||||
}
|
||||
@@ -493,91 +379,6 @@ HRESULT GetDatedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, SY
|
||||
return hr;
|
||||
}
|
||||
|
||||
HRESULT GetMetadataFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, const PowerRenameLib::MetadataPatternMap& patterns)
|
||||
{
|
||||
if (!source || wcslen(source) == 0)
|
||||
{
|
||||
return E_INVALIDARG;
|
||||
}
|
||||
|
||||
std::wstring input(source);
|
||||
std::wstring output;
|
||||
output.reserve(input.length() * 2); // Reserve space to avoid frequent reallocations
|
||||
|
||||
// Build pattern lookup table for fast validation
|
||||
// Using all possible patterns to recognize valid pattern names even when metadata is unavailable
|
||||
auto allPatterns = PowerRenameLib::MetadataPatternExtractor::GetAllPossiblePatterns();
|
||||
std::unordered_set<std::wstring> validPatterns;
|
||||
validPatterns.reserve(allPatterns.size());
|
||||
size_t maxPatternLength = 0;
|
||||
for (const auto& pattern : allPatterns)
|
||||
{
|
||||
validPatterns.insert(pattern);
|
||||
maxPatternLength = std::max(maxPatternLength, pattern.length());
|
||||
}
|
||||
|
||||
size_t pos = 0;
|
||||
while (pos < input.length())
|
||||
{
|
||||
// Handle regular characters
|
||||
if (input[pos] != L'$')
|
||||
{
|
||||
output += input[pos];
|
||||
pos++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Count consecutive dollar signs
|
||||
size_t dollarCount = 0;
|
||||
while (pos < input.length() && input[pos] == L'$')
|
||||
{
|
||||
dollarCount++;
|
||||
pos++;
|
||||
}
|
||||
|
||||
// Even number of dollars: all are escaped (e.g., $$ -> $, $$$$ -> $$)
|
||||
if (dollarCount % 2 == 0)
|
||||
{
|
||||
output.append(dollarCount / 2, L'$');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Odd number of dollars: pairs are escaped, last one might be a pattern prefix
|
||||
// e.g., $ -> might be pattern, $$$ -> $ + might be pattern
|
||||
size_t escapedDollars = dollarCount / 2;
|
||||
|
||||
// If no more characters, output all dollar signs
|
||||
if (pos >= input.length())
|
||||
{
|
||||
output.append(dollarCount, L'$');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try to match a pattern (greedy matching for longest pattern)
|
||||
std::wstring matchedPattern = FindLongestPattern(input, pos, maxPatternLength, validPatterns);
|
||||
|
||||
if (matchedPattern.empty())
|
||||
{
|
||||
// No pattern matched, output all dollar signs
|
||||
output.append(dollarCount, L'$');
|
||||
}
|
||||
else
|
||||
{
|
||||
// Pattern matched
|
||||
output.append(escapedDollars, L'$'); // Output escaped dollars first
|
||||
|
||||
// Replace pattern with its value or keep pattern name if value unavailable
|
||||
std::wstring replacementValue = GetPatternValue(matchedPattern, patterns);
|
||||
output += replacementValue;
|
||||
|
||||
pos += matchedPattern.length();
|
||||
}
|
||||
}
|
||||
|
||||
return StringCchCopy(result, cchMax, output.c_str());
|
||||
}
|
||||
|
||||
|
||||
HRESULT GetShellItemArrayFromDataObject(_In_ IUnknown* dataSource, _COM_Outptr_ IShellItemArray** items)
|
||||
{
|
||||
*items = nullptr;
|
||||
@@ -906,4 +707,4 @@ std::wstring CreateGuidStringWithoutBrackets()
|
||||
}
|
||||
|
||||
return L"";
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include "PowerRenameInterfaces.h"
|
||||
#include "MetadataTypes.h"
|
||||
#include "MetadataPatternExtractor.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
HRESULT GetTrimmedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source);
|
||||
HRESULT GetTransformedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, DWORD flags, bool isFolder);
|
||||
HRESULT GetDatedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, SYSTEMTIME fileTime);
|
||||
HRESULT GetMetadataFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, const PowerRenameLib::MetadataPatternMap& patterns);
|
||||
bool isFileTimeUsed(_In_ PCWSTR source);
|
||||
bool isMetadataUsed(_In_ PCWSTR source, PowerRenameLib::MetadataType metadataType, _In_opt_ PCWSTR filePath = nullptr, bool isFolder = false);
|
||||
bool ShellItemArrayContainsRenamableItem(_In_ IShellItemArray* shellItemArray);
|
||||
bool DataObjectContainsRenamableItem(_In_ IUnknown* dataSource);
|
||||
HRESULT GetShellItemArrayFromDataObject(_In_ IUnknown* dataSource, _COM_Outptr_ IShellItemArray** items);
|
||||
|
||||
@@ -1,237 +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.
|
||||
|
||||
#include "pch.h"
|
||||
#include "MetadataFormatHelper.h"
|
||||
#include <format>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
|
||||
using namespace PowerRenameLib;
|
||||
|
||||
// Formatting functions
|
||||
|
||||
std::wstring MetadataFormatHelper::FormatAperture(double aperture)
|
||||
{
|
||||
return std::format(L"f/{:.1f}", aperture);
|
||||
}
|
||||
|
||||
std::wstring MetadataFormatHelper::FormatShutterSpeed(double speed)
|
||||
{
|
||||
if (speed <= 0.0)
|
||||
{
|
||||
return L"0";
|
||||
}
|
||||
|
||||
if (speed >= 1.0)
|
||||
{
|
||||
return std::format(L"{:.1f}s", speed);
|
||||
}
|
||||
|
||||
const double reciprocal = std::round(1.0 / speed);
|
||||
if (reciprocal <= 1.0)
|
||||
{
|
||||
return std::format(L"{:.3f}s", speed);
|
||||
}
|
||||
|
||||
return std::format(L"1/{:.0f}s", reciprocal);
|
||||
}
|
||||
|
||||
std::wstring MetadataFormatHelper::FormatISO(int64_t iso)
|
||||
{
|
||||
if (iso <= 0)
|
||||
{
|
||||
return L"ISO";
|
||||
}
|
||||
|
||||
return std::format(L"ISO {}", iso);
|
||||
}
|
||||
|
||||
std::wstring MetadataFormatHelper::FormatFlash(int64_t flashValue)
|
||||
{
|
||||
switch (flashValue & 0x1)
|
||||
{
|
||||
case 0:
|
||||
return L"Flash Off";
|
||||
case 1:
|
||||
return L"Flash On";
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return std::format(L"Flash 0x{:X}", static_cast<unsigned int>(flashValue));
|
||||
}
|
||||
|
||||
std::wstring MetadataFormatHelper::FormatCoordinate(double coord, bool isLatitude)
|
||||
{
|
||||
wchar_t direction = isLatitude ? (coord >= 0.0 ? L'N' : L'S') : (coord >= 0.0 ? L'E' : L'W');
|
||||
double absolute = std::abs(coord);
|
||||
int degrees = static_cast<int>(absolute);
|
||||
double minutes = (absolute - static_cast<double>(degrees)) * 60.0;
|
||||
|
||||
return std::format(L"{:d}°{:.2f}'{}", degrees, minutes, direction);
|
||||
}
|
||||
|
||||
std::wstring MetadataFormatHelper::FormatSystemTime(const SYSTEMTIME& st)
|
||||
{
|
||||
return std::format(L"{:04d}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}",
|
||||
st.wYear,
|
||||
st.wMonth,
|
||||
st.wDay,
|
||||
st.wHour,
|
||||
st.wMinute,
|
||||
st.wSecond);
|
||||
}
|
||||
|
||||
// Parsing functions
|
||||
|
||||
double MetadataFormatHelper::ParseGPSRational(const PROPVARIANT& pv)
|
||||
{
|
||||
if ((pv.vt & VT_VECTOR) && pv.caub.cElems >= 8)
|
||||
{
|
||||
return ParseSingleRational(pv.caub.pElems, 0);
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
double MetadataFormatHelper::ParseSingleRational(const uint8_t* bytes, size_t offset)
|
||||
{
|
||||
// Parse a single rational number (8 bytes: numerator + denominator)
|
||||
if (!bytes)
|
||||
return 0.0;
|
||||
|
||||
// Note: Callers are responsible for ensuring the buffer is large enough.
|
||||
// This function assumes offset points to at least 8 bytes of valid data.
|
||||
// All current callers perform cElems >= required_size checks before calling.
|
||||
const uint8_t* rationalBytes = bytes + offset;
|
||||
|
||||
// Parse as little-endian uint32_t values
|
||||
uint32_t numerator = static_cast<uint32_t>(rationalBytes[0]) |
|
||||
(static_cast<uint32_t>(rationalBytes[1]) << 8) |
|
||||
(static_cast<uint32_t>(rationalBytes[2]) << 16) |
|
||||
(static_cast<uint32_t>(rationalBytes[3]) << 24);
|
||||
|
||||
uint32_t denominator = static_cast<uint32_t>(rationalBytes[4]) |
|
||||
(static_cast<uint32_t>(rationalBytes[5]) << 8) |
|
||||
(static_cast<uint32_t>(rationalBytes[6]) << 16) |
|
||||
(static_cast<uint32_t>(rationalBytes[7]) << 24);
|
||||
|
||||
if (denominator != 0)
|
||||
{
|
||||
return static_cast<double>(numerator) / static_cast<double>(denominator);
|
||||
}
|
||||
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
double MetadataFormatHelper::ParseSingleSRational(const uint8_t* bytes, size_t offset)
|
||||
{
|
||||
// Parse a single signed rational number (8 bytes: signed numerator + signed denominator)
|
||||
if (!bytes)
|
||||
return 0.0;
|
||||
|
||||
// Note: Callers are responsible for ensuring the buffer is large enough.
|
||||
// This function assumes offset points to at least 8 bytes of valid data.
|
||||
// All current callers perform cElems >= required_size checks before calling.
|
||||
const uint8_t* rationalBytes = bytes + offset;
|
||||
|
||||
// Parse as little-endian int32_t values (signed)
|
||||
// First construct as unsigned, then reinterpret as signed
|
||||
uint32_t numerator_uint = static_cast<uint32_t>(rationalBytes[0]) |
|
||||
(static_cast<uint32_t>(rationalBytes[1]) << 8) |
|
||||
(static_cast<uint32_t>(rationalBytes[2]) << 16) |
|
||||
(static_cast<uint32_t>(rationalBytes[3]) << 24);
|
||||
|
||||
uint32_t denominator_uint = static_cast<uint32_t>(rationalBytes[4]) |
|
||||
(static_cast<uint32_t>(rationalBytes[5]) << 8) |
|
||||
(static_cast<uint32_t>(rationalBytes[6]) << 16) |
|
||||
(static_cast<uint32_t>(rationalBytes[7]) << 24);
|
||||
|
||||
// Reinterpret as signed
|
||||
int32_t numerator = static_cast<int32_t>(numerator_uint);
|
||||
int32_t denominator = static_cast<int32_t>(denominator_uint);
|
||||
|
||||
if (denominator != 0)
|
||||
{
|
||||
return static_cast<double>(numerator) / static_cast<double>(denominator);
|
||||
}
|
||||
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
std::pair<double, double> MetadataFormatHelper::ParseGPSCoordinates(
|
||||
const PROPVARIANT& latitude,
|
||||
const PROPVARIANT& longitude,
|
||||
const PROPVARIANT& latRef,
|
||||
const PROPVARIANT& lonRef)
|
||||
{
|
||||
double lat = 0.0, lon = 0.0;
|
||||
|
||||
// Parse latitude - typically stored as 3 rationals (degrees, minutes, seconds)
|
||||
if ((latitude.vt & VT_VECTOR) && latitude.caub.cElems >= 24) // 3 rationals * 8 bytes each
|
||||
{
|
||||
const uint8_t* bytes = latitude.caub.pElems;
|
||||
|
||||
// degrees, minutes, seconds (each rational is 8 bytes)
|
||||
double degrees = ParseSingleRational(bytes, 0);
|
||||
double minutes = ParseSingleRational(bytes, 8);
|
||||
double seconds = ParseSingleRational(bytes, 16);
|
||||
|
||||
lat = degrees + minutes / 60.0 + seconds / 3600.0;
|
||||
}
|
||||
|
||||
// Parse longitude
|
||||
if ((longitude.vt & VT_VECTOR) && longitude.caub.cElems >= 24)
|
||||
{
|
||||
const uint8_t* bytes = longitude.caub.pElems;
|
||||
|
||||
double degrees = ParseSingleRational(bytes, 0);
|
||||
double minutes = ParseSingleRational(bytes, 8);
|
||||
double seconds = ParseSingleRational(bytes, 16);
|
||||
|
||||
lon = degrees + minutes / 60.0 + seconds / 3600.0;
|
||||
}
|
||||
|
||||
// Apply direction references (N/S for latitude, E/W for longitude)
|
||||
if (latRef.vt == VT_LPSTR && latRef.pszVal)
|
||||
{
|
||||
if (strcmp(latRef.pszVal, "S") == 0)
|
||||
lat = -lat;
|
||||
}
|
||||
|
||||
if (lonRef.vt == VT_LPSTR && lonRef.pszVal)
|
||||
{
|
||||
if (strcmp(lonRef.pszVal, "W") == 0)
|
||||
lon = -lon;
|
||||
}
|
||||
|
||||
return { lat, lon };
|
||||
}
|
||||
|
||||
std::wstring MetadataFormatHelper::SanitizeForFileName(const std::wstring& str)
|
||||
{
|
||||
// Windows illegal filename characters: < > : " / \ | ? *
|
||||
// Also control characters (0-31) and some others
|
||||
std::wstring sanitized = str;
|
||||
|
||||
// Replace illegal characters with underscore
|
||||
for (auto& ch : sanitized)
|
||||
{
|
||||
// Check for illegal characters
|
||||
if (ch == L'<' || ch == L'>' || ch == L':' || ch == L'"' ||
|
||||
ch == L'/' || ch == L'\\' || ch == L'|' || ch == L'?' || ch == L'*' ||
|
||||
ch < 32) // Control characters
|
||||
{
|
||||
ch = L'_';
|
||||
}
|
||||
}
|
||||
|
||||
// Also remove trailing dots and spaces (Windows doesn't like them at end of filename)
|
||||
while (!sanitized.empty() && (sanitized.back() == L'.' || sanitized.back() == L' '))
|
||||
{
|
||||
sanitized.pop_back();
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
@@ -1,117 +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.
|
||||
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <windows.h>
|
||||
#include <propvarutil.h>
|
||||
|
||||
namespace PowerRenameLib
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper class for formatting and parsing metadata values
|
||||
/// Provides static utility functions for converting metadata to human-readable strings
|
||||
/// and parsing raw metadata values
|
||||
/// </summary>
|
||||
class MetadataFormatHelper
|
||||
{
|
||||
public:
|
||||
// Formatting functions - Convert metadata values to display strings
|
||||
|
||||
/// <summary>
|
||||
/// Format aperture value (f-number)
|
||||
/// </summary>
|
||||
/// <param name="aperture">Aperture value (e.g., 2.8)</param>
|
||||
/// <returns>Formatted string (e.g., "f/2.8")</returns>
|
||||
static std::wstring FormatAperture(double aperture);
|
||||
|
||||
/// <summary>
|
||||
/// Format shutter speed
|
||||
/// </summary>
|
||||
/// <param name="speed">Shutter speed in seconds</param>
|
||||
/// <returns>Formatted string (e.g., "1/100s" or "2.5s")</returns>
|
||||
static std::wstring FormatShutterSpeed(double speed);
|
||||
|
||||
/// <summary>
|
||||
/// Format ISO value
|
||||
/// </summary>
|
||||
/// <param name="iso">ISO speed value</param>
|
||||
/// <returns>Formatted string (e.g., "ISO 400")</returns>
|
||||
static std::wstring FormatISO(int64_t iso);
|
||||
|
||||
/// <summary>
|
||||
/// Format flash status
|
||||
/// </summary>
|
||||
/// <param name="flashValue">Flash value from EXIF</param>
|
||||
/// <returns>Formatted string (e.g., "Flash On" or "Flash Off")</returns>
|
||||
static std::wstring FormatFlash(int64_t flashValue);
|
||||
|
||||
/// <summary>
|
||||
/// Format GPS coordinate
|
||||
/// </summary>
|
||||
/// <param name="coord">Coordinate value in decimal degrees</param>
|
||||
/// <param name="isLatitude">true for latitude, false for longitude</param>
|
||||
/// <returns>Formatted string (e.g., "40°26.76'N")</returns>
|
||||
static std::wstring FormatCoordinate(double coord, bool isLatitude);
|
||||
|
||||
/// <summary>
|
||||
/// Format SYSTEMTIME to string
|
||||
/// </summary>
|
||||
/// <param name="st">SYSTEMTIME structure</param>
|
||||
/// <returns>Formatted string (e.g., "2024-03-15 14:30:45")</returns>
|
||||
static std::wstring FormatSystemTime(const SYSTEMTIME& st);
|
||||
|
||||
// Parsing functions - Convert raw metadata to usable values
|
||||
|
||||
/// <summary>
|
||||
/// Parse GPS rational value from PROPVARIANT
|
||||
/// </summary>
|
||||
/// <param name="pv">PROPVARIANT containing GPS rational data</param>
|
||||
/// <returns>Parsed double value</returns>
|
||||
static double ParseGPSRational(const PROPVARIANT& pv);
|
||||
|
||||
/// <summary>
|
||||
/// Parse single rational value from byte array
|
||||
/// </summary>
|
||||
/// <param name="bytes">Byte array containing rational data</param>
|
||||
/// <param name="offset">Offset in the byte array</param>
|
||||
/// <returns>Parsed double value (numerator / denominator)</returns>
|
||||
static double ParseSingleRational(const uint8_t* bytes, size_t offset);
|
||||
|
||||
/// <summary>
|
||||
/// Parse single signed rational value from byte array
|
||||
/// </summary>
|
||||
/// <param name="bytes">Byte array containing signed rational data</param>
|
||||
/// <param name="offset">Offset in the byte array</param>
|
||||
/// <returns>Parsed double value (signed numerator / signed denominator)</returns>
|
||||
static double ParseSingleSRational(const uint8_t* bytes, size_t offset);
|
||||
|
||||
/// <summary>
|
||||
/// Parse GPS coordinates from PROPVARIANT values
|
||||
/// </summary>
|
||||
/// <param name="latitude">PROPVARIANT containing latitude</param>
|
||||
/// <param name="longitude">PROPVARIANT containing longitude</param>
|
||||
/// <param name="latRef">PROPVARIANT containing latitude reference (N/S)</param>
|
||||
/// <param name="lonRef">PROPVARIANT containing longitude reference (E/W)</param>
|
||||
/// <returns>Pair of (latitude, longitude) in decimal degrees</returns>
|
||||
static std::pair<double, double> ParseGPSCoordinates(
|
||||
const PROPVARIANT& latitude,
|
||||
const PROPVARIANT& longitude,
|
||||
const PROPVARIANT& latRef,
|
||||
const PROPVARIANT& lonRef);
|
||||
|
||||
/// <summary>
|
||||
/// Sanitize a string to make it safe for use in filenames
|
||||
/// Replaces illegal filename characters (< > : " / \ | ? * and control chars) with underscore
|
||||
/// Also removes trailing dots and spaces which Windows doesn't allow at end of filename
|
||||
///
|
||||
/// IMPORTANT: This should ONLY be called in ExtractPatterns to avoid performance waste.
|
||||
/// Do NOT call this function when reading raw metadata values.
|
||||
/// </summary>
|
||||
/// <param name="str">String to sanitize</param>
|
||||
/// <returns>Sanitized string safe for use in filename</returns>
|
||||
static std::wstring SanitizeForFileName(const std::wstring& str);
|
||||
};
|
||||
}
|
||||
@@ -1,353 +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.
|
||||
|
||||
#include "pch.h"
|
||||
#include "MetadataPatternExtractor.h"
|
||||
#include "MetadataFormatHelper.h"
|
||||
#include "WICMetadataExtractor.h"
|
||||
#include <algorithm>
|
||||
#include <format>
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
#include <cmath>
|
||||
#include <utility>
|
||||
|
||||
using namespace PowerRenameLib;
|
||||
|
||||
MetadataPatternExtractor::MetadataPatternExtractor()
|
||||
: extractor(std::make_unique<WICMetadataExtractor>())
|
||||
{
|
||||
}
|
||||
|
||||
MetadataPatternExtractor::~MetadataPatternExtractor() = default;
|
||||
|
||||
MetadataPatternMap MetadataPatternExtractor::ExtractPatterns(
|
||||
const std::wstring& filePath,
|
||||
MetadataType type)
|
||||
{
|
||||
MetadataPatternMap patterns;
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case MetadataType::EXIF:
|
||||
patterns = ExtractEXIFPatterns(filePath);
|
||||
break;
|
||||
case MetadataType::XMP:
|
||||
patterns = ExtractXMPPatterns(filePath);
|
||||
break;
|
||||
default:
|
||||
return MetadataPatternMap();
|
||||
}
|
||||
|
||||
// Sanitize all pattern values for filename safety before returning
|
||||
// This ensures all metadata values are safe to use in filenames (removes illegal chars like <>:"/\|?*)
|
||||
// IMPORTANT: Only call SanitizeForFileName here to avoid performance waste
|
||||
for (auto& [key, value] : patterns)
|
||||
{
|
||||
value = MetadataFormatHelper::SanitizeForFileName(value);
|
||||
}
|
||||
|
||||
return patterns;
|
||||
}
|
||||
|
||||
void MetadataPatternExtractor::ClearCache()
|
||||
{
|
||||
if (extractor)
|
||||
{
|
||||
extractor->ClearCache();
|
||||
}
|
||||
}
|
||||
|
||||
MetadataPatternMap MetadataPatternExtractor::ExtractEXIFPatterns(const std::wstring& filePath)
|
||||
{
|
||||
MetadataPatternMap patterns;
|
||||
|
||||
EXIFMetadata exif;
|
||||
if (!extractor->ExtractEXIFMetadata(filePath, exif))
|
||||
{
|
||||
return patterns;
|
||||
}
|
||||
|
||||
if (exif.cameraMake.has_value())
|
||||
{
|
||||
patterns[MetadataPatterns::CAMERA_MAKE] = exif.cameraMake.value();
|
||||
}
|
||||
|
||||
if (exif.cameraModel.has_value())
|
||||
{
|
||||
patterns[MetadataPatterns::CAMERA_MODEL] = exif.cameraModel.value();
|
||||
}
|
||||
|
||||
if (exif.lensModel.has_value())
|
||||
{
|
||||
patterns[MetadataPatterns::LENS] = exif.lensModel.value();
|
||||
}
|
||||
|
||||
if (exif.iso.has_value())
|
||||
{
|
||||
patterns[MetadataPatterns::ISO] = MetadataFormatHelper::FormatISO(exif.iso.value());
|
||||
}
|
||||
|
||||
if (exif.aperture.has_value())
|
||||
{
|
||||
patterns[MetadataPatterns::APERTURE] = MetadataFormatHelper::FormatAperture(exif.aperture.value());
|
||||
}
|
||||
|
||||
if (exif.shutterSpeed.has_value())
|
||||
{
|
||||
patterns[MetadataPatterns::SHUTTER] = MetadataFormatHelper::FormatShutterSpeed(exif.shutterSpeed.value());
|
||||
}
|
||||
|
||||
if (exif.focalLength.has_value())
|
||||
{
|
||||
patterns[MetadataPatterns::FOCAL] = std::to_wstring(static_cast<int>(exif.focalLength.value())) + L"mm";
|
||||
}
|
||||
|
||||
if (exif.flash.has_value())
|
||||
{
|
||||
patterns[MetadataPatterns::FLASH] = MetadataFormatHelper::FormatFlash(exif.flash.value());
|
||||
}
|
||||
|
||||
if (exif.width.has_value())
|
||||
{
|
||||
patterns[MetadataPatterns::WIDTH] = std::to_wstring(exif.width.value());
|
||||
}
|
||||
|
||||
if (exif.height.has_value())
|
||||
{
|
||||
patterns[MetadataPatterns::HEIGHT] = std::to_wstring(exif.height.value());
|
||||
}
|
||||
|
||||
if (exif.author.has_value())
|
||||
{
|
||||
patterns[MetadataPatterns::AUTHOR] = exif.author.value();
|
||||
}
|
||||
|
||||
if (exif.copyright.has_value())
|
||||
{
|
||||
patterns[MetadataPatterns::COPYRIGHT] = exif.copyright.value();
|
||||
}
|
||||
|
||||
if (exif.latitude.has_value())
|
||||
{
|
||||
patterns[MetadataPatterns::LATITUDE] = MetadataFormatHelper::FormatCoordinate(exif.latitude.value(), true);
|
||||
}
|
||||
|
||||
if (exif.longitude.has_value())
|
||||
{
|
||||
patterns[MetadataPatterns::LONGITUDE] = MetadataFormatHelper::FormatCoordinate(exif.longitude.value(), false);
|
||||
}
|
||||
|
||||
// Only extract DATE_TAKEN patterns (most commonly used)
|
||||
if (exif.dateTaken.has_value())
|
||||
{
|
||||
const SYSTEMTIME& date = exif.dateTaken.value();
|
||||
patterns[MetadataPatterns::DATE_TAKEN_YYYY] = std::format(L"{:04d}", date.wYear);
|
||||
patterns[MetadataPatterns::DATE_TAKEN_YY] = std::format(L"{:02d}", date.wYear % 100);
|
||||
patterns[MetadataPatterns::DATE_TAKEN_MM] = std::format(L"{:02d}", date.wMonth);
|
||||
patterns[MetadataPatterns::DATE_TAKEN_DD] = std::format(L"{:02d}", date.wDay);
|
||||
patterns[MetadataPatterns::DATE_TAKEN_HH] = std::format(L"{:02d}", date.wHour);
|
||||
patterns[MetadataPatterns::DATE_TAKEN_mm] = std::format(L"{:02d}", date.wMinute);
|
||||
patterns[MetadataPatterns::DATE_TAKEN_SS] = std::format(L"{:02d}", date.wSecond);
|
||||
}
|
||||
// Note: dateDigitized and dateModified are still extracted but not exposed as patterns
|
||||
|
||||
if (exif.exposureBias.has_value())
|
||||
{
|
||||
patterns[MetadataPatterns::EXPOSURE_BIAS] = std::format(L"{:.2f}", exif.exposureBias.value());
|
||||
}
|
||||
|
||||
if (exif.orientation.has_value())
|
||||
{
|
||||
patterns[MetadataPatterns::ORIENTATION] = std::to_wstring(exif.orientation.value());
|
||||
}
|
||||
|
||||
if (exif.colorSpace.has_value())
|
||||
{
|
||||
patterns[MetadataPatterns::COLOR_SPACE] = std::to_wstring(exif.colorSpace.value());
|
||||
}
|
||||
|
||||
if (exif.altitude.has_value())
|
||||
{
|
||||
patterns[MetadataPatterns::ALTITUDE] = std::format(L"{:.2f} m", exif.altitude.value());
|
||||
}
|
||||
|
||||
return patterns;
|
||||
}
|
||||
|
||||
MetadataPatternMap MetadataPatternExtractor::ExtractXMPPatterns(const std::wstring& filePath)
|
||||
{
|
||||
MetadataPatternMap patterns;
|
||||
|
||||
XMPMetadata xmp;
|
||||
if (!extractor->ExtractXMPMetadata(filePath, xmp))
|
||||
{
|
||||
return patterns;
|
||||
}
|
||||
|
||||
if (xmp.creator.has_value())
|
||||
{
|
||||
const auto& creator = xmp.creator.value();
|
||||
patterns[MetadataPatterns::AUTHOR] = creator;
|
||||
patterns[MetadataPatterns::CREATOR] = creator;
|
||||
}
|
||||
|
||||
if (xmp.rights.has_value())
|
||||
{
|
||||
const auto& rights = xmp.rights.value();
|
||||
patterns[MetadataPatterns::RIGHTS] = rights;
|
||||
patterns[MetadataPatterns::COPYRIGHT] = rights;
|
||||
}
|
||||
|
||||
if (xmp.title.has_value())
|
||||
{
|
||||
patterns[MetadataPatterns::TITLE] = xmp.title.value();
|
||||
}
|
||||
|
||||
if (xmp.description.has_value())
|
||||
{
|
||||
patterns[MetadataPatterns::DESCRIPTION] = xmp.description.value();
|
||||
}
|
||||
|
||||
if (xmp.subject.has_value())
|
||||
{
|
||||
std::wstring joined;
|
||||
for (const auto& entry : xmp.subject.value())
|
||||
{
|
||||
if (!joined.empty())
|
||||
{
|
||||
joined.append(L"; ");
|
||||
}
|
||||
joined.append(entry);
|
||||
}
|
||||
if (!joined.empty())
|
||||
{
|
||||
patterns[MetadataPatterns::SUBJECT] = joined;
|
||||
}
|
||||
}
|
||||
|
||||
if (xmp.creatorTool.has_value())
|
||||
{
|
||||
patterns[MetadataPatterns::CREATOR_TOOL] = xmp.creatorTool.value();
|
||||
}
|
||||
|
||||
if (xmp.documentID.has_value())
|
||||
{
|
||||
patterns[MetadataPatterns::DOCUMENT_ID] = xmp.documentID.value();
|
||||
}
|
||||
|
||||
if (xmp.instanceID.has_value())
|
||||
{
|
||||
patterns[MetadataPatterns::INSTANCE_ID] = xmp.instanceID.value();
|
||||
}
|
||||
|
||||
if (xmp.originalDocumentID.has_value())
|
||||
{
|
||||
patterns[MetadataPatterns::ORIGINAL_DOCUMENT_ID] = xmp.originalDocumentID.value();
|
||||
}
|
||||
|
||||
if (xmp.versionID.has_value())
|
||||
{
|
||||
patterns[MetadataPatterns::VERSION_ID] = xmp.versionID.value();
|
||||
}
|
||||
|
||||
// Only extract CREATE_DATE patterns (primary creation time)
|
||||
if (xmp.createDate.has_value())
|
||||
{
|
||||
const SYSTEMTIME& date = xmp.createDate.value();
|
||||
patterns[MetadataPatterns::CREATE_DATE_YYYY] = std::format(L"{:04d}", date.wYear);
|
||||
patterns[MetadataPatterns::CREATE_DATE_YY] = std::format(L"{:02d}", date.wYear % 100);
|
||||
patterns[MetadataPatterns::CREATE_DATE_MM] = std::format(L"{:02d}", date.wMonth);
|
||||
patterns[MetadataPatterns::CREATE_DATE_DD] = std::format(L"{:02d}", date.wDay);
|
||||
patterns[MetadataPatterns::CREATE_DATE_HH] = std::format(L"{:02d}", date.wHour);
|
||||
patterns[MetadataPatterns::CREATE_DATE_mm] = std::format(L"{:02d}", date.wMinute);
|
||||
patterns[MetadataPatterns::CREATE_DATE_SS] = std::format(L"{:02d}", date.wSecond);
|
||||
}
|
||||
// Note: modifyDate and metadataDate are still extracted but not exposed as patterns
|
||||
|
||||
return patterns;
|
||||
}
|
||||
|
||||
// AddDatePatterns function has been removed as dynamic patterns are no longer supported.
|
||||
// Date patterns are now directly added inline for DATE_TAKEN and CREATE_DATE only.
|
||||
// Formatting functions have been moved to MetadataFormatHelper for better testability.
|
||||
|
||||
std::vector<std::wstring> MetadataPatternExtractor::GetSupportedPatterns(MetadataType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case MetadataType::EXIF:
|
||||
return {
|
||||
MetadataPatterns::CAMERA_MAKE,
|
||||
MetadataPatterns::CAMERA_MODEL,
|
||||
MetadataPatterns::LENS,
|
||||
MetadataPatterns::ISO,
|
||||
MetadataPatterns::APERTURE,
|
||||
MetadataPatterns::SHUTTER,
|
||||
MetadataPatterns::FOCAL,
|
||||
MetadataPatterns::FLASH,
|
||||
MetadataPatterns::WIDTH,
|
||||
MetadataPatterns::HEIGHT,
|
||||
MetadataPatterns::AUTHOR,
|
||||
MetadataPatterns::COPYRIGHT,
|
||||
MetadataPatterns::LATITUDE,
|
||||
MetadataPatterns::LONGITUDE,
|
||||
MetadataPatterns::DATE_TAKEN_YYYY,
|
||||
MetadataPatterns::DATE_TAKEN_YY,
|
||||
MetadataPatterns::DATE_TAKEN_MM,
|
||||
MetadataPatterns::DATE_TAKEN_DD,
|
||||
MetadataPatterns::DATE_TAKEN_HH,
|
||||
MetadataPatterns::DATE_TAKEN_mm,
|
||||
MetadataPatterns::DATE_TAKEN_SS,
|
||||
MetadataPatterns::EXPOSURE_BIAS,
|
||||
MetadataPatterns::ORIENTATION,
|
||||
MetadataPatterns::COLOR_SPACE,
|
||||
MetadataPatterns::ALTITUDE
|
||||
};
|
||||
|
||||
case MetadataType::XMP:
|
||||
return {
|
||||
MetadataPatterns::AUTHOR,
|
||||
MetadataPatterns::COPYRIGHT,
|
||||
MetadataPatterns::RIGHTS,
|
||||
MetadataPatterns::TITLE,
|
||||
MetadataPatterns::DESCRIPTION,
|
||||
MetadataPatterns::SUBJECT,
|
||||
MetadataPatterns::CREATOR,
|
||||
MetadataPatterns::CREATOR_TOOL,
|
||||
MetadataPatterns::DOCUMENT_ID,
|
||||
MetadataPatterns::INSTANCE_ID,
|
||||
MetadataPatterns::ORIGINAL_DOCUMENT_ID,
|
||||
MetadataPatterns::VERSION_ID,
|
||||
MetadataPatterns::CREATE_DATE_YYYY,
|
||||
MetadataPatterns::CREATE_DATE_YY,
|
||||
MetadataPatterns::CREATE_DATE_MM,
|
||||
MetadataPatterns::CREATE_DATE_DD,
|
||||
MetadataPatterns::CREATE_DATE_HH,
|
||||
MetadataPatterns::CREATE_DATE_mm,
|
||||
MetadataPatterns::CREATE_DATE_SS
|
||||
};
|
||||
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::wstring> MetadataPatternExtractor::GetAllPossiblePatterns()
|
||||
{
|
||||
auto exifPatterns = GetSupportedPatterns(MetadataType::EXIF);
|
||||
auto xmpPatterns = GetSupportedPatterns(MetadataType::XMP);
|
||||
|
||||
std::vector<std::wstring> allPatterns;
|
||||
allPatterns.reserve(exifPatterns.size() + xmpPatterns.size());
|
||||
|
||||
allPatterns.insert(allPatterns.end(), exifPatterns.begin(), exifPatterns.end());
|
||||
allPatterns.insert(allPatterns.end(), xmpPatterns.begin(), xmpPatterns.end());
|
||||
|
||||
std::sort(allPatterns.begin(), allPatterns.end());
|
||||
allPatterns.erase(std::unique(allPatterns.begin(), allPatterns.end()), allPatterns.end());
|
||||
|
||||
return allPatterns;
|
||||
}
|
||||
|
||||
@@ -1,39 +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.
|
||||
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include "MetadataTypes.h"
|
||||
|
||||
namespace PowerRenameLib
|
||||
{
|
||||
// Pattern-Value mapping for metadata replacement
|
||||
using MetadataPatternMap = std::unordered_map<std::wstring, std::wstring>;
|
||||
|
||||
/// <summary>
|
||||
/// Metadata pattern extractor that converts metadata into replaceable patterns
|
||||
/// </summary>
|
||||
class MetadataPatternExtractor
|
||||
{
|
||||
public:
|
||||
MetadataPatternExtractor();
|
||||
~MetadataPatternExtractor();
|
||||
|
||||
MetadataPatternMap ExtractPatterns(const std::wstring& filePath, MetadataType type);
|
||||
|
||||
void ClearCache();
|
||||
|
||||
static std::vector<std::wstring> GetSupportedPatterns(MetadataType type);
|
||||
static std::vector<std::wstring> GetAllPossiblePatterns();
|
||||
|
||||
private:
|
||||
std::unique_ptr<class WICMetadataExtractor> extractor;
|
||||
|
||||
MetadataPatternMap ExtractEXIFPatterns(const std::wstring& filePath);
|
||||
MetadataPatternMap ExtractXMPPatterns(const std::wstring& filePath);
|
||||
};
|
||||
}
|
||||
@@ -1,87 +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.
|
||||
|
||||
#include "pch.h"
|
||||
#include "MetadataResultCache.h"
|
||||
|
||||
using namespace PowerRenameLib;
|
||||
|
||||
namespace
|
||||
{
|
||||
template <typename Metadata, typename CacheEntry, typename Cache, typename Mutex, typename Loader>
|
||||
bool GetOrLoadInternal(const std::wstring& filePath,
|
||||
Metadata& outMetadata,
|
||||
Cache& cache,
|
||||
Mutex& mutex,
|
||||
const Loader& loader)
|
||||
{
|
||||
{
|
||||
std::shared_lock sharedLock(mutex);
|
||||
auto it = cache.find(filePath);
|
||||
if (it != cache.end())
|
||||
{
|
||||
// Return cached result (success or failure)
|
||||
outMetadata = it->second.data;
|
||||
return it->second.wasSuccessful;
|
||||
}
|
||||
}
|
||||
|
||||
if (!loader)
|
||||
{
|
||||
// No loader provided
|
||||
return false;
|
||||
}
|
||||
|
||||
Metadata loaded{};
|
||||
const bool result = loader(loaded);
|
||||
|
||||
// Cache the result (success or failure)
|
||||
{
|
||||
std::unique_lock uniqueLock(mutex);
|
||||
// Check if another thread cached it while we were loading
|
||||
auto it = cache.find(filePath);
|
||||
if (it == cache.end())
|
||||
{
|
||||
// Not cached yet, insert our result
|
||||
cache.emplace(filePath, CacheEntry{ result, loaded });
|
||||
}
|
||||
else
|
||||
{
|
||||
// Another thread cached it, use their result
|
||||
outMetadata = it->second.data;
|
||||
return it->second.wasSuccessful;
|
||||
}
|
||||
}
|
||||
|
||||
outMetadata = loaded;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
bool MetadataResultCache::GetOrLoadEXIF(const std::wstring& filePath,
|
||||
EXIFMetadata& outMetadata,
|
||||
const EXIFLoader& loader)
|
||||
{
|
||||
return GetOrLoadInternal<EXIFMetadata, CacheEntry<EXIFMetadata>>(filePath, outMetadata, exifCache, exifMutex, loader);
|
||||
}
|
||||
|
||||
bool MetadataResultCache::GetOrLoadXMP(const std::wstring& filePath,
|
||||
XMPMetadata& outMetadata,
|
||||
const XMPLoader& loader)
|
||||
{
|
||||
return GetOrLoadInternal<XMPMetadata, CacheEntry<XMPMetadata>>(filePath, outMetadata, xmpCache, xmpMutex, loader);
|
||||
}
|
||||
|
||||
void MetadataResultCache::ClearAll()
|
||||
{
|
||||
{
|
||||
std::unique_lock lock(exifMutex);
|
||||
exifCache.clear();
|
||||
}
|
||||
|
||||
{
|
||||
std::unique_lock lock(xmpMutex);
|
||||
xmpCache.clear();
|
||||
}
|
||||
}
|
||||
@@ -1,39 +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.
|
||||
|
||||
#pragma once
|
||||
#include "MetadataTypes.h"
|
||||
#include <shared_mutex>
|
||||
#include <unordered_map>
|
||||
#include <string>
|
||||
#include <functional>
|
||||
|
||||
namespace PowerRenameLib
|
||||
{
|
||||
class MetadataResultCache
|
||||
{
|
||||
public:
|
||||
using EXIFLoader = std::function<bool(EXIFMetadata&)>;
|
||||
using XMPLoader = std::function<bool(XMPMetadata&)>;
|
||||
|
||||
bool GetOrLoadEXIF(const std::wstring& filePath, EXIFMetadata& outMetadata, const EXIFLoader& loader);
|
||||
bool GetOrLoadXMP(const std::wstring& filePath, XMPMetadata& outMetadata, const XMPLoader& loader);
|
||||
|
||||
void ClearAll();
|
||||
|
||||
private:
|
||||
// Wrapper to cache both success and failure states
|
||||
template<typename T>
|
||||
struct CacheEntry
|
||||
{
|
||||
bool wasSuccessful;
|
||||
T data;
|
||||
};
|
||||
|
||||
mutable std::shared_mutex exifMutex;
|
||||
mutable std::shared_mutex xmpMutex;
|
||||
std::unordered_map<std::wstring, CacheEntry<EXIFMetadata>> exifCache;
|
||||
std::unordered_map<std::wstring, CacheEntry<XMPMetadata>> xmpCache;
|
||||
};
|
||||
}
|
||||
@@ -1,156 +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.
|
||||
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
#include <windows.h>
|
||||
|
||||
namespace PowerRenameLib
|
||||
{
|
||||
/// <summary>
|
||||
/// Supported metadata format types
|
||||
/// </summary>
|
||||
enum class MetadataType
|
||||
{
|
||||
EXIF, // EXIF metadata (camera settings, date taken, etc.)
|
||||
XMP // XMP metadata (Dublin Core, Photoshop, etc.)
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Complete EXIF metadata structure
|
||||
/// Contains all commonly used EXIF fields with optional values
|
||||
/// </summary>
|
||||
struct EXIFMetadata
|
||||
{
|
||||
// Date and time information
|
||||
std::optional<SYSTEMTIME> dateTaken; // DateTimeOriginal
|
||||
std::optional<SYSTEMTIME> dateDigitized; // DateTimeDigitized
|
||||
std::optional<SYSTEMTIME> dateModified; // DateTime
|
||||
|
||||
// Camera information
|
||||
std::optional<std::wstring> cameraMake; // Make
|
||||
std::optional<std::wstring> cameraModel; // Model
|
||||
std::optional<std::wstring> lensModel; // LensModel
|
||||
|
||||
// Shooting parameters
|
||||
std::optional<int64_t> iso; // ISO speed
|
||||
std::optional<double> aperture; // F-number
|
||||
std::optional<double> shutterSpeed; // Exposure time
|
||||
std::optional<double> focalLength; // Focal length in mm
|
||||
std::optional<double> exposureBias; // Exposure bias value
|
||||
std::optional<int64_t> flash; // Flash status
|
||||
|
||||
// Image properties
|
||||
std::optional<int64_t> width; // Image width in pixels
|
||||
std::optional<int64_t> height; // Image height in pixels
|
||||
std::optional<int64_t> orientation; // Image orientation
|
||||
std::optional<int64_t> colorSpace; // Color space
|
||||
|
||||
// Author and copyright
|
||||
std::optional<std::wstring> author; // Artist
|
||||
std::optional<std::wstring> copyright; // Copyright notice
|
||||
|
||||
// GPS information
|
||||
std::optional<double> latitude; // GPS latitude in decimal degrees
|
||||
std::optional<double> longitude; // GPS longitude in decimal degrees
|
||||
std::optional<double> altitude; // GPS altitude in meters
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// XMP (Extensible Metadata Platform) metadata structure
|
||||
/// Contains XMP Basic, Dublin Core, Rights and Media Management schema fields
|
||||
/// </summary>
|
||||
struct XMPMetadata
|
||||
{
|
||||
// XMP Basic schema - https://ns.adobe.com/xap/1.0/
|
||||
std::optional<SYSTEMTIME> createDate; // xmp:CreateDate
|
||||
std::optional<SYSTEMTIME> modifyDate; // xmp:ModifyDate
|
||||
std::optional<SYSTEMTIME> metadataDate; // xmp:MetadataDate
|
||||
std::optional<std::wstring> creatorTool; // xmp:CreatorTool
|
||||
|
||||
// Dublin Core schema - http://purl.org/dc/elements/1.1/
|
||||
std::optional<std::wstring> title; // dc:title
|
||||
std::optional<std::wstring> description; // dc:description
|
||||
std::optional<std::wstring> creator; // dc:creator (author)
|
||||
std::optional<std::vector<std::wstring>> subject; // dc:subject (keywords)
|
||||
|
||||
// XMP Rights Management schema - http://ns.adobe.com/xap/1.0/rights/
|
||||
std::optional<std::wstring> rights; // xmpRights:WebStatement (copyright)
|
||||
|
||||
// XMP Media Management schema - http://ns.adobe.com/xap/1.0/mm/
|
||||
std::optional<std::wstring> documentID; // xmpMM:DocumentID
|
||||
std::optional<std::wstring> instanceID; // xmpMM:InstanceID
|
||||
std::optional<std::wstring> originalDocumentID; // xmpMM:OriginalDocumentID
|
||||
std::optional<std::wstring> versionID; // xmpMM:VersionID
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Constants for metadata pattern names
|
||||
/// </summary>
|
||||
namespace MetadataPatterns
|
||||
{
|
||||
// EXIF patterns
|
||||
constexpr wchar_t CAMERA_MAKE[] = L"CAMERA_MAKE";
|
||||
constexpr wchar_t CAMERA_MODEL[] = L"CAMERA_MODEL";
|
||||
constexpr wchar_t LENS[] = L"LENS";
|
||||
constexpr wchar_t ISO[] = L"ISO";
|
||||
constexpr wchar_t APERTURE[] = L"APERTURE";
|
||||
constexpr wchar_t SHUTTER[] = L"SHUTTER";
|
||||
constexpr wchar_t FOCAL[] = L"FOCAL";
|
||||
constexpr wchar_t FLASH[] = L"FLASH";
|
||||
constexpr wchar_t WIDTH[] = L"WIDTH";
|
||||
constexpr wchar_t HEIGHT[] = L"HEIGHT";
|
||||
constexpr wchar_t AUTHOR[] = L"AUTHOR";
|
||||
constexpr wchar_t COPYRIGHT[] = L"COPYRIGHT";
|
||||
constexpr wchar_t LATITUDE[] = L"LATITUDE";
|
||||
constexpr wchar_t LONGITUDE[] = L"LONGITUDE";
|
||||
|
||||
// Date components from EXIF DateTimeOriginal (when photo was taken)
|
||||
constexpr wchar_t DATE_TAKEN_YYYY[] = L"DATE_TAKEN_YYYY";
|
||||
constexpr wchar_t DATE_TAKEN_YY[] = L"DATE_TAKEN_YY";
|
||||
constexpr wchar_t DATE_TAKEN_MM[] = L"DATE_TAKEN_MM";
|
||||
constexpr wchar_t DATE_TAKEN_DD[] = L"DATE_TAKEN_DD";
|
||||
constexpr wchar_t DATE_TAKEN_HH[] = L"DATE_TAKEN_HH";
|
||||
constexpr wchar_t DATE_TAKEN_mm[] = L"DATE_TAKEN_mm";
|
||||
constexpr wchar_t DATE_TAKEN_SS[] = L"DATE_TAKEN_SS";
|
||||
|
||||
// Additional EXIF patterns
|
||||
constexpr wchar_t EXPOSURE_BIAS[] = L"EXPOSURE_BIAS";
|
||||
constexpr wchar_t ORIENTATION[] = L"ORIENTATION";
|
||||
constexpr wchar_t COLOR_SPACE[] = L"COLOR_SPACE";
|
||||
constexpr wchar_t ALTITUDE[] = L"ALTITUDE";
|
||||
|
||||
// XMP patterns
|
||||
constexpr wchar_t CREATOR_TOOL[] = L"CREATOR_TOOL";
|
||||
|
||||
// Date components from XMP CreateDate
|
||||
constexpr wchar_t CREATE_DATE_YYYY[] = L"CREATE_DATE_YYYY";
|
||||
constexpr wchar_t CREATE_DATE_YY[] = L"CREATE_DATE_YY";
|
||||
constexpr wchar_t CREATE_DATE_MM[] = L"CREATE_DATE_MM";
|
||||
constexpr wchar_t CREATE_DATE_DD[] = L"CREATE_DATE_DD";
|
||||
constexpr wchar_t CREATE_DATE_HH[] = L"CREATE_DATE_HH";
|
||||
constexpr wchar_t CREATE_DATE_mm[] = L"CREATE_DATE_mm";
|
||||
constexpr wchar_t CREATE_DATE_SS[] = L"CREATE_DATE_SS";
|
||||
|
||||
// Dublin Core patterns
|
||||
constexpr wchar_t TITLE[] = L"TITLE";
|
||||
constexpr wchar_t DESCRIPTION[] = L"DESCRIPTION";
|
||||
constexpr wchar_t CREATOR[] = L"CREATOR";
|
||||
constexpr wchar_t SUBJECT[] = L"SUBJECT"; // Keywords
|
||||
|
||||
// XMP Rights pattern
|
||||
constexpr wchar_t RIGHTS[] = L"RIGHTS"; // Copyright
|
||||
|
||||
// XMP Media Management patterns
|
||||
constexpr wchar_t DOCUMENT_ID[] = L"DOCUMENT_ID";
|
||||
constexpr wchar_t INSTANCE_ID[] = L"INSTANCE_ID";
|
||||
constexpr wchar_t ORIGINAL_DOCUMENT_ID[] = L"ORIGINAL_DOCUMENT_ID";
|
||||
constexpr wchar_t VERSION_ID[] = L"VERSION_ID";
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,7 @@
|
||||
#pragma once
|
||||
#include "pch.h"
|
||||
#include "MetadataTypes.h"
|
||||
#include "MetadataPatternExtractor.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
|
||||
enum PowerRenameFlags
|
||||
{
|
||||
@@ -25,9 +22,6 @@ enum PowerRenameFlags
|
||||
CreationTime = 0x4000,
|
||||
ModificationTime = 0x8000,
|
||||
AccessTime = 0x10000,
|
||||
// Metadata source flags
|
||||
MetadataSourceEXIF = 0x20000, // Default
|
||||
MetadataSourceXMP = 0x40000,
|
||||
};
|
||||
|
||||
enum PowerRenameFilters
|
||||
@@ -53,7 +47,6 @@ public:
|
||||
IFACEMETHOD(OnReplaceTermChanged)(_In_ PCWSTR replaceTerm) = 0;
|
||||
IFACEMETHOD(OnFlagsChanged)(_In_ DWORD flags) = 0;
|
||||
IFACEMETHOD(OnFileTimeChanged)(_In_ SYSTEMTIME fileTime) = 0;
|
||||
IFACEMETHOD(OnMetadataChanged)() = 0;
|
||||
};
|
||||
|
||||
interface __declspec(uuid("E3ED45B5-9CE0-47E2-A595-67EB950B9B72")) IPowerRenameRegEx : public IUnknown
|
||||
@@ -69,9 +62,6 @@ public:
|
||||
IFACEMETHOD(PutFlags)(_In_ DWORD flags) = 0;
|
||||
IFACEMETHOD(PutFileTime)(_In_ SYSTEMTIME fileTime) = 0;
|
||||
IFACEMETHOD(ResetFileTime)() = 0;
|
||||
IFACEMETHOD(PutMetadataPatterns)(_In_ const PowerRenameLib::MetadataPatternMap& patterns) = 0;
|
||||
IFACEMETHOD(ResetMetadata)() = 0;
|
||||
IFACEMETHOD(GetMetadataType)(_Out_ PowerRenameLib::MetadataType* metadataType) = 0;
|
||||
IFACEMETHOD(Replace)(_In_ PCWSTR source, _Outptr_ PWSTR* result, unsigned long& enumIndex) = 0;
|
||||
};
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ IFACEMETHODIMP CPowerRenameItem::GetTime(_In_ DWORD flags, _Outptr_ SYSTEMTIME*
|
||||
else
|
||||
{
|
||||
// Default to modification time if no specific flag is set
|
||||
parsedTimeType = PowerRenameFlags::CreationTime;
|
||||
parsedTimeType = PowerRenameFlags::CreationTime;
|
||||
}
|
||||
|
||||
if (m_isTimeParsed && parsedTimeType == m_parsedTimeType)
|
||||
@@ -86,13 +86,6 @@ IFACEMETHODIMP CPowerRenameItem::GetTime(_In_ DWORD flags, _Outptr_ SYSTEMTIME*
|
||||
HANDLE hFile = CreateFileW(m_path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
|
||||
if (hFile != INVALID_HANDLE_VALUE)
|
||||
{
|
||||
// Use RAII-style scope guard to ensure handle is always closed
|
||||
struct FileHandleCloser
|
||||
{
|
||||
HANDLE handle;
|
||||
~FileHandleCloser() { if (handle != INVALID_HANDLE_VALUE) CloseHandle(handle); }
|
||||
} scopedHandle{ hFile };
|
||||
|
||||
FILETIME FileTime;
|
||||
bool success = false;
|
||||
|
||||
@@ -129,6 +122,8 @@ IFACEMETHODIMP CPowerRenameItem::GetTime(_In_ DWORD flags, _Outptr_ SYSTEMTIME*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CloseHandle(hFile);
|
||||
}
|
||||
*time = m_time;
|
||||
return hr;
|
||||
|
||||
@@ -16,24 +16,19 @@
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="Shared">
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<PropertyGroup>
|
||||
<OutDir>..\..\..\..\$(Platform)\$(Configuration)\WinUI3Apps\</OutDir>
|
||||
<DepsPath>$(ProjectDir)..\..\..\..\deps</DepsPath>
|
||||
</PropertyGroup>
|
||||
<ImportGroup Label="PropertySheets">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<PropertyGroup>
|
||||
<OutDir>..\..\..\..\$(Platform)\$(Configuration)\WinUI3Apps\</OutDir>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<PreprocessorDefinitions>WIN32;_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<AdditionalIncludeDirectories>$(ProjectDir)..\;$(ProjectDir)..\ui;$(ProjectDir)..\dll;$(ProjectDir)..\lib;$(ProjectDir)..\..\..\;$(ProjectDir)..\..\..\common\Telemetry;%(AdditionalIncludeDirectories);$(GeneratedFilesDir)</AdditionalIncludeDirectories>
|
||||
<AdditionalOptions>/FS %(AdditionalOptions)</AdditionalOptions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<AdditionalDependencies>windowscodecs.lib;propsys.lib;ole32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="Enumerating.h" />
|
||||
@@ -52,12 +47,6 @@
|
||||
<ClInclude Include="pch.h" />
|
||||
<ClInclude Include="targetver.h" />
|
||||
<ClInclude Include="trace.h" />
|
||||
<ClInclude Include="MetadataTypes.h" />
|
||||
<ClInclude Include="PropVariantValue.h" />
|
||||
<ClInclude Include="WICMetadataExtractor.h" />
|
||||
<ClInclude Include="MetadataPatternExtractor.h" />
|
||||
<ClInclude Include="MetadataFormatHelper.h" />
|
||||
<ClInclude Include="MetadataResultCache.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="Enumerating.cpp" />
|
||||
@@ -75,10 +64,6 @@
|
||||
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="trace.cpp" />
|
||||
<ClCompile Include="WICMetadataExtractor.cpp" />
|
||||
<ClCompile Include="MetadataPatternExtractor.cpp" />
|
||||
<ClCompile Include="MetadataFormatHelper.cpp" />
|
||||
<ClCompile Include="MetadataResultCache.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
|
||||
@@ -462,12 +462,6 @@ IFACEMETHODIMP CPowerRenameManager::OnFileTimeChanged(_In_ SYSTEMTIME /*fileTime
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
IFACEMETHODIMP CPowerRenameManager::OnMetadataChanged()
|
||||
{
|
||||
_PerformRegExRename();
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT CPowerRenameManager::s_CreateInstance(_Outptr_ IPowerRenameManager** ppsrm)
|
||||
{
|
||||
*ppsrm = nullptr;
|
||||
|
||||
@@ -50,7 +50,6 @@ public:
|
||||
IFACEMETHODIMP OnReplaceTermChanged(_In_ PCWSTR replaceTerm);
|
||||
IFACEMETHODIMP OnFlagsChanged(_In_ DWORD flags);
|
||||
IFACEMETHODIMP OnFileTimeChanged(_In_ SYSTEMTIME fileTime);
|
||||
IFACEMETHODIMP OnMetadataChanged();
|
||||
|
||||
static HRESULT s_CreateInstance(_Outptr_ IPowerRenameManager** ppsrm);
|
||||
|
||||
|
||||
@@ -328,22 +328,6 @@ IFACEMETHODIMP CPowerRenameRegEx::ResetFileTime()
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
IFACEMETHODIMP CPowerRenameRegEx::PutMetadataPatterns(_In_ const PowerRenameLib::MetadataPatternMap& patterns)
|
||||
{
|
||||
m_metadataPatterns = patterns;
|
||||
m_useMetadata = true;
|
||||
_OnMetadataChanged();
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
IFACEMETHODIMP CPowerRenameRegEx::ResetMetadata()
|
||||
{
|
||||
m_metadataPatterns.clear();
|
||||
m_useMetadata = false;
|
||||
_OnMetadataChanged();
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT CPowerRenameRegEx::s_CreateInstance(_Outptr_ IPowerRenameRegEx** renameRegEx)
|
||||
{
|
||||
*renameRegEx = nullptr;
|
||||
@@ -403,39 +387,10 @@ HRESULT CPowerRenameRegEx::Replace(_In_ PCWSTR source, _Outptr_ PWSTR* result, u
|
||||
// TODO: creating the regex could be costly. May want to cache this.
|
||||
wchar_t newReplaceTerm[MAX_PATH] = { 0 };
|
||||
bool fileTimeErrorOccurred = false;
|
||||
bool metadataErrorOccurred = false;
|
||||
bool appliedTemplateTransform = false;
|
||||
|
||||
std::wstring replaceTemplate;
|
||||
if (m_replaceTerm)
|
||||
{
|
||||
replaceTemplate = m_replaceTerm;
|
||||
}
|
||||
|
||||
if (m_useFileTime)
|
||||
{
|
||||
if (FAILED(GetDatedFileName(newReplaceTerm, ARRAYSIZE(newReplaceTerm), replaceTemplate.c_str(), m_fileTime)))
|
||||
{
|
||||
if (FAILED(GetDatedFileName(newReplaceTerm, ARRAYSIZE(newReplaceTerm), m_replaceTerm, m_fileTime)))
|
||||
fileTimeErrorOccurred = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
replaceTemplate.assign(newReplaceTerm);
|
||||
appliedTemplateTransform = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_useMetadata)
|
||||
{
|
||||
if (FAILED(GetMetadataFileName(newReplaceTerm, ARRAYSIZE(newReplaceTerm), replaceTemplate.c_str(), m_metadataPatterns)))
|
||||
{
|
||||
metadataErrorOccurred = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
replaceTemplate.assign(newReplaceTerm);
|
||||
appliedTemplateTransform = true;
|
||||
}
|
||||
}
|
||||
|
||||
std::wstring sourceToUse;
|
||||
@@ -444,9 +399,9 @@ HRESULT CPowerRenameRegEx::Replace(_In_ PCWSTR source, _Outptr_ PWSTR* result, u
|
||||
|
||||
std::wstring searchTerm(m_searchTerm);
|
||||
std::wstring replaceTerm;
|
||||
if (appliedTemplateTransform)
|
||||
if (m_useFileTime && !fileTimeErrorOccurred)
|
||||
{
|
||||
replaceTerm = replaceTemplate;
|
||||
replaceTerm = newReplaceTerm;
|
||||
}
|
||||
else if (m_replaceTerm)
|
||||
{
|
||||
@@ -651,43 +606,3 @@ void CPowerRenameRegEx::_OnFileTimeChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CPowerRenameRegEx::_OnMetadataChanged()
|
||||
{
|
||||
CSRWSharedAutoLock lock(&m_lockEvents);
|
||||
|
||||
for (auto it : m_renameRegExEvents)
|
||||
{
|
||||
if (it.pEvents)
|
||||
{
|
||||
it.pEvents->OnMetadataChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PowerRenameLib::MetadataType CPowerRenameRegEx::_GetMetadataTypeFromFlags() const
|
||||
{
|
||||
if (m_flags & MetadataSourceXMP)
|
||||
return PowerRenameLib::MetadataType::XMP;
|
||||
|
||||
// Default to EXIF
|
||||
return PowerRenameLib::MetadataType::EXIF;
|
||||
}
|
||||
|
||||
// Interface method implementation
|
||||
IFACEMETHODIMP CPowerRenameRegEx::GetMetadataType(_Out_ PowerRenameLib::MetadataType* metadataType)
|
||||
{
|
||||
if (metadataType == nullptr)
|
||||
return E_POINTER;
|
||||
|
||||
*metadataType = _GetMetadataTypeFromFlags();
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
// Convenience method for internal use
|
||||
PowerRenameLib::MetadataType CPowerRenameRegEx::GetMetadataType() const
|
||||
{
|
||||
return _GetMetadataTypeFromFlags();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
#include "Enumerating.h"
|
||||
|
||||
#include "Randomizer.h"
|
||||
#include "MetadataTypes.h"
|
||||
#include "MetadataPatternExtractor.h"
|
||||
|
||||
#include "PowerRenameInterfaces.h"
|
||||
|
||||
@@ -31,13 +29,7 @@ public:
|
||||
IFACEMETHODIMP PutFlags(_In_ DWORD flags);
|
||||
IFACEMETHODIMP PutFileTime(_In_ SYSTEMTIME fileTime);
|
||||
IFACEMETHODIMP ResetFileTime();
|
||||
IFACEMETHODIMP PutMetadataPatterns(_In_ const PowerRenameLib::MetadataPatternMap& patterns);
|
||||
IFACEMETHODIMP ResetMetadata();
|
||||
IFACEMETHODIMP GetMetadataType(_Out_ PowerRenameLib::MetadataType* metadataType);
|
||||
IFACEMETHODIMP Replace(_In_ PCWSTR source, _Outptr_ PWSTR* result, unsigned long& enumIndex);
|
||||
|
||||
// Get current metadata type based on flags
|
||||
PowerRenameLib::MetadataType GetMetadataType() const;
|
||||
|
||||
static HRESULT s_CreateInstance(_Outptr_ IPowerRenameRegEx** renameRegEx);
|
||||
|
||||
@@ -49,9 +41,7 @@ protected:
|
||||
void _OnReplaceTermChanged();
|
||||
void _OnFlagsChanged();
|
||||
void _OnFileTimeChanged();
|
||||
void _OnMetadataChanged();
|
||||
HRESULT _OnEnumerateOrRandomizeItemsChanged();
|
||||
PowerRenameLib::MetadataType _GetMetadataTypeFromFlags() const;
|
||||
|
||||
size_t _Find(std::wstring data, std::wstring toSearch, bool caseInsensitive, size_t pos);
|
||||
|
||||
@@ -64,9 +54,6 @@ protected:
|
||||
SYSTEMTIME m_fileTime = { 0 };
|
||||
bool m_useFileTime = false;
|
||||
|
||||
PowerRenameLib::MetadataPatternMap m_metadataPatterns;
|
||||
bool m_useMetadata = false;
|
||||
|
||||
CSRWLock m_lock;
|
||||
CSRWLock m_lockEvents;
|
||||
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <propvarutil.h>
|
||||
#include <propidl.h>
|
||||
|
||||
namespace PowerRenameLib
|
||||
{
|
||||
/// <summary>
|
||||
/// RAII wrapper around PROPVARIANT to ensure proper initialization and cleanup.
|
||||
/// Move-only semantics keep ownership simple while still allowing use in optionals.
|
||||
/// </summary>
|
||||
struct PropVariantValue
|
||||
{
|
||||
PropVariantValue() noexcept
|
||||
{
|
||||
PropVariantInit(&value);
|
||||
}
|
||||
|
||||
~PropVariantValue()
|
||||
{
|
||||
PropVariantClear(&value);
|
||||
}
|
||||
|
||||
PropVariantValue(const PropVariantValue&) = delete;
|
||||
PropVariantValue& operator=(const PropVariantValue&) = delete;
|
||||
|
||||
PropVariantValue(PropVariantValue&& other) noexcept
|
||||
{
|
||||
value = other.value;
|
||||
PropVariantInit(&other.value); // Properly clear the moved-from object
|
||||
}
|
||||
|
||||
PropVariantValue& operator=(PropVariantValue&& other) noexcept
|
||||
{
|
||||
if (this != &other)
|
||||
{
|
||||
PropVariantClear(&value);
|
||||
value = other.value;
|
||||
PropVariantInit(&other.value); // Properly clear the moved-from object
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
PROPVARIANT* GetAddressOf() noexcept
|
||||
{
|
||||
return &value;
|
||||
}
|
||||
|
||||
PROPVARIANT& Get() noexcept
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
const PROPVARIANT& Get() const noexcept
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
private:
|
||||
PROPVARIANT value;
|
||||
};
|
||||
}
|
||||
@@ -1,13 +1,9 @@
|
||||
#include "pch.h"
|
||||
#include <winrt/base.h>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
|
||||
#include "Renaming.h"
|
||||
#include <Helpers.h>
|
||||
#include "MetadataPatternExtractor.h"
|
||||
#include "PowerRenameRegEx.h"
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
bool DoRename(CComPtr<IPowerRenameRegEx>& spRenameRegEx, unsigned long& itemEnumIndex, CComPtr<IPowerRenameItem>& spItem)
|
||||
@@ -18,7 +14,6 @@ bool DoRename(CComPtr<IPowerRenameRegEx>& spRenameRegEx, unsigned long& itemEnum
|
||||
|
||||
PWSTR replaceTerm = nullptr;
|
||||
bool useFileTime = false;
|
||||
bool useMetadata = false;
|
||||
|
||||
winrt::check_hresult(spRenameRegEx->GetReplaceTerm(&replaceTerm));
|
||||
|
||||
@@ -26,6 +21,7 @@ bool DoRename(CComPtr<IPowerRenameRegEx>& spRenameRegEx, unsigned long& itemEnum
|
||||
{
|
||||
useFileTime = true;
|
||||
}
|
||||
CoTaskMemFree(replaceTerm);
|
||||
|
||||
int id = -1;
|
||||
winrt::check_hresult(spItem->GetId(&id));
|
||||
@@ -34,29 +30,6 @@ bool DoRename(CComPtr<IPowerRenameRegEx>& spRenameRegEx, unsigned long& itemEnum
|
||||
bool isSubFolderContent = false;
|
||||
winrt::check_hresult(spItem->GetIsFolder(&isFolder));
|
||||
winrt::check_hresult(spItem->GetIsSubFolderContent(&isSubFolderContent));
|
||||
|
||||
// Get metadata type to check if metadata patterns are used
|
||||
PowerRenameLib::MetadataType metadataType;
|
||||
HRESULT hr = spRenameRegEx->GetMetadataType(&metadataType);
|
||||
if (FAILED(hr))
|
||||
{
|
||||
// Fallback to default metadata type if call fails
|
||||
metadataType = PowerRenameLib::MetadataType::EXIF;
|
||||
}
|
||||
|
||||
// Check if metadata is used AND if this file type supports metadata
|
||||
// Get file path early for metadata type checking and reuse later
|
||||
PWSTR filePath = nullptr;
|
||||
winrt::check_hresult(spItem->GetPath(&filePath));
|
||||
std::wstring filePathStr(filePath); // Copy once for reuse
|
||||
CoTaskMemFree(filePath); // Free immediately after copying
|
||||
|
||||
if (isMetadataUsed(replaceTerm, metadataType, filePathStr.c_str(), isFolder))
|
||||
{
|
||||
useMetadata = true;
|
||||
}
|
||||
|
||||
CoTaskMemFree(replaceTerm);
|
||||
if ((isFolder && (flags & PowerRenameFlags::ExcludeFolders)) ||
|
||||
(!isFolder && (flags & PowerRenameFlags::ExcludeFiles)) ||
|
||||
(isSubFolderContent && (flags & PowerRenameFlags::ExcludeSubfolders)) ||
|
||||
@@ -109,53 +82,6 @@ bool DoRename(CComPtr<IPowerRenameRegEx>& spRenameRegEx, unsigned long& itemEnum
|
||||
winrt::check_hresult(spRenameRegEx->PutFileTime(fileTime));
|
||||
}
|
||||
|
||||
if (useMetadata)
|
||||
{
|
||||
// Extract metadata patterns from the file
|
||||
// Note: filePathStr was already obtained and saved earlier for reuse
|
||||
|
||||
// Get metadata type using the interface method
|
||||
PowerRenameLib::MetadataType metadataType;
|
||||
HRESULT hr = spRenameRegEx->GetMetadataType(&metadataType);
|
||||
if (FAILED(hr))
|
||||
{
|
||||
// Fallback to default metadata type if call fails
|
||||
metadataType = PowerRenameLib::MetadataType::EXIF;
|
||||
}
|
||||
// Extract all patterns for the selected metadata type
|
||||
// At this point we know the file is a supported image format (jpg/jpeg/png/tif/tiff)
|
||||
static std::mutex s_metadataMutex; // Mutex to protect static variables
|
||||
static std::once_flag s_metadataExtractorInitFlag;
|
||||
static std::shared_ptr<PowerRenameLib::MetadataPatternExtractor> s_metadataExtractor;
|
||||
static std::optional<PowerRenameLib::MetadataType> s_activeMetadataType;
|
||||
|
||||
// Initialize the extractor only once
|
||||
std::call_once(s_metadataExtractorInitFlag, []() {
|
||||
s_metadataExtractor = std::make_shared<PowerRenameLib::MetadataPatternExtractor>();
|
||||
});
|
||||
|
||||
// Protect access to shared state
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(s_metadataMutex);
|
||||
|
||||
// Clear cache if metadata type has changed
|
||||
if (s_activeMetadataType.has_value() && s_activeMetadataType.value() != metadataType)
|
||||
{
|
||||
s_metadataExtractor->ClearCache();
|
||||
}
|
||||
|
||||
// Update the active metadata type
|
||||
s_activeMetadataType = metadataType;
|
||||
}
|
||||
|
||||
// Extract patterns (this can be done outside the lock if ExtractPatterns is thread-safe)
|
||||
PowerRenameLib::MetadataPatternMap patterns = s_metadataExtractor->ExtractPatterns(filePathStr, metadataType);
|
||||
|
||||
// Always call PutMetadataPatterns to ensure all patterns get replaced
|
||||
// Even if empty, this keeps metadata placeholders consistent when no values are extracted
|
||||
winrt::check_hresult(spRenameRegEx->PutMetadataPatterns(patterns));
|
||||
}
|
||||
|
||||
PWSTR newName = nullptr;
|
||||
|
||||
// Failure here means we didn't match anything or had nothing to match
|
||||
@@ -167,10 +93,6 @@ bool DoRename(CComPtr<IPowerRenameRegEx>& spRenameRegEx, unsigned long& itemEnum
|
||||
winrt::check_hresult(spRenameRegEx->ResetFileTime());
|
||||
}
|
||||
|
||||
if (useMetadata)
|
||||
{
|
||||
winrt::check_hresult(spRenameRegEx->ResetMetadata());
|
||||
}
|
||||
wchar_t resultName[MAX_PATH] = { 0 };
|
||||
|
||||
PWSTR newNameToUse = nullptr;
|
||||
@@ -284,4 +206,4 @@ bool DoRename(CComPtr<IPowerRenameRegEx>& spRenameRegEx, unsigned long& itemEnum
|
||||
CoTaskMemFree(originalName);
|
||||
|
||||
return wouldRename;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,64 +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.
|
||||
|
||||
#pragma once
|
||||
#include "MetadataTypes.h"
|
||||
#include "MetadataResultCache.h"
|
||||
#include "PropVariantValue.h"
|
||||
#include <wincodec.h>
|
||||
#include <atlbase.h>
|
||||
|
||||
namespace PowerRenameLib
|
||||
{
|
||||
/// <summary>
|
||||
/// Windows Imaging Component (WIC) implementation for metadata extraction
|
||||
/// Provides efficient batch extraction of all metadata types with built-in caching
|
||||
/// </summary>
|
||||
class WICMetadataExtractor
|
||||
{
|
||||
public:
|
||||
WICMetadataExtractor();
|
||||
~WICMetadataExtractor();
|
||||
|
||||
// Public metadata extraction methods
|
||||
bool ExtractEXIFMetadata(
|
||||
const std::wstring& filePath,
|
||||
EXIFMetadata& outMetadata);
|
||||
|
||||
bool ExtractXMPMetadata(
|
||||
const std::wstring& filePath,
|
||||
XMPMetadata& outMetadata);
|
||||
|
||||
void ClearCache();
|
||||
|
||||
private:
|
||||
// WIC factory management
|
||||
static CComPtr<IWICImagingFactory> GetWICFactory();
|
||||
static void InitializeWIC();
|
||||
|
||||
// WIC operations
|
||||
CComPtr<IWICBitmapDecoder> CreateDecoder(const std::wstring& filePath);
|
||||
CComPtr<IWICMetadataQueryReader> GetMetadataReader(IWICBitmapDecoder* decoder);
|
||||
|
||||
bool LoadEXIFMetadata(const std::wstring& filePath, EXIFMetadata& outMetadata);
|
||||
bool LoadXMPMetadata(const std::wstring& filePath, XMPMetadata& outMetadata);
|
||||
|
||||
// Batch extraction methods
|
||||
void ExtractAllEXIFFields(IWICMetadataQueryReader* reader, EXIFMetadata& metadata);
|
||||
void ExtractGPSData(IWICMetadataQueryReader* reader, EXIFMetadata& metadata);
|
||||
void ExtractAllXMPFields(IWICMetadataQueryReader* reader, XMPMetadata& metadata);
|
||||
|
||||
// Field reading helpers
|
||||
std::optional<SYSTEMTIME> ReadDateTime(IWICMetadataQueryReader* reader, const std::wstring& path);
|
||||
std::optional<std::wstring> ReadString(IWICMetadataQueryReader* reader, const std::wstring& path);
|
||||
std::optional<int64_t> ReadInteger(IWICMetadataQueryReader* reader, const std::wstring& path);
|
||||
std::optional<double> ReadDouble(IWICMetadataQueryReader* reader, const std::wstring& path);
|
||||
|
||||
// Helper methods
|
||||
std::optional<PropVariantValue> ReadMetadata(IWICMetadataQueryReader* reader, const std::wstring& path);
|
||||
|
||||
private:
|
||||
MetadataResultCache cache;
|
||||
};
|
||||
}
|
||||
@@ -28,17 +28,5 @@
|
||||
#include <charconv>
|
||||
#include <string>
|
||||
#include <random>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <fstream>
|
||||
#include <chrono>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <winrt/base.h>
|
||||
|
||||
// Windows Imaging Component (WIC) headers
|
||||
#include <wincodec.h>
|
||||
#include <wincodecsdk.h>
|
||||
#include <propkey.h>
|
||||
#include <propvarutil.h>
|
||||
|
||||
@@ -1,766 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "Helpers.h"
|
||||
#include "MetadataPatternExtractor.h"
|
||||
#include "MetadataTypes.h"
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace HelpersTests
|
||||
{
|
||||
TEST_CLASS(GetMetadataFileNameTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(BasicPatternReplacement)
|
||||
{
|
||||
// Test basic pattern replacement with available metadata
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
patterns[L"CAMERA_MAKE"] = L"Canon";
|
||||
patterns[L"ISO"] = L"ISO 400";
|
||||
patterns[L"DATE_TAKEN_YYYY"] = L"2024";
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo_$CAMERA_MAKE_$ISO", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"photo_Canon_ISO 400", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(PatternWithoutValueShowsPatternName)
|
||||
{
|
||||
// Test that patterns without values show the pattern name with $ prefix
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
patterns[L"CAMERA_MAKE"] = L"Canon";
|
||||
// ISO is not in the map
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo_$CAMERA_MAKE_$ISO", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"photo_Canon_$ISO", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(EmptyPatternShowsPatternName)
|
||||
{
|
||||
// Test that patterns with empty value show the pattern name with $ prefix
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
patterns[L"CAMERA_MAKE"] = L"Canon";
|
||||
patterns[L"ISO"] = L"";
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo_$CAMERA_MAKE_$ISO", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"photo_Canon_$ISO", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(EscapedDollarSigns)
|
||||
{
|
||||
// Test that $$ is converted to single $
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
patterns[L"ISO"] = L"ISO 400";
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo_$$_$ISO", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"photo_$_ISO 400", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(MultipleEscapedDollarSigns)
|
||||
{
|
||||
// Test that $$$$ is converted to $$
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
patterns[L"ISO"] = L"ISO 400";
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo_$$$$price", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"photo_$$price", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(OddDollarSignsWithPattern)
|
||||
{
|
||||
// Test that $$$ becomes $ followed by pattern
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
patterns[L"ISO"] = L"ISO 400";
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo_$$$ISO", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"photo_$ISO 400", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(LongestPatternMatchPriority)
|
||||
{
|
||||
// Test that longer patterns are matched first (DATE_TAKEN_YYYY vs DATE_TAKEN_YY)
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
patterns[L"DATE_TAKEN_YYYY"] = L"2024";
|
||||
patterns[L"DATE_TAKEN_YY"] = L"24";
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo_$DATE_TAKEN_YYYY", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"photo_2024", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(MultiplePatterns)
|
||||
{
|
||||
// Test multiple patterns in one string
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
patterns[L"CAMERA_MAKE"] = L"Canon";
|
||||
patterns[L"CAMERA_MODEL"] = L"EOS R5";
|
||||
patterns[L"ISO"] = L"ISO 800";
|
||||
patterns[L"DATE_TAKEN_YYYY"] = L"2024";
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH,
|
||||
L"$DATE_TAKEN_YYYY-$CAMERA_MAKE-$CAMERA_MODEL-$ISO", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"2024-Canon-EOS R5-ISO 800", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(UnrecognizedPatternIgnored)
|
||||
{
|
||||
// Test that unrecognized patterns are not replaced
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
patterns[L"CAMERA_MAKE"] = L"Canon";
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo_$CAMERA_MAKE_$INVALID_PATTERN", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"photo_Canon_$INVALID_PATTERN", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(NoPatterns)
|
||||
{
|
||||
// Test string with no patterns
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
patterns[L"CAMERA_MAKE"] = L"Canon";
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo_name_without_patterns", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"photo_name_without_patterns", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(EmptyInput)
|
||||
{
|
||||
// Test with empty input string
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"", patterns);
|
||||
|
||||
Assert::IsTrue(FAILED(hr));
|
||||
}
|
||||
|
||||
TEST_METHOD(NullInput)
|
||||
{
|
||||
// Test with null input
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, nullptr, patterns);
|
||||
|
||||
Assert::IsTrue(FAILED(hr));
|
||||
}
|
||||
|
||||
TEST_METHOD(DollarAtEnd)
|
||||
{
|
||||
// Test dollar sign at the end of string
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
patterns[L"ISO"] = L"ISO 400";
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo_$ISO$", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"photo_ISO 400$", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(ThreeDollarsAtEnd)
|
||||
{
|
||||
// Test three dollar signs at the end
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo$$$", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"photo$$$", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(ComplexMixedScenario)
|
||||
{
|
||||
// Test complex scenario with mixed patterns, escapes, and regular text
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
patterns[L"CAMERA_MAKE"] = L"Canon";
|
||||
patterns[L"ISO"] = L"ISO 400";
|
||||
patterns[L"APERTURE"] = L"f/2.8";
|
||||
patterns[L"LENS"] = L""; // Empty value
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH,
|
||||
L"$$price_$CAMERA_MAKE_$$$ISO_$APERTURE_$LENS_$$end", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"$price_Canon_$ISO 400_f/2.8_$LENS_$end", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(AllEXIFPatterns)
|
||||
{
|
||||
// Test with various EXIF patterns
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
patterns[L"WIDTH"] = L"4000";
|
||||
patterns[L"HEIGHT"] = L"3000";
|
||||
patterns[L"FOCAL"] = L"50mm";
|
||||
patterns[L"SHUTTER"] = L"1/100s";
|
||||
patterns[L"FLASH"] = L"Flash Off";
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH,
|
||||
L"photo_$WIDTH x $HEIGHT_$FOCAL_$SHUTTER_$FLASH", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"photo_4000 x 3000_50mm_1/100s_Flash Off", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(AllXMPPatterns)
|
||||
{
|
||||
// Test with various XMP patterns
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
patterns[L"TITLE"] = L"Sunset";
|
||||
patterns[L"CREATOR"] = L"John Doe";
|
||||
patterns[L"DESCRIPTION"] = L"Beautiful sunset";
|
||||
patterns[L"CREATE_DATE_YYYY"] = L"2024";
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH,
|
||||
L"$CREATE_DATE_YYYY-$TITLE-by-$CREATOR", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"2024-Sunset-by-John Doe", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(DateComponentPatterns)
|
||||
{
|
||||
// Test date component patterns
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
patterns[L"DATE_TAKEN_YYYY"] = L"2024";
|
||||
patterns[L"DATE_TAKEN_MM"] = L"03";
|
||||
patterns[L"DATE_TAKEN_DD"] = L"15";
|
||||
patterns[L"DATE_TAKEN_HH"] = L"14";
|
||||
patterns[L"DATE_TAKEN_mm"] = L"30";
|
||||
patterns[L"DATE_TAKEN_SS"] = L"45";
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH,
|
||||
L"photo_$DATE_TAKEN_YYYY-$DATE_TAKEN_MM-$DATE_TAKEN_DD_$DATE_TAKEN_HH-$DATE_TAKEN_mm-$DATE_TAKEN_SS",
|
||||
patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"photo_2024-03-15_14-30-45", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(SpecialCharactersInValues)
|
||||
{
|
||||
// Test that special characters in metadata values are preserved
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
patterns[L"TITLE"] = L"Photo (with) [brackets] & symbols!";
|
||||
patterns[L"DESCRIPTION"] = L"Test: value; with, punctuation.";
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH,
|
||||
L"$TITLE - $DESCRIPTION", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"Photo (with) [brackets] & symbols! - Test: value; with, punctuation.", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(ConsecutivePatternsWithoutSeparator)
|
||||
{
|
||||
// Test consecutive patterns without separator
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
patterns[L"CAMERA_MAKE"] = L"Canon";
|
||||
patterns[L"CAMERA_MODEL"] = L"R5";
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"$CAMERA_MAKE$CAMERA_MODEL", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"CanonR5", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(PatternAtStart)
|
||||
{
|
||||
// Test pattern at the beginning of string
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
patterns[L"CAMERA_MAKE"] = L"Canon";
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"$CAMERA_MAKE_photo", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"Canon_photo", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(PatternAtEnd)
|
||||
{
|
||||
// Test pattern at the end of string
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
patterns[L"CAMERA_MAKE"] = L"Canon";
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo_$CAMERA_MAKE", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"photo_Canon", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(OnlyPattern)
|
||||
{
|
||||
// Test string with only a pattern
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
patterns[L"CAMERA_MAKE"] = L"Canon";
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"$CAMERA_MAKE", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"Canon", result);
|
||||
}
|
||||
};
|
||||
|
||||
TEST_CLASS(PatternMatchingTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(VerifyLongestPatternMatching)
|
||||
{
|
||||
// This test verifies the greedy matching behavior
|
||||
// When we have overlapping pattern names, the longest should be matched first
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
patterns[L"DATE_TAKEN_Y"] = L"4";
|
||||
patterns[L"DATE_TAKEN_YY"] = L"24";
|
||||
patterns[L"DATE_TAKEN_YYYY"] = L"2024";
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
|
||||
// Should match YYYY (longest)
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"$DATE_TAKEN_YYYY", patterns);
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"2024", result);
|
||||
|
||||
// Should match YY (available pattern)
|
||||
hr = GetMetadataFileName(result, MAX_PATH, L"$DATE_TAKEN_YY", patterns);
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"24", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(PartialPatternNames)
|
||||
{
|
||||
// Test that partial pattern names don't match longer patterns
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
patterns[L"CAMERA_MODEL"] = L"EOS R5";
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
// CAMERA is not a valid pattern, should not match
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"$CAMERA_MODEL", patterns);
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"EOS R5", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(CaseSensitivePatterns)
|
||||
{
|
||||
// Test that pattern names are case-sensitive
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
patterns[L"CAMERA_MAKE"] = L"Canon";
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
// lowercase should not match
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"$camera_make", patterns);
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"$camera_make", result); // Not replaced
|
||||
}
|
||||
|
||||
TEST_METHOD(EmptyPatternMap)
|
||||
{
|
||||
// Test with empty pattern map
|
||||
PowerRenameLib::MetadataPatternMap patterns; // Empty
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo_$ISO_$CAMERA_MAKE", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
// Patterns should show with $ prefix since they're valid but have no values
|
||||
Assert::AreEqual(L"photo_$ISO_$CAMERA_MAKE", result);
|
||||
}
|
||||
};
|
||||
|
||||
TEST_CLASS(EdgeCaseTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(VeryLongString)
|
||||
{
|
||||
// Test with a very long input string
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
patterns[L"CAMERA_MAKE"] = L"Canon";
|
||||
|
||||
std::wstring longInput = L"prefix_";
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
longInput += L"$CAMERA_MAKE_";
|
||||
}
|
||||
|
||||
wchar_t result[4096] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, 4096, longInput.c_str(), patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
// Verify it starts correctly
|
||||
Assert::IsTrue(wcsstr(result, L"prefix_Canon_") == result);
|
||||
}
|
||||
|
||||
TEST_METHOD(ManyConsecutiveDollars)
|
||||
{
|
||||
// Test with many consecutive dollar signs
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
// 8 dollars should become 4 dollars
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo$$$$$$$$name", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"photo$$$$name", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(OnlyDollars)
|
||||
{
|
||||
// Test string with only dollar signs
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"$$$$", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"$$", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(UnicodeCharacters)
|
||||
{
|
||||
// Test with unicode characters in pattern values
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
patterns[L"TITLE"] = L"照片_фото_φωτογραφία";
|
||||
patterns[L"CREATOR"] = L"张三_Иван_Γιάννης";
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"$TITLE-$CREATOR", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"照片_фото_φωτογραφία-张三_Иван_Γιάννης", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(SingleDollar)
|
||||
{
|
||||
// Test with single dollar not followed by pattern
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"price$100", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"price$100", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(DollarFollowedByNumber)
|
||||
{
|
||||
// Test dollar followed by numbers (not a pattern)
|
||||
PowerRenameLib::MetadataPatternMap patterns;
|
||||
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"cost_$123.45", patterns);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"cost_$123.45", result);
|
||||
}
|
||||
};
|
||||
|
||||
TEST_CLASS(GetDatedFileNameTests)
|
||||
{
|
||||
public:
|
||||
// Helper to get a fixed test time for consistent testing
|
||||
SYSTEMTIME GetTestTime()
|
||||
{
|
||||
SYSTEMTIME testTime = { 0 };
|
||||
testTime.wYear = 2024;
|
||||
testTime.wMonth = 3; // March
|
||||
testTime.wDay = 15; // 15th
|
||||
testTime.wHour = 14; // 2 PM (24-hour format)
|
||||
testTime.wMinute = 30;
|
||||
testTime.wSecond = 45;
|
||||
testTime.wMilliseconds = 123;
|
||||
testTime.wDayOfWeek = 5; // Friday (0=Sunday, 5=Friday)
|
||||
return testTime;
|
||||
}
|
||||
|
||||
// Category 1: Tests for invalid patterns with extra characters (verify negative lookahead prevents wrong matching)
|
||||
|
||||
TEST_METHOD(InvalidPattern_YYY_NotMatched)
|
||||
{
|
||||
// Test $YYY (3 Y's) is not a valid pattern and should remain unchanged
|
||||
// Negative lookahead in $YY(?!Y) prevents matching $YYY
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$YYY", testTime);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"file_$YYY", result); // $YYY is invalid, should remain unchanged
|
||||
}
|
||||
|
||||
TEST_METHOD(InvalidPattern_DDD_NotPartiallyMatched)
|
||||
{
|
||||
// Test that $DDD (short weekday) is not confused with $DD (2-digit day)
|
||||
// This verifies negative lookahead works correctly
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$DDD", testTime);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"file_Fri", result); // Should be "Fri", not "15D"
|
||||
}
|
||||
|
||||
TEST_METHOD(InvalidPattern_MMM_NotPartiallyMatched)
|
||||
{
|
||||
// Test that $MMM (short month name) is not confused with $MM (2-digit month)
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$MMM", testTime);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"file_Mar", result); // Should be "Mar", not "03M"
|
||||
}
|
||||
|
||||
TEST_METHOD(InvalidPattern_HHH_NotMatched)
|
||||
{
|
||||
// Test $HHH (3 H's) is not valid and negative lookahead prevents $HH from matching
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$HHH", testTime);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"file_$HHH", result); // Should remain unchanged
|
||||
}
|
||||
|
||||
TEST_METHOD(SeparatedPatterns_SingleY)
|
||||
{
|
||||
// Test multiple $Y with separators works correctly
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$Y-$Y-$Y", testTime);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"file_4-4-4", result); // Each $Y outputs "4" (from 2024)
|
||||
}
|
||||
|
||||
TEST_METHOD(SeparatedPatterns_SingleD)
|
||||
{
|
||||
// Test multiple $D with separators works correctly
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$D.$D.$D", testTime);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"file_15.15.15", result); // Each $D outputs "15"
|
||||
}
|
||||
|
||||
// Category 2: Tests for mixed length patterns (verify longer patterns don't get matched incorrectly)
|
||||
|
||||
TEST_METHOD(MixedLengthYear_QuadFollowedBySingle)
|
||||
{
|
||||
// Test $YYYY$Y - should be 2024 + 4
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$YYYY$Y", testTime);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"file_20244", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(MixedLengthDay_TripleFollowedBySingle)
|
||||
{
|
||||
// Test $DDD$D - should be "Fri" + "15"
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$DDD$D", testTime);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"file_Fri15", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(MixedLengthDay_QuadFollowedByDouble)
|
||||
{
|
||||
// Test $DDDD$DD - should be "Friday" + "15"
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$DDDD$DD", testTime);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"file_Friday15", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(MixedLengthMonth_TripleFollowedBySingle)
|
||||
{
|
||||
// Test $MMM$M - should be "Mar" + "3"
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$MMM$M", testTime);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"file_Mar3", result);
|
||||
}
|
||||
|
||||
// Category 3: Tests for boundary conditions (patterns at start, end, with special chars)
|
||||
|
||||
TEST_METHOD(PatternAtStart)
|
||||
{
|
||||
// Test pattern at the very start of filename
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"$YYYY$M$D", testTime);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"2024315", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(PatternAtEnd)
|
||||
{
|
||||
// Test pattern at the very end of filename
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$Y", testTime);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"file_4", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(PatternWithSpecialChars)
|
||||
{
|
||||
// Test patterns surrounded by special characters
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file-$Y.$Y-$M", testTime);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"file-4.4-3", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(EmptyFileName)
|
||||
{
|
||||
// Test with empty input string - should return E_INVALIDARG
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"", testTime);
|
||||
|
||||
Assert::IsTrue(FAILED(hr)); // Empty string should fail
|
||||
Assert::AreEqual(E_INVALIDARG, hr);
|
||||
}
|
||||
|
||||
// Category 4: Tests to explicitly verify negative lookahead is working
|
||||
|
||||
TEST_METHOD(NegativeLookahead_YearNotMatchedInYYYY)
|
||||
{
|
||||
// Verify $Y doesn't match when part of $YYYY
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$YYYY", testTime);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"file_2024", result); // Should be "2024", not "202Y"
|
||||
}
|
||||
|
||||
TEST_METHOD(NegativeLookahead_MonthNotMatchedInMMM)
|
||||
{
|
||||
// Verify $M doesn't match when part of $MMM
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$MMM", testTime);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"file_Mar", result); // Should be "Mar", not "3ar"
|
||||
}
|
||||
|
||||
TEST_METHOD(NegativeLookahead_DayNotMatchedInDDDD)
|
||||
{
|
||||
// Verify $D doesn't match when part of $DDDD
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$DDDD", testTime);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"file_Friday", result); // Should be "Friday", not "15riday"
|
||||
}
|
||||
|
||||
TEST_METHOD(NegativeLookahead_HourNotMatchedInHH)
|
||||
{
|
||||
// Verify $H doesn't match when part of $HH
|
||||
// Note: $HH is 12-hour format, so 14:00 (2 PM) displays as "02"
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$HH", testTime);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"file_02", result); // 14:00 in 12-hour format is "02 PM"
|
||||
}
|
||||
|
||||
TEST_METHOD(NegativeLookahead_MillisecondNotMatchedInFFF)
|
||||
{
|
||||
// Verify $f doesn't match when part of $fff
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$fff", testTime);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"file_123", result); // Should be "123", not "1ff"
|
||||
}
|
||||
|
||||
// Category 5: Complex mixed scenarios
|
||||
|
||||
TEST_METHOD(ComplexMixedPattern_AllFormats)
|
||||
{
|
||||
// Test a complex realistic filename with mixed pattern lengths
|
||||
// Note: Using $hh for 24-hour format instead of $HH (which is 12-hour)
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"Photo_$YYYY-$MM-$DD_$hh-$mm-$ss_$fff", testTime);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"Photo_2024-03-15_14-30-45_123", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(ComplexMixedPattern_WithSeparators)
|
||||
{
|
||||
// Test multiple patterns of different lengths with separators
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"$YYYY_$Y-$Y_$MM_$M", testTime);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"2024_4-4_03_3", result);
|
||||
}
|
||||
|
||||
TEST_METHOD(ComplexMixedPattern_DayFormats)
|
||||
{
|
||||
// Test all day format variations in one string
|
||||
SYSTEMTIME testTime = GetTestTime();
|
||||
wchar_t result[MAX_PATH] = { 0 };
|
||||
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"$D-$DD-$DDD-$DDDD", testTime);
|
||||
|
||||
Assert::IsTrue(SUCCEEDED(hr));
|
||||
Assert::AreEqual(L"15-15-Fri-Friday", result);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,487 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "MetadataFormatHelper.h"
|
||||
#include <cmath>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
using namespace PowerRenameLib;
|
||||
|
||||
namespace MetadataFormatHelperTests
|
||||
{
|
||||
TEST_CLASS(FormatApertureTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(FormatAperture_ValidValue)
|
||||
{
|
||||
// Test formatting a typical aperture value
|
||||
std::wstring result = MetadataFormatHelper::FormatAperture(2.8);
|
||||
Assert::AreEqual(L"f/2.8", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(FormatAperture_SmallValue)
|
||||
{
|
||||
// Test small aperture (large f-number)
|
||||
std::wstring result = MetadataFormatHelper::FormatAperture(1.4);
|
||||
Assert::AreEqual(L"f/1.4", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(FormatAperture_LargeValue)
|
||||
{
|
||||
// Test large aperture (small f-number)
|
||||
std::wstring result = MetadataFormatHelper::FormatAperture(22.0);
|
||||
Assert::AreEqual(L"f/22.0", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(FormatAperture_RoundedValue)
|
||||
{
|
||||
// Test rounding to one decimal place
|
||||
std::wstring result = MetadataFormatHelper::FormatAperture(5.66666);
|
||||
Assert::AreEqual(L"f/5.7", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(FormatAperture_Zero)
|
||||
{
|
||||
// Test zero value
|
||||
std::wstring result = MetadataFormatHelper::FormatAperture(0.0);
|
||||
Assert::AreEqual(L"f/0.0", result.c_str());
|
||||
}
|
||||
};
|
||||
|
||||
TEST_CLASS(FormatShutterSpeedTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(FormatShutterSpeed_FastSpeed)
|
||||
{
|
||||
// Test fast shutter speed (fraction of second)
|
||||
std::wstring result = MetadataFormatHelper::FormatShutterSpeed(0.002);
|
||||
Assert::AreEqual(L"1/500s", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(FormatShutterSpeed_VeryFastSpeed)
|
||||
{
|
||||
// Test very fast shutter speed
|
||||
std::wstring result = MetadataFormatHelper::FormatShutterSpeed(0.0001);
|
||||
Assert::AreEqual(L"1/10000s", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(FormatShutterSpeed_SlowSpeed)
|
||||
{
|
||||
// Test slow shutter speed (more than 1 second)
|
||||
std::wstring result = MetadataFormatHelper::FormatShutterSpeed(2.5);
|
||||
Assert::AreEqual(L"2.5s", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(FormatShutterSpeed_OneSecond)
|
||||
{
|
||||
// Test exactly 1 second
|
||||
std::wstring result = MetadataFormatHelper::FormatShutterSpeed(1.0);
|
||||
Assert::AreEqual(L"1.0s", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(FormatShutterSpeed_VerySlowSpeed)
|
||||
{
|
||||
// Test very slow shutter speed (< 1 second but close)
|
||||
std::wstring result = MetadataFormatHelper::FormatShutterSpeed(0.5);
|
||||
Assert::AreEqual(L"1/2s", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(FormatShutterSpeed_Zero)
|
||||
{
|
||||
// Test zero value
|
||||
std::wstring result = MetadataFormatHelper::FormatShutterSpeed(0.0);
|
||||
Assert::AreEqual(L"0", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(FormatShutterSpeed_Negative)
|
||||
{
|
||||
// Test negative value (invalid but should handle gracefully)
|
||||
std::wstring result = MetadataFormatHelper::FormatShutterSpeed(-1.0);
|
||||
Assert::AreEqual(L"0", result.c_str());
|
||||
}
|
||||
};
|
||||
|
||||
TEST_CLASS(FormatISOTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(FormatISO_TypicalValue)
|
||||
{
|
||||
// Test typical ISO value
|
||||
std::wstring result = MetadataFormatHelper::FormatISO(400);
|
||||
Assert::AreEqual(L"ISO 400", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(FormatISO_LowValue)
|
||||
{
|
||||
// Test low ISO value
|
||||
std::wstring result = MetadataFormatHelper::FormatISO(100);
|
||||
Assert::AreEqual(L"ISO 100", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(FormatISO_HighValue)
|
||||
{
|
||||
// Test high ISO value
|
||||
std::wstring result = MetadataFormatHelper::FormatISO(12800);
|
||||
Assert::AreEqual(L"ISO 12800", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(FormatISO_Zero)
|
||||
{
|
||||
// Test zero value
|
||||
std::wstring result = MetadataFormatHelper::FormatISO(0);
|
||||
Assert::AreEqual(L"ISO", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(FormatISO_Negative)
|
||||
{
|
||||
// Test negative value (invalid but should handle gracefully)
|
||||
std::wstring result = MetadataFormatHelper::FormatISO(-100);
|
||||
Assert::AreEqual(L"ISO", result.c_str());
|
||||
}
|
||||
};
|
||||
|
||||
TEST_CLASS(FormatFlashTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(FormatFlash_Off)
|
||||
{
|
||||
// Test flash off (bit 0 = 0)
|
||||
std::wstring result = MetadataFormatHelper::FormatFlash(0x0);
|
||||
Assert::AreEqual(L"Flash Off", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(FormatFlash_On)
|
||||
{
|
||||
// Test flash on (bit 0 = 1)
|
||||
std::wstring result = MetadataFormatHelper::FormatFlash(0x1);
|
||||
Assert::AreEqual(L"Flash On", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(FormatFlash_OnWithAdditionalFlags)
|
||||
{
|
||||
// Test flash on with additional flags
|
||||
std::wstring result = MetadataFormatHelper::FormatFlash(0x5); // 0b0101 = fired, return detected
|
||||
Assert::AreEqual(L"Flash On", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(FormatFlash_OffWithAdditionalFlags)
|
||||
{
|
||||
// Test flash off with additional flags
|
||||
std::wstring result = MetadataFormatHelper::FormatFlash(0x10); // Bit 0 is 0
|
||||
Assert::AreEqual(L"Flash Off", result.c_str());
|
||||
}
|
||||
};
|
||||
|
||||
TEST_CLASS(FormatCoordinateTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(FormatCoordinate_NorthLatitude)
|
||||
{
|
||||
// Test north latitude
|
||||
std::wstring result = MetadataFormatHelper::FormatCoordinate(40.7128, true);
|
||||
Assert::AreEqual(L"40°42.77'N", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(FormatCoordinate_SouthLatitude)
|
||||
{
|
||||
// Test south latitude
|
||||
std::wstring result = MetadataFormatHelper::FormatCoordinate(-33.8688, true);
|
||||
Assert::AreEqual(L"33°52.13'S", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(FormatCoordinate_EastLongitude)
|
||||
{
|
||||
// Test east longitude
|
||||
std::wstring result = MetadataFormatHelper::FormatCoordinate(151.2093, false);
|
||||
Assert::AreEqual(L"151°12.56'E", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(FormatCoordinate_WestLongitude)
|
||||
{
|
||||
// Test west longitude
|
||||
std::wstring result = MetadataFormatHelper::FormatCoordinate(-74.0060, false);
|
||||
Assert::AreEqual(L"74°0.36'W", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(FormatCoordinate_ZeroLatitude)
|
||||
{
|
||||
// Test equator (0 degrees latitude)
|
||||
std::wstring result = MetadataFormatHelper::FormatCoordinate(0.0, true);
|
||||
Assert::AreEqual(L"0°0.00'N", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(FormatCoordinate_ZeroLongitude)
|
||||
{
|
||||
// Test prime meridian (0 degrees longitude)
|
||||
std::wstring result = MetadataFormatHelper::FormatCoordinate(0.0, false);
|
||||
Assert::AreEqual(L"0°0.00'E", result.c_str());
|
||||
}
|
||||
};
|
||||
|
||||
TEST_CLASS(FormatSystemTimeTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(FormatSystemTime_ValidDateTime)
|
||||
{
|
||||
// Test formatting a valid date and time
|
||||
SYSTEMTIME st = { 0 };
|
||||
st.wYear = 2024;
|
||||
st.wMonth = 3;
|
||||
st.wDay = 15;
|
||||
st.wHour = 14;
|
||||
st.wMinute = 30;
|
||||
st.wSecond = 45;
|
||||
|
||||
std::wstring result = MetadataFormatHelper::FormatSystemTime(st);
|
||||
Assert::AreEqual(L"2024-03-15 14:30:45", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(FormatSystemTime_Midnight)
|
||||
{
|
||||
// Test midnight time
|
||||
SYSTEMTIME st = { 0 };
|
||||
st.wYear = 2024;
|
||||
st.wMonth = 1;
|
||||
st.wDay = 1;
|
||||
st.wHour = 0;
|
||||
st.wMinute = 0;
|
||||
st.wSecond = 0;
|
||||
|
||||
std::wstring result = MetadataFormatHelper::FormatSystemTime(st);
|
||||
Assert::AreEqual(L"2024-01-01 00:00:00", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(FormatSystemTime_EndOfDay)
|
||||
{
|
||||
// Test end of day time
|
||||
SYSTEMTIME st = { 0 };
|
||||
st.wYear = 2024;
|
||||
st.wMonth = 12;
|
||||
st.wDay = 31;
|
||||
st.wHour = 23;
|
||||
st.wMinute = 59;
|
||||
st.wSecond = 59;
|
||||
|
||||
std::wstring result = MetadataFormatHelper::FormatSystemTime(st);
|
||||
Assert::AreEqual(L"2024-12-31 23:59:59", result.c_str());
|
||||
}
|
||||
};
|
||||
|
||||
TEST_CLASS(ParseSingleRationalTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(ParseSingleRational_ValidValue)
|
||||
{
|
||||
// Test parsing a valid rational: 5/2 = 2.5
|
||||
uint8_t bytes[] = { 5, 0, 0, 0, 2, 0, 0, 0 };
|
||||
double result = MetadataFormatHelper::ParseSingleRational(bytes, 0);
|
||||
Assert::AreEqual(2.5, result, 0.001);
|
||||
}
|
||||
|
||||
TEST_METHOD(ParseSingleRational_IntegerResult)
|
||||
{
|
||||
// Test parsing rational that results in integer: 10/5 = 2.0
|
||||
uint8_t bytes[] = { 10, 0, 0, 0, 5, 0, 0, 0 };
|
||||
double result = MetadataFormatHelper::ParseSingleRational(bytes, 0);
|
||||
Assert::AreEqual(2.0, result, 0.001);
|
||||
}
|
||||
|
||||
TEST_METHOD(ParseSingleRational_LargeNumerator)
|
||||
{
|
||||
// Test parsing with large numerator: 1000/100 = 10.0
|
||||
uint8_t bytes[] = { 0xE8, 0x03, 0, 0, 100, 0, 0, 0 }; // 1000 in little-endian
|
||||
double result = MetadataFormatHelper::ParseSingleRational(bytes, 0);
|
||||
Assert::AreEqual(10.0, result, 0.001);
|
||||
}
|
||||
|
||||
TEST_METHOD(ParseSingleRational_ZeroDenominator)
|
||||
{
|
||||
// Test parsing with zero denominator (should return 0.0)
|
||||
uint8_t bytes[] = { 5, 0, 0, 0, 0, 0, 0, 0 };
|
||||
double result = MetadataFormatHelper::ParseSingleRational(bytes, 0);
|
||||
Assert::AreEqual(0.0, result, 0.001);
|
||||
}
|
||||
|
||||
TEST_METHOD(ParseSingleRational_ZeroNumerator)
|
||||
{
|
||||
// Test parsing with zero numerator: 0/5 = 0.0
|
||||
uint8_t bytes[] = { 0, 0, 0, 0, 5, 0, 0, 0 };
|
||||
double result = MetadataFormatHelper::ParseSingleRational(bytes, 0);
|
||||
Assert::AreEqual(0.0, result, 0.001);
|
||||
}
|
||||
|
||||
TEST_METHOD(ParseSingleRational_WithOffset)
|
||||
{
|
||||
// Test parsing with offset
|
||||
uint8_t bytes[] = { 0xFF, 0xFF, 0xFF, 0xFF, 10, 0, 0, 0, 5, 0, 0, 0 }; // Offset = 4
|
||||
double result = MetadataFormatHelper::ParseSingleRational(bytes, 4);
|
||||
Assert::AreEqual(2.0, result, 0.001);
|
||||
}
|
||||
|
||||
TEST_METHOD(ParseSingleRational_NullPointer)
|
||||
{
|
||||
// Test with null pointer (should return 0.0)
|
||||
double result = MetadataFormatHelper::ParseSingleRational(nullptr, 0);
|
||||
Assert::AreEqual(0.0, result, 0.001);
|
||||
}
|
||||
};
|
||||
|
||||
TEST_CLASS(ParseSingleSRationalTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(ParseSingleSRational_PositiveValue)
|
||||
{
|
||||
// Test parsing positive signed rational: 5/2 = 2.5
|
||||
uint8_t bytes[] = { 5, 0, 0, 0, 2, 0, 0, 0 };
|
||||
double result = MetadataFormatHelper::ParseSingleSRational(bytes, 0);
|
||||
Assert::AreEqual(2.5, result, 0.001);
|
||||
}
|
||||
|
||||
TEST_METHOD(ParseSingleSRational_NegativeNumerator)
|
||||
{
|
||||
// Test parsing negative numerator: -5/2 = -2.5
|
||||
uint8_t bytes[] = { 0xFB, 0xFF, 0xFF, 0xFF, 2, 0, 0, 0 }; // -5 in two's complement
|
||||
double result = MetadataFormatHelper::ParseSingleSRational(bytes, 0);
|
||||
Assert::AreEqual(-2.5, result, 0.001);
|
||||
}
|
||||
|
||||
TEST_METHOD(ParseSingleSRational_NegativeDenominator)
|
||||
{
|
||||
// Test parsing negative denominator: 5/-2 = -2.5
|
||||
uint8_t bytes[] = { 5, 0, 0, 0, 0xFE, 0xFF, 0xFF, 0xFF }; // -2 in two's complement
|
||||
double result = MetadataFormatHelper::ParseSingleSRational(bytes, 0);
|
||||
Assert::AreEqual(-2.5, result, 0.001);
|
||||
}
|
||||
|
||||
TEST_METHOD(ParseSingleSRational_BothNegative)
|
||||
{
|
||||
// Test parsing both negative: -5/-2 = 2.5
|
||||
uint8_t bytes[] = { 0xFB, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0xFF };
|
||||
double result = MetadataFormatHelper::ParseSingleSRational(bytes, 0);
|
||||
Assert::AreEqual(2.5, result, 0.001);
|
||||
}
|
||||
|
||||
TEST_METHOD(ParseSingleSRational_ExposureBias)
|
||||
{
|
||||
// Test typical exposure bias value: -1/3 ≈ -0.333
|
||||
uint8_t bytes[] = { 0xFF, 0xFF, 0xFF, 0xFF, 3, 0, 0, 0 }; // -1/3
|
||||
double result = MetadataFormatHelper::ParseSingleSRational(bytes, 0);
|
||||
Assert::AreEqual(-0.333, result, 0.001);
|
||||
}
|
||||
|
||||
TEST_METHOD(ParseSingleSRational_ZeroDenominator)
|
||||
{
|
||||
// Test with zero denominator (should return 0.0)
|
||||
uint8_t bytes[] = { 5, 0, 0, 0, 0, 0, 0, 0 };
|
||||
double result = MetadataFormatHelper::ParseSingleSRational(bytes, 0);
|
||||
Assert::AreEqual(0.0, result, 0.001);
|
||||
}
|
||||
|
||||
TEST_METHOD(ParseSingleSRational_NullPointer)
|
||||
{
|
||||
// Test with null pointer (should return 0.0)
|
||||
double result = MetadataFormatHelper::ParseSingleSRational(nullptr, 0);
|
||||
Assert::AreEqual(0.0, result, 0.001);
|
||||
}
|
||||
};
|
||||
|
||||
TEST_CLASS(SanitizeForFileNameTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(SanitizeForFileName_ValidString)
|
||||
{
|
||||
// Test string without illegal characters
|
||||
std::wstring result = MetadataFormatHelper::SanitizeForFileName(L"Canon EOS 5D");
|
||||
Assert::AreEqual(L"Canon EOS 5D", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(SanitizeForFileName_WithColon)
|
||||
{
|
||||
// Test string with colon (illegal character)
|
||||
std::wstring result = MetadataFormatHelper::SanitizeForFileName(L"Photo:001");
|
||||
Assert::AreEqual(L"Photo_001", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(SanitizeForFileName_WithSlashes)
|
||||
{
|
||||
// Test string with forward and backward slashes
|
||||
std::wstring result = MetadataFormatHelper::SanitizeForFileName(L"Photos/2024\\January");
|
||||
Assert::AreEqual(L"Photos_2024_January", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(SanitizeForFileName_WithMultipleIllegalChars)
|
||||
{
|
||||
// Test string with multiple illegal characters
|
||||
std::wstring result = MetadataFormatHelper::SanitizeForFileName(L"<Test>:File|Name*?.txt");
|
||||
Assert::AreEqual(L"_Test__File_Name__.txt", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(SanitizeForFileName_WithQuotes)
|
||||
{
|
||||
// Test string with quotes
|
||||
std::wstring result = MetadataFormatHelper::SanitizeForFileName(L"Photo \"Best Shot\"");
|
||||
Assert::AreEqual(L"Photo _Best Shot_", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(SanitizeForFileName_WithTrailingDot)
|
||||
{
|
||||
// Test string with trailing dot (should be removed)
|
||||
std::wstring result = MetadataFormatHelper::SanitizeForFileName(L"filename.");
|
||||
Assert::AreEqual(L"filename", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(SanitizeForFileName_WithTrailingSpace)
|
||||
{
|
||||
// Test string with trailing space (should be removed)
|
||||
std::wstring result = MetadataFormatHelper::SanitizeForFileName(L"filename ");
|
||||
Assert::AreEqual(L"filename", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(SanitizeForFileName_WithMultipleTrailingDotsAndSpaces)
|
||||
{
|
||||
// Test string with multiple trailing dots and spaces
|
||||
std::wstring result = MetadataFormatHelper::SanitizeForFileName(L"filename. . ");
|
||||
Assert::AreEqual(L"filename", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(SanitizeForFileName_WithControlCharacters)
|
||||
{
|
||||
// Test string with control characters
|
||||
std::wstring result = MetadataFormatHelper::SanitizeForFileName(L"File\x01Name\x1F");
|
||||
Assert::AreEqual(L"File_Name_", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(SanitizeForFileName_EmptyString)
|
||||
{
|
||||
// Test empty string
|
||||
std::wstring result = MetadataFormatHelper::SanitizeForFileName(L"");
|
||||
Assert::AreEqual(L"", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(SanitizeForFileName_OnlyIllegalCharacters)
|
||||
{
|
||||
// Test string with only illegal characters
|
||||
std::wstring result = MetadataFormatHelper::SanitizeForFileName(L"<>:\"/\\|?*");
|
||||
Assert::AreEqual(L"_________", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(SanitizeForFileName_OnlyTrailingCharacters)
|
||||
{
|
||||
// Test string with only dots and spaces (should return empty)
|
||||
std::wstring result = MetadataFormatHelper::SanitizeForFileName(L". . ");
|
||||
Assert::AreEqual(L"", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(SanitizeForFileName_UnicodeCharacters)
|
||||
{
|
||||
// Test string with valid Unicode characters
|
||||
std::wstring result = MetadataFormatHelper::SanitizeForFileName(L"照片_2024年");
|
||||
Assert::AreEqual(L"照片_2024年", result.c_str());
|
||||
}
|
||||
|
||||
TEST_METHOD(SanitizeForFileName_MixedContent)
|
||||
{
|
||||
// Test realistic metadata string with multiple issues
|
||||
std::wstring result = MetadataFormatHelper::SanitizeForFileName(L"Copyright © 2024: John/Jane Doe. ");
|
||||
Assert::AreEqual(L"Copyright © 2024_ John_Jane Doe", result.c_str());
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -62,12 +62,6 @@ IFACEMETHODIMP CMockPowerRenameRegExEvents::OnFileTimeChanged(_In_ SYSTEMTIME fi
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
IFACEMETHODIMP CMockPowerRenameRegExEvents::OnMetadataChanged()
|
||||
{
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
|
||||
HRESULT CMockPowerRenameRegExEvents::s_CreateInstance(_Outptr_ IPowerRenameRegExEvents** ppsrree)
|
||||
{
|
||||
*ppsrree = nullptr;
|
||||
@@ -80,4 +74,3 @@ HRESULT CMockPowerRenameRegExEvents::s_CreateInstance(_Outptr_ IPowerRenameRegEx
|
||||
}
|
||||
return hr;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ public:
|
||||
IFACEMETHODIMP OnReplaceTermChanged(_In_ PCWSTR replaceTerm);
|
||||
IFACEMETHODIMP OnFlagsChanged(_In_ DWORD flags);
|
||||
IFACEMETHODIMP OnFileTimeChanged(_In_ SYSTEMTIME fileTime);
|
||||
IFACEMETHODIMP OnMetadataChanged();
|
||||
|
||||
static HRESULT s_CreateInstance(_Outptr_ IPowerRenameRegExEvents** ppsrree);
|
||||
|
||||
@@ -40,4 +39,3 @@ public:
|
||||
SYSTEMTIME m_fileTime = { 0 };
|
||||
long m_refCount;
|
||||
};
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<AdditionalLibraryDirectories>$(VCInstallDir)UnitTest\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||
<AdditionalDependencies>$(OutDir)\..\..\WinUI3Apps\PowerRenameLib.lib;comctl32.lib;pathcch.lib;windowscodecs.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;Pathcch.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalDependencies>$(OutDir)\..\..\WinUI3Apps\PowerRenameLib.lib;comctl32.lib;pathcch.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;Pathcch.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
@@ -49,14 +49,11 @@
|
||||
<ClInclude Include="CommonRegExTests.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="HelpersTests.cpp" />
|
||||
<ClCompile Include="MockPowerRenameItem.cpp" />
|
||||
<ClCompile Include="MockPowerRenameManagerEvents.cpp" />
|
||||
<ClCompile Include="MockPowerRenameRegExEvents.cpp" />
|
||||
<ClCompile Include="PowerRenameRegExBoostTests.cpp" />
|
||||
<ClCompile Include="PowerRenameManagerTests.cpp" />
|
||||
<ClCompile Include="MetadataFormatHelperTests.cpp" />
|
||||
<ClCompile Include="WICMetadataExtractorTests.cpp" />
|
||||
<ClCompile Include="pch.cpp">
|
||||
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
@@ -76,30 +73,8 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
<!-- Include all test data files for deployment -->
|
||||
<None Include="testdata\exif_test.jpg">
|
||||
<DeploymentContent>true</DeploymentContent>
|
||||
</None>
|
||||
<None Include="testdata\exif_test_2.jpg">
|
||||
<DeploymentContent>true</DeploymentContent>
|
||||
</None>
|
||||
<None Include="testdata\xmp_test.jpg">
|
||||
<DeploymentContent>true</DeploymentContent>
|
||||
</None>
|
||||
<None Include="testdata\xmp_test_2.jpg">
|
||||
<DeploymentContent>true</DeploymentContent>
|
||||
</None>
|
||||
<None Include="testdata\ATTRIBUTION.md">
|
||||
<DeploymentContent>true</DeploymentContent>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<Target Name="CopyTestData" AfterTargets="Build">
|
||||
<ItemGroup>
|
||||
<TestDataFiles Include="$(ProjectDir)testdata\**\*.*" />
|
||||
</ItemGroup>
|
||||
<Copy SourceFiles="@(TestDataFiles)" DestinationFolder="$(OutDir)testdata\%(RecursiveDir)" SkipUnchangedFiles="true" />
|
||||
</Target>
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />
|
||||
<Import Project="..\..\..\..\packages\boost.1.87.0\build\boost.targets" Condition="Exists('..\..\..\..\packages\boost.1.87.0\build\boost.targets')" />
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup>
|
||||
<ClCompile Include="HelpersTests.cpp" />
|
||||
<ClCompile Include="MockPowerRenameItem.cpp" />
|
||||
<ClCompile Include="MockPowerRenameManagerEvents.cpp" />
|
||||
<ClCompile Include="MockPowerRenameRegExEvents.cpp" />
|
||||
@@ -31,9 +30,6 @@
|
||||
<Filter Include="Header Files">
|
||||
<UniqueIdentifier>{d34a343a-52ef-4296-83c9-a94fa62062ff}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="testdata">
|
||||
<UniqueIdentifier>{8c9f3e2d-1a4b-4e5f-9c7d-2b8a6f3e1d4c}</UniqueIdentifier>
|
||||
</Filter>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="PowerRenameUnitTests.rc">
|
||||
@@ -42,20 +38,5 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
<None Include="testdata\exif_test.jpg">
|
||||
<Filter>testdata</Filter>
|
||||
</None>
|
||||
<None Include="testdata\exif_test_2.jpg">
|
||||
<Filter>testdata</Filter>
|
||||
</None>
|
||||
<None Include="testdata\xmp_test.jpg">
|
||||
<Filter>testdata</Filter>
|
||||
</None>
|
||||
<None Include="testdata\xmp_test_2.jpg">
|
||||
<Filter>testdata</Filter>
|
||||
</None>
|
||||
<None Include="testdata\ATTRIBUTION.md">
|
||||
<Filter>testdata</Filter>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,244 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "WICMetadataExtractor.h"
|
||||
#include <filesystem>
|
||||
#include <sstream>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
using namespace PowerRenameLib;
|
||||
|
||||
namespace WICMetadataExtractorTests
|
||||
{
|
||||
// Helper function to get the test data directory path
|
||||
std::wstring GetTestDataPath()
|
||||
{
|
||||
// Get the directory where the test DLL is located
|
||||
// When running with vstest, we need to get the DLL module handle
|
||||
HMODULE hModule = nullptr;
|
||||
GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
||||
reinterpret_cast<LPCWSTR>(&GetTestDataPath),
|
||||
&hModule);
|
||||
|
||||
wchar_t modulePath[MAX_PATH];
|
||||
GetModuleFileNameW(hModule, modulePath, MAX_PATH);
|
||||
std::filesystem::path dllPath(modulePath);
|
||||
|
||||
// Navigate to the test data directory
|
||||
// The test data is in the output directory alongside the DLL
|
||||
std::filesystem::path testDataPath = dllPath.parent_path() / L"testdata";
|
||||
|
||||
return testDataPath.wstring();
|
||||
}
|
||||
|
||||
TEST_CLASS(ExtractEXIFMetadataTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(ExtractEXIF_InvalidFile_ReturnsFalse)
|
||||
{
|
||||
// Test that EXIF extraction fails for nonexistent file
|
||||
WICMetadataExtractor extractor;
|
||||
EXIFMetadata metadata;
|
||||
|
||||
std::wstring testFile = GetTestDataPath() + L"\\nonexistent.jpg";
|
||||
bool result = extractor.ExtractEXIFMetadata(testFile, metadata);
|
||||
|
||||
Assert::IsFalse(result, L"EXIF extraction should fail for nonexistent file");
|
||||
}
|
||||
|
||||
TEST_METHOD(ExtractEXIF_ExifTest_AllFields)
|
||||
{
|
||||
// Test exif_test.jpg which contains comprehensive EXIF data
|
||||
WICMetadataExtractor extractor;
|
||||
EXIFMetadata metadata;
|
||||
|
||||
std::wstring testFile = GetTestDataPath() + L"\\exif_test.jpg";
|
||||
bool result = extractor.ExtractEXIFMetadata(testFile, metadata);
|
||||
|
||||
Assert::IsTrue(result, L"EXIF extraction should succeed");
|
||||
|
||||
// Verify all the fields that are in exif_test.jpg
|
||||
Assert::IsTrue(metadata.cameraMake.has_value(), L"Camera make should be present");
|
||||
Assert::AreEqual(L"samsung", metadata.cameraMake.value().c_str(), L"Camera make should be samsung");
|
||||
|
||||
Assert::IsTrue(metadata.cameraModel.has_value(), L"Camera model should be present");
|
||||
Assert::AreEqual(L"SM-G930P", metadata.cameraModel.value().c_str(), L"Camera model should be SM-G930P");
|
||||
|
||||
Assert::IsTrue(metadata.lensModel.has_value(), L"Lens model should be present");
|
||||
Assert::AreEqual(L"Samsung Galaxy S7 Rear Camera", metadata.lensModel.value().c_str(), L"Lens model should match");
|
||||
|
||||
Assert::IsTrue(metadata.iso.has_value(), L"ISO should be present");
|
||||
Assert::AreEqual(40, static_cast<int>(metadata.iso.value()), L"ISO should be 40");
|
||||
|
||||
Assert::IsTrue(metadata.aperture.has_value(), L"Aperture should be present");
|
||||
Assert::AreEqual(1.7, metadata.aperture.value(), 0.01, L"Aperture should be f/1.7");
|
||||
|
||||
Assert::IsTrue(metadata.shutterSpeed.has_value(), L"Shutter speed should be present");
|
||||
Assert::AreEqual(0.000625, metadata.shutterSpeed.value(), 0.000001, L"Shutter speed should be 0.000625s");
|
||||
|
||||
Assert::IsTrue(metadata.focalLength.has_value(), L"Focal length should be present");
|
||||
Assert::AreEqual(4.2, metadata.focalLength.value(), 0.1, L"Focal length should be 4.2mm");
|
||||
|
||||
Assert::IsTrue(metadata.flash.has_value(), L"Flash should be present");
|
||||
Assert::AreEqual(0u, static_cast<unsigned int>(metadata.flash.value()), L"Flash should be 0x0");
|
||||
|
||||
Assert::IsTrue(metadata.exposureBias.has_value(), L"Exposure bias should be present");
|
||||
Assert::AreEqual(0.0, metadata.exposureBias.value(), 0.01, L"Exposure bias should be 0 EV");
|
||||
|
||||
Assert::IsTrue(metadata.author.has_value(), L"Author should be present");
|
||||
Assert::AreEqual(L"Carl Seibert (Exif)", metadata.author.value().c_str(), L"Author should match");
|
||||
|
||||
Assert::IsTrue(metadata.copyright.has_value(), L"Copyright should be present");
|
||||
Assert::IsTrue(metadata.copyright.value().find(L"Carl Seibert") != std::wstring::npos, L"Copyright should contain Carl Seibert");
|
||||
}
|
||||
|
||||
TEST_METHOD(ExtractEXIF_ExifTest2_WidthHeight)
|
||||
{
|
||||
// Test exif_test_2.jpg which only contains width and height
|
||||
WICMetadataExtractor extractor;
|
||||
EXIFMetadata metadata;
|
||||
|
||||
std::wstring testFile = GetTestDataPath() + L"\\exif_test_2.jpg";
|
||||
bool result = extractor.ExtractEXIFMetadata(testFile, metadata);
|
||||
|
||||
Assert::IsTrue(result, L"EXIF extraction should succeed");
|
||||
|
||||
// exif_test_2.jpg only has width and height
|
||||
Assert::IsTrue(metadata.width.has_value(), L"Width should be present");
|
||||
Assert::AreEqual(1080u, static_cast<unsigned int>(metadata.width.value()), L"Width should be 1080px");
|
||||
|
||||
Assert::IsTrue(metadata.height.has_value(), L"Height should be present");
|
||||
Assert::AreEqual(810u, static_cast<unsigned int>(metadata.height.value()), L"Height should be 810px");
|
||||
|
||||
// Other fields should not be present
|
||||
Assert::IsFalse(metadata.cameraMake.has_value(), L"Camera make should not be present in exif_test_2.jpg");
|
||||
Assert::IsFalse(metadata.cameraModel.has_value(), L"Camera model should not be present in exif_test_2.jpg");
|
||||
Assert::IsFalse(metadata.iso.has_value(), L"ISO should not be present in exif_test_2.jpg");
|
||||
}
|
||||
|
||||
TEST_METHOD(ExtractEXIF_ClearCache)
|
||||
{
|
||||
// Test cache clearing works
|
||||
WICMetadataExtractor extractor;
|
||||
EXIFMetadata metadata;
|
||||
|
||||
std::wstring testFile = GetTestDataPath() + L"\\exif_test.jpg";
|
||||
|
||||
bool result1 = extractor.ExtractEXIFMetadata(testFile, metadata);
|
||||
Assert::IsTrue(result1);
|
||||
|
||||
extractor.ClearCache();
|
||||
|
||||
EXIFMetadata metadata2;
|
||||
bool result2 = extractor.ExtractEXIFMetadata(testFile, metadata2);
|
||||
Assert::IsTrue(result2);
|
||||
|
||||
// Both calls should succeed
|
||||
Assert::AreEqual(metadata.cameraMake.value().c_str(), metadata2.cameraMake.value().c_str());
|
||||
}
|
||||
};
|
||||
|
||||
TEST_CLASS(ExtractXMPMetadataTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(ExtractXMP_InvalidFile_ReturnsFalse)
|
||||
{
|
||||
// Test that XMP extraction fails for nonexistent file
|
||||
WICMetadataExtractor extractor;
|
||||
XMPMetadata metadata;
|
||||
|
||||
std::wstring testFile = GetTestDataPath() + L"\\nonexistent.jpg";
|
||||
bool result = extractor.ExtractXMPMetadata(testFile, metadata);
|
||||
|
||||
Assert::IsFalse(result, L"XMP extraction should fail for nonexistent file");
|
||||
}
|
||||
|
||||
TEST_METHOD(ExtractXMP_XmpTest_AllFields)
|
||||
{
|
||||
// Test xmp_test.jpg which contains comprehensive XMP data
|
||||
WICMetadataExtractor extractor;
|
||||
XMPMetadata metadata;
|
||||
|
||||
std::wstring testFile = GetTestDataPath() + L"\\xmp_test.jpg";
|
||||
bool result = extractor.ExtractXMPMetadata(testFile, metadata);
|
||||
|
||||
Assert::IsTrue(result, L"XMP extraction should succeed");
|
||||
|
||||
// Verify all the fields that are in xmp_test.jpg
|
||||
Assert::IsTrue(metadata.title.has_value(), L"Title should be present");
|
||||
Assert::AreEqual(L"object name here", metadata.title.value().c_str(), L"Title should match");
|
||||
|
||||
Assert::IsTrue(metadata.description.has_value(), L"Description should be present");
|
||||
Assert::IsTrue(metadata.description.value().find(L"This is a metadata test file") != std::wstring::npos,
|
||||
L"Description should contain expected text");
|
||||
|
||||
Assert::IsTrue(metadata.rights.has_value(), L"Rights should be present");
|
||||
Assert::AreEqual(L"metadatamatters.blog", metadata.rights.value().c_str(), L"Rights should match");
|
||||
|
||||
Assert::IsTrue(metadata.creatorTool.has_value(), L"Creator tool should be present");
|
||||
Assert::IsTrue(metadata.creatorTool.value().find(L"Adobe Photoshop Lightroom") != std::wstring::npos,
|
||||
L"Creator tool should contain Lightroom");
|
||||
|
||||
Assert::IsTrue(metadata.documentID.has_value(), L"Document ID should be present");
|
||||
Assert::IsTrue(metadata.documentID.value().find(L"xmp.did:") != std::wstring::npos,
|
||||
L"Document ID should start with xmp.did:");
|
||||
|
||||
Assert::IsTrue(metadata.instanceID.has_value(), L"Instance ID should be present");
|
||||
Assert::IsTrue(metadata.instanceID.value().find(L"xmp.iid:") != std::wstring::npos,
|
||||
L"Instance ID should start with xmp.iid:");
|
||||
|
||||
Assert::IsTrue(metadata.subject.has_value(), L"Subject keywords should be present");
|
||||
Assert::IsTrue(metadata.subject.value().size() > 0, L"Should have at least one keyword");
|
||||
}
|
||||
|
||||
TEST_METHOD(ExtractXMP_XmpTest2_BasicFields)
|
||||
{
|
||||
// Test xmp_test_2.jpg which only contains basic XMP fields
|
||||
WICMetadataExtractor extractor;
|
||||
XMPMetadata metadata;
|
||||
|
||||
std::wstring testFile = GetTestDataPath() + L"\\xmp_test_2.jpg";
|
||||
bool result = extractor.ExtractXMPMetadata(testFile, metadata);
|
||||
|
||||
Assert::IsTrue(result, L"XMP extraction should succeed");
|
||||
|
||||
// xmp_test_2.jpg only has CreatorTool, DocumentID, and InstanceID
|
||||
Assert::IsTrue(metadata.creatorTool.has_value(), L"Creator tool should be present");
|
||||
Assert::IsTrue(metadata.creatorTool.value().find(L"Adobe Photoshop CS6") != std::wstring::npos,
|
||||
L"Creator tool should be Photoshop CS6");
|
||||
|
||||
Assert::IsTrue(metadata.documentID.has_value(), L"Document ID should be present");
|
||||
Assert::IsTrue(metadata.documentID.value().find(L"xmp.did:") != std::wstring::npos,
|
||||
L"Document ID should start with xmp.did:");
|
||||
|
||||
Assert::IsTrue(metadata.instanceID.has_value(), L"Instance ID should be present");
|
||||
Assert::IsTrue(metadata.instanceID.value().find(L"xmp.iid:") != std::wstring::npos,
|
||||
L"Instance ID should start with xmp.iid:");
|
||||
|
||||
// Other fields should not be present
|
||||
Assert::IsFalse(metadata.title.has_value(), L"Title should not be present in xmp_test_2.jpg");
|
||||
Assert::IsFalse(metadata.description.has_value(), L"Description should not be present in xmp_test_2.jpg");
|
||||
Assert::IsFalse(metadata.rights.has_value(), L"Rights should not be present in xmp_test_2.jpg");
|
||||
Assert::IsFalse(metadata.creator.has_value(), L"Creator should not be present in xmp_test_2.jpg");
|
||||
}
|
||||
|
||||
TEST_METHOD(ExtractXMP_ClearCache)
|
||||
{
|
||||
// Test cache clearing works
|
||||
WICMetadataExtractor extractor;
|
||||
XMPMetadata metadata;
|
||||
|
||||
std::wstring testFile = GetTestDataPath() + L"\\xmp_test.jpg";
|
||||
|
||||
bool result1 = extractor.ExtractXMPMetadata(testFile, metadata);
|
||||
Assert::IsTrue(result1);
|
||||
|
||||
extractor.ClearCache();
|
||||
|
||||
XMPMetadata metadata2;
|
||||
bool result2 = extractor.ExtractXMPMetadata(testFile, metadata2);
|
||||
Assert::IsTrue(result2);
|
||||
|
||||
// Both calls should succeed
|
||||
Assert::AreEqual(metadata.title.value().c_str(), metadata2.title.value().c_str());
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
# Test Data Attribution
|
||||
|
||||
This directory contains test image files used for PowerRename metadata extraction unit tests. These images are sourced from Wikimedia Commons and are used under the Creative Commons licenses specified below.
|
||||
|
||||
## Test Files and Licenses
|
||||
|
||||
### Files from Carlseibert
|
||||
|
||||
**License:** [Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)](https://creativecommons.org/licenses/by-sa/4.0/)
|
||||
|
||||
- `exif_test.jpg` - Uploaded by [Carlseibert](https://commons.wikimedia.org/wiki/File%3AMetadata_test_file_-_includes_data_in_IIM%2C_XMP%2C_and_Exif.jpg) on Wikimedia Commons
|
||||
- `xmp_test.jpg` - Uploaded by [Carlseibert](https://commons.wikimedia.org/wiki/File%3AMetadata_test_file_-_includes_data_in_IIM%2C_XMP%2C_and_Exif.jpg) on Wikimedia Commons
|
||||
|
||||
### Files from Edward Steven
|
||||
|
||||
**License:** [Creative Commons Attribution-ShareAlike 2.0 Generic (CC BY-SA 2.0)](https://creativecommons.org/licenses/by-sa/2.0/)
|
||||
|
||||
- `exif_test_2.jpg` - Uploaded by [Edward Steven](https://commons.wikimedia.org/wiki/File%3AAreca_hutchinsoniana.jpg) on Wikimedia Commons
|
||||
- `xmp_test_2.jpg` - Uploaded by [Edward Steven](https://commons.wikimedia.org/wiki/File%3AAreca_hutchinsoniana.jpg) on Wikimedia Commons
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
We gratefully acknowledge the contributions of Carlseibert and Edward Steven for making these images available under Creative Commons licenses. Their work enables us to test metadata extraction functionality with real-world EXIF and XMP data.
|
||||
|
||||
## Usage
|
||||
|
||||
These test images are used in PowerRename's unit tests to verify correct extraction of:
|
||||
- EXIF metadata (camera make, model, ISO, aperture, shutter speed, etc.)
|
||||
- XMP metadata (creator, title, description, copyright, etc.)
|
||||
- GPS coordinates
|
||||
- Date/time information
|
||||
|
||||
## License Compliance
|
||||
|
||||
These test images are distributed as part of the PowerToys source code repository under their original Creative Commons licenses:
|
||||
- Files from Carlseibert: CC BY-SA 4.0
|
||||
- Files from Edward Steven: CC BY-SA 2.0
|
||||
|
||||
**Modifications:** These images have not been modified from their original versions downloaded from Wikimedia Commons. They are used in their original form for metadata extraction testing purposes.
|
||||
|
||||
**Distribution:** These test images are included in the PowerToys source repository and comply with the terms of their respective Creative Commons licenses through proper attribution in this file. While included in the source code, these images are not distributed in end-user installation packages or releases.
|
||||
|
||||
**Derivatives:** Any modifications or derivative works of these images must comply with the respective CC BY-SA license terms, including proper attribution and applying the same license to the modified versions.
|
||||
|
||||
For more information about Creative Commons licenses, visit: https://creativecommons.org/licenses/
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 136 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 740 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 136 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 740 KiB |
342
src/runner/cli_server.cpp
Normal file
342
src/runner/cli_server.cpp
Normal file
@@ -0,0 +1,342 @@
|
||||
#include "pch.h"
|
||||
#include "cli_server.h"
|
||||
|
||||
#include "command_registry.h"
|
||||
|
||||
#include <common/logger/logger.h>
|
||||
#include <common/utils/json.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <thread>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr wchar_t PIPE_NAME[] = LR"(\\.\pipe\PowerToys.Runner.CLI)";
|
||||
constexpr DWORD PIPE_BUFFER_SIZE = 64 * 1024;
|
||||
|
||||
std::once_flag startFlag;
|
||||
std::atomic_bool running = false;
|
||||
|
||||
std::wstring utf8_to_wstring(const std::string& input)
|
||||
{
|
||||
if (input.empty())
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
int wideSize = MultiByteToWideChar(CP_UTF8, 0, input.data(), static_cast<int>(input.size()), nullptr, 0);
|
||||
if (wideSize <= 0)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
std::wstring result(static_cast<size_t>(wideSize), L'\0');
|
||||
MultiByteToWideChar(CP_UTF8, 0, input.data(), static_cast<int>(input.size()), result.data(), wideSize);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string wstring_to_utf8(const std::wstring& input)
|
||||
{
|
||||
if (input.empty())
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
int narrowSize = WideCharToMultiByte(CP_UTF8, 0, input.data(), static_cast<int>(input.size()), nullptr, 0, nullptr, nullptr);
|
||||
if (narrowSize <= 0)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string result(static_cast<size_t>(narrowSize), '\0');
|
||||
WideCharToMultiByte(CP_UTF8, 0, input.data(), static_cast<int>(input.size()), result.data(), narrowSize, nullptr, nullptr);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool read_message(HANDLE pipe, std::string& out)
|
||||
{
|
||||
char buffer[4096];
|
||||
DWORD bytesRead = 0;
|
||||
bool continueReading = true;
|
||||
|
||||
while (continueReading)
|
||||
{
|
||||
BOOL success = ReadFile(pipe, buffer, sizeof(buffer), &bytesRead, nullptr);
|
||||
if (!success)
|
||||
{
|
||||
DWORD error = GetLastError();
|
||||
if (error == ERROR_MORE_DATA)
|
||||
{
|
||||
out.append(buffer, buffer + bytesRead);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (error != ERROR_BROKEN_PIPE && error != ERROR_PIPE_NOT_CONNECTED)
|
||||
{
|
||||
Logger::warn(L"CLI pipe read failed with error {}", error);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (bytesRead > 0)
|
||||
{
|
||||
out.append(buffer, buffer + bytesRead);
|
||||
}
|
||||
continueReading = false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
json::JsonArray parameters_to_json(const std::vector<pt::cli::CommandParameter>& parameters)
|
||||
{
|
||||
json::JsonArray array;
|
||||
for (const auto& parameter : parameters)
|
||||
{
|
||||
json::JsonObject node;
|
||||
node.SetNamedValue(L"name", json::value(parameter.name));
|
||||
node.SetNamedValue(L"required", json::value(parameter.required));
|
||||
node.SetNamedValue(L"description", json::value(parameter.description));
|
||||
array.Append(json::value(std::move(node)));
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
pt::cli::CommandResult handle_system_command(const std::wstring& action, const json::JsonObject& args)
|
||||
{
|
||||
if (action == L"list-modules")
|
||||
{
|
||||
auto snapshot = CommandRegistry::instance().snapshot();
|
||||
json::JsonArray modules;
|
||||
for (auto& moduleInfo : snapshot)
|
||||
{
|
||||
json::JsonObject moduleJson;
|
||||
moduleJson.SetNamedValue(L"module", json::value(moduleInfo.moduleKey));
|
||||
|
||||
json::JsonArray commands;
|
||||
for (const auto& descriptor : moduleInfo.commands)
|
||||
{
|
||||
json::JsonObject cmdJson;
|
||||
cmdJson.SetNamedValue(L"action", json::value(descriptor.action));
|
||||
cmdJson.SetNamedValue(L"description", json::value(descriptor.description));
|
||||
cmdJson.SetNamedValue(L"requiresElevation", json::value(descriptor.requiresElevation));
|
||||
cmdJson.SetNamedValue(L"longRunning", json::value(descriptor.longRunning));
|
||||
cmdJson.SetNamedValue(L"parameters", json::value(parameters_to_json(descriptor.parameters)));
|
||||
commands.Append(json::value(std::move(cmdJson)));
|
||||
}
|
||||
|
||||
moduleJson.SetNamedValue(L"commands", json::value(std::move(commands)));
|
||||
modules.Append(json::value(std::move(moduleJson)));
|
||||
}
|
||||
|
||||
json::JsonObject payload;
|
||||
payload.SetNamedValue(L"modules", json::value(std::move(modules)));
|
||||
return pt::cli::CommandResult::Success(std::move(payload));
|
||||
}
|
||||
|
||||
if (action == L"list-commands")
|
||||
{
|
||||
if (!args.HasKey(L"module"))
|
||||
{
|
||||
return pt::cli::CommandResult::Error(L"E_ARGS_INVALID", L"'module' argument is required.");
|
||||
}
|
||||
|
||||
auto moduleName = std::wstring(args.GetNamedString(L"module").c_str());
|
||||
auto reflection = CommandRegistry::instance().snapshot(moduleName);
|
||||
if (!reflection.has_value())
|
||||
{
|
||||
return pt::cli::CommandResult::Error(L"E_MODULE_NOT_FOUND", L"Module not registered.");
|
||||
}
|
||||
|
||||
json::JsonArray commands;
|
||||
for (const auto& descriptor : reflection->commands)
|
||||
{
|
||||
json::JsonObject cmdJson;
|
||||
cmdJson.SetNamedValue(L"action", json::value(descriptor.action));
|
||||
cmdJson.SetNamedValue(L"description", json::value(descriptor.description));
|
||||
cmdJson.SetNamedValue(L"requiresElevation", json::value(descriptor.requiresElevation));
|
||||
cmdJson.SetNamedValue(L"longRunning", json::value(descriptor.longRunning));
|
||||
cmdJson.SetNamedValue(L"parameters", json::value(parameters_to_json(descriptor.parameters)));
|
||||
commands.Append(json::value(std::move(cmdJson)));
|
||||
}
|
||||
|
||||
json::JsonObject payload;
|
||||
payload.SetNamedValue(L"module", json::value(reflection->moduleKey));
|
||||
payload.SetNamedValue(L"commands", json::value(std::move(commands)));
|
||||
return pt::cli::CommandResult::Success(std::move(payload));
|
||||
}
|
||||
|
||||
if (action == L"ping")
|
||||
{
|
||||
json::JsonObject payload;
|
||||
payload.SetNamedValue(L"status", json::value(L"ok"));
|
||||
return pt::cli::CommandResult::Success(std::move(payload));
|
||||
}
|
||||
|
||||
return pt::cli::CommandResult::Error(L"E_COMMAND_NOT_FOUND", L"Unsupported system command.");
|
||||
}
|
||||
|
||||
pt::cli::CommandResult dispatch_command(const std::wstring& module, const std::wstring& action, const json::JsonObject& args)
|
||||
{
|
||||
if (module == L"$system")
|
||||
{
|
||||
return handle_system_command(action, args);
|
||||
}
|
||||
|
||||
pt::cli::CommandInvocation invocation{ action, args };
|
||||
return CommandRegistry::instance().execute(module, invocation);
|
||||
}
|
||||
|
||||
json::JsonObject build_error_payload(const std::wstring& code, const std::wstring& message)
|
||||
{
|
||||
json::JsonObject error;
|
||||
error.SetNamedValue(L"code", json::value(code));
|
||||
error.SetNamedValue(L"message", json::value(message));
|
||||
return error;
|
||||
}
|
||||
|
||||
void write_response(HANDLE pipe, const json::JsonObject& response)
|
||||
{
|
||||
auto serialized = response.Stringify();
|
||||
auto utf8 = wstring_to_utf8(serialized.c_str());
|
||||
DWORD bytesWritten = 0;
|
||||
WriteFile(pipe, utf8.data(), static_cast<DWORD>(utf8.size()), &bytesWritten, nullptr);
|
||||
FlushFileBuffers(pipe);
|
||||
}
|
||||
|
||||
void handle_session(HANDLE pipe)
|
||||
{
|
||||
std::string rawRequest;
|
||||
if (!read_message(pipe, rawRequest))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
json::JsonObject response;
|
||||
response.SetNamedValue(L"v", json::value(1));
|
||||
|
||||
try
|
||||
{
|
||||
auto requestText = utf8_to_wstring(rawRequest);
|
||||
if (requestText.empty())
|
||||
{
|
||||
response.SetNamedValue(L"status", json::value(L"error"));
|
||||
response.SetNamedValue(L"error", json::value(build_error_payload(L"E_INVALID", L"Empty request.")));
|
||||
write_response(pipe, response);
|
||||
return;
|
||||
}
|
||||
|
||||
auto jsonValue = winrt::Windows::Data::Json::JsonValue::Parse(requestText);
|
||||
auto root = jsonValue.GetObjectW();
|
||||
|
||||
auto correlationId = root.GetNamedString(L"correlationId", L"");
|
||||
response.SetNamedValue(L"correlationId", json::value(correlationId));
|
||||
|
||||
if (!root.HasKey(L"command"))
|
||||
{
|
||||
response.SetNamedValue(L"status", json::value(L"error"));
|
||||
response.SetNamedValue(L"error", json::value(build_error_payload(L"E_INVALID", L"Missing command payload.")));
|
||||
write_response(pipe, response);
|
||||
return;
|
||||
}
|
||||
|
||||
auto command = root.GetNamedObject(L"command");
|
||||
const std::wstring module = std::wstring(command.GetNamedString(L"module", L"").c_str());
|
||||
const std::wstring action = std::wstring(command.GetNamedString(L"action", L"").c_str());
|
||||
|
||||
json::JsonObject args = json::JsonObject();
|
||||
if (command.HasKey(L"args"))
|
||||
{
|
||||
args = command.GetNamedObject(L"args");
|
||||
}
|
||||
|
||||
if (module.empty() || action.empty())
|
||||
{
|
||||
response.SetNamedValue(L"status", json::value(L"error"));
|
||||
response.SetNamedValue(L"error", json::value(build_error_payload(L"E_ARGS_INVALID", L"'module' and 'action' must be provided.")));
|
||||
write_response(pipe, response);
|
||||
return;
|
||||
}
|
||||
|
||||
auto result = dispatch_command(module, action, args);
|
||||
if (result.ok)
|
||||
{
|
||||
response.SetNamedValue(L"status", json::value(L"ok"));
|
||||
response.SetNamedValue(L"result", json::value(result.data));
|
||||
}
|
||||
else
|
||||
{
|
||||
response.SetNamedValue(L"status", json::value(L"error"));
|
||||
auto errorCode = result.errorCode.empty() ? L"E_INTERNAL" : result.errorCode;
|
||||
auto errorMsg = result.errorMessage.empty() ? L"Command failed." : result.errorMessage;
|
||||
response.SetNamedValue(L"error", json::value(build_error_payload(errorCode, errorMsg)));
|
||||
}
|
||||
}
|
||||
catch (const winrt::hresult_error&)
|
||||
{
|
||||
response.SetNamedValue(L"status", json::value(L"error"));
|
||||
response.SetNamedValue(L"error", json::value(build_error_payload(L"E_INVALID_JSON", L"Request payload was not valid JSON.")));
|
||||
}
|
||||
catch (const std::exception& ex)
|
||||
{
|
||||
Logger::error(L"CLI request processing threw: {}", winrt::to_hstring(ex.what()));
|
||||
response.SetNamedValue(L"status", json::value(L"error"));
|
||||
response.SetNamedValue(L"error", json::value(build_error_payload(L"E_INTERNAL", L"Internal processing failure.")));
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::error(L"CLI request processing failed with unknown exception.");
|
||||
response.SetNamedValue(L"status", json::value(L"error"));
|
||||
response.SetNamedValue(L"error", json::value(build_error_payload(L"E_INTERNAL", L"Unknown processing failure.")));
|
||||
}
|
||||
|
||||
write_response(pipe, response);
|
||||
}
|
||||
|
||||
void server_loop()
|
||||
{
|
||||
while (running.load())
|
||||
{
|
||||
HANDLE pipe = CreateNamedPipeW(
|
||||
PIPE_NAME,
|
||||
PIPE_ACCESS_DUPLEX,
|
||||
PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
|
||||
PIPE_UNLIMITED_INSTANCES,
|
||||
PIPE_BUFFER_SIZE,
|
||||
PIPE_BUFFER_SIZE,
|
||||
0,
|
||||
nullptr);
|
||||
|
||||
if (pipe == INVALID_HANDLE_VALUE)
|
||||
{
|
||||
DWORD error = GetLastError();
|
||||
Logger::error(L"Failed to create CLI named pipe (error {}).", error);
|
||||
Sleep(1000);
|
||||
continue;
|
||||
}
|
||||
|
||||
BOOL connected = ConnectNamedPipe(pipe, nullptr)
|
||||
? TRUE
|
||||
: (GetLastError() == ERROR_PIPE_CONNECTED);
|
||||
|
||||
if (connected)
|
||||
{
|
||||
handle_session(pipe);
|
||||
}
|
||||
|
||||
FlushFileBuffers(pipe);
|
||||
DisconnectNamedPipe(pipe);
|
||||
CloseHandle(pipe);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void start_cli_server()
|
||||
{
|
||||
std::call_once(startFlag, [] {
|
||||
running = true;
|
||||
std::thread(server_loop).detach();
|
||||
});
|
||||
}
|
||||
3
src/runner/cli_server.h
Normal file
3
src/runner/cli_server.h
Normal file
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
void start_cli_server();
|
||||
144
src/runner/command_registry.cpp
Normal file
144
src/runner/command_registry.cpp
Normal file
@@ -0,0 +1,144 @@
|
||||
#include "pch.h"
|
||||
#include "command_registry.h"
|
||||
|
||||
#include <common/logger/logger.h>
|
||||
#include <common/utils/elevation.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cwctype>
|
||||
|
||||
CommandRegistry& CommandRegistry::instance()
|
||||
{
|
||||
static CommandRegistry registry;
|
||||
return registry;
|
||||
}
|
||||
|
||||
void CommandRegistry::register_module(PowertoyModuleIface* module)
|
||||
{
|
||||
if (!module)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
auto provider = module->command_provider();
|
||||
if (!provider)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
std::wstring moduleKey = provider->ModuleKey();
|
||||
if (moduleKey.empty())
|
||||
{
|
||||
moduleKey = module->get_key();
|
||||
}
|
||||
|
||||
auto descriptors = provider->DescribeCommands();
|
||||
if (descriptors.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Entry entry{};
|
||||
entry.provider = provider;
|
||||
entry.moduleKey = moduleKey;
|
||||
|
||||
for (const auto& descriptor : descriptors)
|
||||
{
|
||||
auto normalizedAction = normalize_key(descriptor.action);
|
||||
entry.descriptorsByAction.emplace(normalizedAction, descriptor);
|
||||
}
|
||||
|
||||
std::unique_lock guard{ mutex_ };
|
||||
entries_[normalize_key(moduleKey)] = std::move(entry);
|
||||
}
|
||||
|
||||
pt::cli::CommandResult CommandRegistry::execute(const std::wstring& moduleKey, const pt::cli::CommandInvocation& invocation)
|
||||
{
|
||||
std::shared_lock guard{ mutex_ };
|
||||
|
||||
auto moduleIt = entries_.find(normalize_key(moduleKey));
|
||||
if (moduleIt == entries_.end())
|
||||
{
|
||||
return pt::cli::CommandResult::Error(L"E_MODULE_NOT_FOUND", L"Module not registered for CLI use.");
|
||||
}
|
||||
|
||||
auto& entry = moduleIt->second;
|
||||
|
||||
auto descriptorIt = entry.descriptorsByAction.find(normalize_key(invocation.action));
|
||||
if (descriptorIt == entry.descriptorsByAction.end())
|
||||
{
|
||||
return pt::cli::CommandResult::Error(L"E_COMMAND_NOT_FOUND", L"Command not available for this module.");
|
||||
}
|
||||
|
||||
const auto& descriptor = descriptorIt->second;
|
||||
if (descriptor.requiresElevation && !is_process_elevated())
|
||||
{
|
||||
return pt::cli::CommandResult::Error(L"E_NEEDS_ELEVATION", L"This command requires elevation.");
|
||||
}
|
||||
|
||||
auto provider = entry.provider;
|
||||
guard.unlock();
|
||||
|
||||
try
|
||||
{
|
||||
return provider->Execute(invocation);
|
||||
}
|
||||
catch (const std::exception& ex)
|
||||
{
|
||||
Logger::error(L"CLI command execution failed: {}", winrt::to_hstring(ex.what()));
|
||||
return pt::cli::CommandResult::Error(L"E_INTERNAL", L"Command execution failed due to an internal error.");
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::error(L"CLI command execution failed due to an unknown exception.");
|
||||
return pt::cli::CommandResult::Error(L"E_INTERNAL", L"Command execution failed due to an unknown error.");
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<CommandModuleReflection> CommandRegistry::snapshot() const
|
||||
{
|
||||
std::shared_lock guard{ mutex_ };
|
||||
std::vector<CommandModuleReflection> result;
|
||||
result.reserve(entries_.size());
|
||||
|
||||
for (const auto& [normalizedKey, entry] : entries_)
|
||||
{
|
||||
CommandModuleReflection reflection;
|
||||
reflection.moduleKey = entry.moduleKey;
|
||||
for (const auto& [actionKey, descriptor] : entry.descriptorsByAction)
|
||||
{
|
||||
reflection.commands.push_back(descriptor);
|
||||
}
|
||||
result.push_back(std::move(reflection));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::optional<CommandModuleReflection> CommandRegistry::snapshot(const std::wstring& moduleKey) const
|
||||
{
|
||||
std::shared_lock guard{ mutex_ };
|
||||
auto it = entries_.find(normalize_key(moduleKey));
|
||||
if (it == entries_.end())
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
CommandModuleReflection reflection;
|
||||
reflection.moduleKey = it->second.moduleKey;
|
||||
for (const auto& [actionKey, descriptor] : it->second.descriptorsByAction)
|
||||
{
|
||||
reflection.commands.push_back(descriptor);
|
||||
}
|
||||
|
||||
return reflection;
|
||||
}
|
||||
|
||||
std::wstring CommandRegistry::normalize_key(const std::wstring& value)
|
||||
{
|
||||
std::wstring normalized = value;
|
||||
std::transform(normalized.begin(), normalized.end(), normalized.begin(), [](wchar_t ch) {
|
||||
return static_cast<wchar_t>(std::towlower(ch));
|
||||
});
|
||||
return normalized;
|
||||
}
|
||||
42
src/runner/command_registry.h
Normal file
42
src/runner/command_registry.h
Normal file
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include <modules/interface/powertoy_module_interface.h>
|
||||
#include <modules/interface/powertoy_cli.h>
|
||||
#include <common/utils/json.h>
|
||||
|
||||
#include <unordered_map>
|
||||
#include <shared_mutex>
|
||||
#include <vector>
|
||||
#include <optional>
|
||||
|
||||
struct CommandModuleReflection
|
||||
{
|
||||
std::wstring moduleKey;
|
||||
std::vector<pt::cli::CommandDescriptor> commands;
|
||||
};
|
||||
|
||||
class CommandRegistry
|
||||
{
|
||||
public:
|
||||
static CommandRegistry& instance();
|
||||
|
||||
void register_module(PowertoyModuleIface* module);
|
||||
|
||||
pt::cli::CommandResult execute(const std::wstring& moduleKey, const pt::cli::CommandInvocation& invocation);
|
||||
|
||||
std::vector<CommandModuleReflection> snapshot() const;
|
||||
std::optional<CommandModuleReflection> snapshot(const std::wstring& moduleKey) const;
|
||||
|
||||
private:
|
||||
struct Entry
|
||||
{
|
||||
pt::cli::IModuleCommandProvider* provider = nullptr;
|
||||
std::wstring moduleKey;
|
||||
std::unordered_map<std::wstring, pt::cli::CommandDescriptor> descriptorsByAction;
|
||||
};
|
||||
|
||||
static std::wstring normalize_key(const std::wstring& value);
|
||||
|
||||
mutable std::shared_mutex mutex_;
|
||||
std::unordered_map<std::wstring, Entry> entries_;
|
||||
};
|
||||
@@ -28,7 +28,9 @@
|
||||
#include <common/utils/clean_video_conference.h>
|
||||
|
||||
#include "UpdateUtils.h"
|
||||
#include "ActionRunnerUtils.h"
|
||||
#include "ActionRunnerUtils.h"
|
||||
#include "command_registry.h"
|
||||
#include "cli_server.h"
|
||||
|
||||
#include <winrt/Windows.System.h>
|
||||
|
||||
@@ -180,17 +182,22 @@ int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow
|
||||
L"PowerToys.LightSwitchModuleInterface.dll",
|
||||
};
|
||||
|
||||
for (auto moduleSubdir : knownModules)
|
||||
{
|
||||
try
|
||||
{
|
||||
auto pt_module = load_powertoy(moduleSubdir);
|
||||
modules().emplace(pt_module->get_key(), std::move(pt_module));
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
std::wstring errorMessage = POWER_TOYS_MODULE_LOAD_FAIL;
|
||||
errorMessage += moduleSubdir;
|
||||
for (auto moduleSubdir : knownModules)
|
||||
{
|
||||
try
|
||||
{
|
||||
auto ptModule = load_powertoy(moduleSubdir);
|
||||
std::wstring moduleKey{ ptModule->get_key() };
|
||||
auto [it, inserted] = modules().emplace(moduleKey, std::move(ptModule));
|
||||
if (inserted)
|
||||
{
|
||||
CommandRegistry::instance().register_module(it->second.operator->());
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
std::wstring errorMessage = POWER_TOYS_MODULE_LOAD_FAIL;
|
||||
errorMessage += moduleSubdir;
|
||||
|
||||
#ifdef _DEBUG
|
||||
// In debug mode, simply log the warning and continue execution.
|
||||
@@ -205,11 +212,12 @@ int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow
|
||||
MB_OK | MB_ICONERROR);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
// Start initial powertoys
|
||||
start_enabled_powertoys();
|
||||
std::wstring product_version = get_product_version();
|
||||
Trace::EventLaunch(product_version, isProcessElevated);
|
||||
}
|
||||
// Start initial powertoys
|
||||
start_enabled_powertoys();
|
||||
start_cli_server();
|
||||
std::wstring product_version = get_product_version();
|
||||
Trace::EventLaunch(product_version, isProcessElevated);
|
||||
PTSettingsHelper::save_last_version_run(product_version);
|
||||
|
||||
if (openSettings)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Target Name="GenerateResourceFiles" BeforeTargets="PrepareForBuild">
|
||||
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted $(SolutionDir)\tools\build\convert-resx-to-rc.ps1 $(MSBuildThisFileDirectory) resource.base.h resource.h runner.base.rc runner.rc" />
|
||||
@@ -65,6 +65,8 @@
|
||||
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="powertoy_module.cpp" />
|
||||
<ClCompile Include="command_registry.cpp" />
|
||||
<ClCompile Include="cli_server.cpp" />
|
||||
<ClCompile Include="main.cpp" />
|
||||
<ClCompile Include="restart_elevated.cpp" />
|
||||
<ClCompile Include="centralized_kb_hook.cpp" />
|
||||
@@ -86,6 +88,8 @@
|
||||
<ClInclude Include="centralized_kb_hook.h" />
|
||||
<ClInclude Include="settings_telemetry.h" />
|
||||
<ClInclude Include="UpdateUtils.h" />
|
||||
<ClInclude Include="command_registry.h" />
|
||||
<ClInclude Include="cli_server.h" />
|
||||
<ClInclude Include="powertoy_module.h" />
|
||||
<ClInclude Include="resource.h" />
|
||||
<ClInclude Include="restart_elevated.h" />
|
||||
@@ -174,4 +178,4 @@
|
||||
<Error Condition="!Exists('..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
|
||||
<Error Condition="!Exists('..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets'))" />
|
||||
</Target>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup>
|
||||
<ClCompile Include="command_registry.cpp">
|
||||
<Filter>Utils</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="cli_server.cpp">
|
||||
<Filter>Utils</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="main.cpp" />
|
||||
<ClCompile Include="pch.cpp" />
|
||||
<ClCompile Include="unhandled_exception_handler.cpp">
|
||||
@@ -57,6 +63,12 @@
|
||||
<ClInclude Include="tray_icon.h">
|
||||
<Filter>Utils</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="command_registry.h">
|
||||
<Filter>Utils</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="cli_server.h">
|
||||
<Filter>Utils</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="powertoy_module.h">
|
||||
<Filter>Utils</Filter>
|
||||
</ClInclude>
|
||||
@@ -132,4 +144,4 @@
|
||||
<ItemGroup>
|
||||
<Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natvis" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
340
tools/ptcli/Program.cs
Normal file
340
tools/ptcli/Program.cs
Normal file
@@ -0,0 +1,340 @@
|
||||
// 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.Globalization;
|
||||
using System.IO;
|
||||
using System.IO.Pipes;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PowerToys.Cli
|
||||
{
|
||||
internal sealed class Program
|
||||
{
|
||||
private const string PipeName = "PowerToys.Runner.CLI";
|
||||
private static readonly JsonSerializerOptions JsonOutputOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
PropertyNamingPolicy = null,
|
||||
};
|
||||
|
||||
private static readonly JsonSerializerOptions JsonRequestOptions = new()
|
||||
{
|
||||
WriteIndented = false,
|
||||
PropertyNamingPolicy = null,
|
||||
};
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
public static async Task<int> Main(string[] args)
|
||||
{
|
||||
if (args.Length == 0)
|
||||
{
|
||||
PrintHelp();
|
||||
return 1;
|
||||
}
|
||||
|
||||
bool listModules = false;
|
||||
bool listCommands = false;
|
||||
string? listCommandsModule = null;
|
||||
string? module = null;
|
||||
string? action = null;
|
||||
bool rawJson = false;
|
||||
int timeoutMs = 20000;
|
||||
|
||||
var payload = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
for (int i = 0; i < args.Length; i++)
|
||||
{
|
||||
var argument = args[i];
|
||||
switch (argument)
|
||||
{
|
||||
case "--help":
|
||||
case "-h":
|
||||
PrintHelp();
|
||||
return 0;
|
||||
case "--list-modules":
|
||||
listModules = true;
|
||||
break;
|
||||
case "--list-commands":
|
||||
listCommands = true;
|
||||
if (i + 1 < args.Length && !args[i + 1].StartsWith('-'))
|
||||
{
|
||||
listCommandsModule = args[++i];
|
||||
}
|
||||
|
||||
break;
|
||||
case "--module":
|
||||
case "-m":
|
||||
module = RequireValue(args, ref i, argument);
|
||||
break;
|
||||
case "--action":
|
||||
case "-a":
|
||||
action = RequireValue(args, ref i, argument);
|
||||
break;
|
||||
case "--json":
|
||||
rawJson = true;
|
||||
break;
|
||||
case "--timeout":
|
||||
var timeoutValue = RequireValue(args, ref i, argument);
|
||||
if (!int.TryParse(timeoutValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out timeoutMs) || timeoutMs <= 0)
|
||||
{
|
||||
Console.Error.WriteLine("--timeout expects a positive integer value (milliseconds).");
|
||||
return 1;
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
if (argument.StartsWith("--", StringComparison.Ordinal))
|
||||
{
|
||||
var key = argument.Substring(2);
|
||||
var value = RequireValue(args, ref i, argument);
|
||||
payload[key] = ParseValue(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.Error.WriteLine($"Unrecognized argument '{argument}'.");
|
||||
PrintHelp();
|
||||
return 1;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (listModules)
|
||||
{
|
||||
return await ExecuteCommandAsync(string.Empty, "list-modules", new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase), rawJson, timeoutMs);
|
||||
}
|
||||
|
||||
if (listCommands)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(listCommandsModule))
|
||||
{
|
||||
Console.Error.WriteLine("--list-commands requires a module name (e.g. ptcli --list-commands awake).");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var argsPayload = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["module"] = listCommandsModule!,
|
||||
};
|
||||
|
||||
return await ExecuteCommandAsync(string.Empty, "list-commands", argsPayload, rawJson, timeoutMs);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(module) || string.IsNullOrWhiteSpace(action))
|
||||
{
|
||||
Console.Error.WriteLine("Both --module and --action must be specified.");
|
||||
PrintHelp();
|
||||
return 1;
|
||||
}
|
||||
|
||||
return await ExecuteCommandAsync(module!, action!, payload, rawJson, timeoutMs);
|
||||
}
|
||||
catch (TimeoutException)
|
||||
{
|
||||
Console.Error.WriteLine("Timed out while communicating with the PowerToys runner.");
|
||||
return 1;
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Pipe communication failed: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static string RequireValue(string[] args, ref int index, string option)
|
||||
{
|
||||
if (index + 1 >= args.Length)
|
||||
{
|
||||
Console.Error.WriteLine($"Option {option} requires a value.");
|
||||
Environment.Exit(1);
|
||||
}
|
||||
|
||||
return args[++index];
|
||||
}
|
||||
|
||||
private static object ParseValue(string value)
|
||||
{
|
||||
if (bool.TryParse(value, out var boolValue))
|
||||
{
|
||||
return boolValue;
|
||||
}
|
||||
|
||||
if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
|
||||
{
|
||||
return intValue;
|
||||
}
|
||||
|
||||
if (double.TryParse(value, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var doubleValue))
|
||||
{
|
||||
return doubleValue;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
private static async Task<int> ExecuteCommandAsync(string module, string action, Dictionary<string, object> args, bool rawJson, int timeoutMs)
|
||||
{
|
||||
var request = new
|
||||
{
|
||||
v = 1,
|
||||
correlationId = Guid.NewGuid().ToString(),
|
||||
command = new
|
||||
{
|
||||
module,
|
||||
action,
|
||||
args,
|
||||
},
|
||||
options = new
|
||||
{
|
||||
timeoutMs,
|
||||
wantProgress = false,
|
||||
},
|
||||
};
|
||||
|
||||
string payload = JsonSerializer.Serialize(request, JsonRequestOptions);
|
||||
|
||||
using var client = new NamedPipeClientStream(
|
||||
".",
|
||||
PipeName,
|
||||
PipeDirection.InOut,
|
||||
PipeOptions.Asynchronous);
|
||||
|
||||
await client.ConnectAsync(timeoutMs).ConfigureAwait(false);
|
||||
client.ReadMode = PipeTransmissionMode.Message;
|
||||
|
||||
using (var writer = new StreamWriter(client, Encoding.UTF8, leaveOpen: true))
|
||||
{
|
||||
await writer.WriteAsync(payload).ConfigureAwait(false);
|
||||
await writer.FlushAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
client.WaitForPipeDrain();
|
||||
|
||||
var responseMessage = await ReadMessageAsync(client).ConfigureAwait(false);
|
||||
if (string.IsNullOrEmpty(responseMessage))
|
||||
{
|
||||
Console.Error.WriteLine("Received empty response from the PowerToys runner.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var document = JsonSerializer.Deserialize<JsonElement>(responseMessage);
|
||||
var status = document.TryGetProperty("status", out var statusElement) ? statusElement.GetString() : "error";
|
||||
|
||||
if (rawJson)
|
||||
{
|
||||
Console.WriteLine(JsonSerializer.Serialize(document, JsonOutputOptions));
|
||||
return string.Equals(status, "ok", StringComparison.OrdinalIgnoreCase) ? 0 : 1;
|
||||
}
|
||||
|
||||
if (string.Equals(status, "ok", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (document.TryGetProperty("result", out var resultElement))
|
||||
{
|
||||
RenderResult(resultElement);
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("Command completed successfully.");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (document.TryGetProperty("error", out var errorElement))
|
||||
{
|
||||
var code = errorElement.TryGetProperty("code", out var codeElement) ? codeElement.GetString() : "E_UNKNOWN";
|
||||
var message = errorElement.TryGetProperty("message", out var messageElement) ? messageElement.GetString() : "Command failed.";
|
||||
Console.Error.WriteLine($"{code}: {message}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.Error.WriteLine("Command failed.");
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static async Task<string?> ReadMessageAsync(NamedPipeClientStream client)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
using var reader = new StreamReader(client, Encoding.UTF8, false, bufferSize: 1024, leaveOpen: true);
|
||||
char[] buffer = new char[1024];
|
||||
int read;
|
||||
while ((read = await reader.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) > 0)
|
||||
{
|
||||
builder.Append(buffer, 0, read);
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static void RenderResult(JsonElement element)
|
||||
{
|
||||
if (element.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
if (element.TryGetProperty("modules", out var modulesElement) && modulesElement.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var module in modulesElement.EnumerateArray())
|
||||
{
|
||||
var name = module.TryGetProperty("module", out var moduleName) ? moduleName.GetString() : "<module>";
|
||||
Console.WriteLine(name);
|
||||
|
||||
if (module.TryGetProperty("commands", out var commandsElement) && commandsElement.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var command in commandsElement.EnumerateArray())
|
||||
{
|
||||
var action = command.TryGetProperty("action", out var actionElement) ? actionElement.GetString() : "<action>";
|
||||
var description = command.TryGetProperty("description", out var descriptionElement) ? descriptionElement.GetString() : string.Empty;
|
||||
Console.WriteLine($" - {action}: {description}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (element.TryGetProperty("commands", out var commands) && commands.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
var moduleName = element.TryGetProperty("module", out var moduleElement) ? moduleElement.GetString() : "<module>";
|
||||
Console.WriteLine(moduleName);
|
||||
foreach (var command in commands.EnumerateArray())
|
||||
{
|
||||
var action = command.TryGetProperty("action", out var actionElement) ? actionElement.GetString() : "<action>";
|
||||
var description = command.TryGetProperty("description", out var descriptionElement) ? descriptionElement.GetString() : string.Empty;
|
||||
Console.WriteLine($" - {action}: {description}");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine(JsonSerializer.Serialize(element, JsonOutputOptions));
|
||||
}
|
||||
|
||||
private static void PrintHelp()
|
||||
{
|
||||
Console.WriteLine("PowerToys CLI");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Usage:");
|
||||
Console.WriteLine(" ptcli --list-modules");
|
||||
Console.WriteLine(" ptcli --list-commands <module>");
|
||||
Console.WriteLine(" ptcli -m <module> -a <action> [--key value] [--json]");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Examples:");
|
||||
Console.WriteLine(" ptcli --list-modules");
|
||||
Console.WriteLine(" ptcli --list-commands awake");
|
||||
Console.WriteLine(" ptcli -m awake -a status");
|
||||
Console.WriteLine(" ptcli -m awake -a set --mode timed --duration 30m --displayOn true");
|
||||
}
|
||||
}
|
||||
}
|
||||
24
tools/ptcli/ptcli.csproj
Normal file
24
tools/ptcli/ptcli.csproj
Normal file
@@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<!-- Look at Directory.Build.props in root for common settings as well -->
|
||||
<Import Project="..\..\src\Common.SelfContained.props" />
|
||||
<Import Project="..\..\src\Common.Dotnet.CsWinRT.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<ProjectGuid>{2589570C-B068-41CA-A554-BDCAE6FC4CAC}</ProjectGuid>
|
||||
<OutputType>Exe</OutputType>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<AssemblyName>PowerToys.Cli</AssemblyName>
|
||||
<RootNamespace>PowerToys.Cli</RootNamespace>
|
||||
<OutputPath>..\..\$(Platform)\$(Configuration)\ptcli\</OutputPath>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Platform)'=='x64'">
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Platform)'=='ARM64'">
|
||||
<RuntimeIdentifier>win-arm64</RuntimeIdentifier>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user