diff --git a/.gitignore b/.gitignore
index 06f7035..c8f556c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,6 +7,8 @@
*.egg-info
/packaging/innosetup/.extensions
+/packaging/choco/*/*-generated
+/packaging/choco/*/*.generated.nuspec
# Caches
__pycache__
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index 810b888..67aa9fb 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -1,10 +1,11 @@
{
- "recommendations": [
- "chouzz.vscode-innosetup",
- "davidanson.vscode-markdownlint",
- "ms-python.debugpy",
- "ms-python.python",
- "ms-python.vscode-pylance",
- "charliermarsh.ruff"
- ]
+ "recommendations": [
+ "chouzz.vscode-innosetup",
+ "davidanson.vscode-markdownlint",
+ "ms-python.debugpy",
+ "ms-python.python",
+ "ms-python.vscode-pylance",
+ "charliermarsh.ruff",
+ "ms-vscode.powershell"
+ ]
}
\ No newline at end of file
diff --git a/README_choco.md b/README_choco.md
new file mode 100644
index 0000000..9bf0234
--- /dev/null
+++ b/README_choco.md
@@ -0,0 +1,34 @@
+VCF Generator Lite can convert a contact list into a single vCard file, which can be batch-imported into mobile phone
+contacts or used for other purposes.
+
+## Features
+
+- **Smart Parsing**: Batch contacts in `Name Phone Note` format (note optional), automatically handles tabs and spaces.
+- **Batch Generation**: Combines all contacts into a single `.vcf` file.
+- **Number Validation**: Automatically skips invalid numbers and quickly locates error rows.
+- **Editing Assistance**: Displays line numbers in text area, supports one-click quote removal.
+
+## Usage
+
+1. Copy the name and phone number in the format of `Name Phone Note` on each line into the text field below. The note
+ can be omitted.
+ ```text
+ Isaac Newton 13445467890 British mathematician
+ Muhammad 13554678907
+ Confucius 13645436748
+ ```
+2. Click **Generate**, select a path to save the file.
+3. You can use the generated vCard file wherever you need it.
+
+> [!NOTE]
+>
+> - You can use both tabs and spaces to separate the name and phone number.
+> - The program will automatically remove extra spaces from the text field.
+>
+> For example, ` Han Meimei 13333333333 A well-known girl` will be recognized as
+>
+>
+> > - Name: Han Meimei
+> > - Phone: 13333333333
+> > - Note: A well-known girl
+>
diff --git a/packaging/choco/installable/tools-template/LICENSE.txt b/packaging/choco/installable/tools-template/LICENSE.txt
new file mode 100644
index 0000000..97285cb
--- /dev/null
+++ b/packaging/choco/installable/tools-template/LICENSE.txt
@@ -0,0 +1,5 @@
+From: $dynamic:{license_url}
+
+LICENSE
+
+$dynamic:{license_content}
diff --git a/packaging/choco/installable/tools-template/VERIFICATION.txt b/packaging/choco/installable/tools-template/VERIFICATION.txt
new file mode 100644
index 0000000..701ef0a
--- /dev/null
+++ b/packaging/choco/installable/tools-template/VERIFICATION.txt
@@ -0,0 +1,8 @@
+VERIFICATION
+Verification is intended to assist the Chocolatey moderators and community
+in verifying that this package's contents are trustworthy.
+
+1. Download the following file from https://gitee.com/hellotool/VCFGeneratorLiteWithTkinter/releases/v$dynamic:{version_wheel}:
+ - $dynamic:{installer_filename}
+2. Use any tool to verify the file information.
+ - SHA 256: $dynamic:{installer_sha256}
\ No newline at end of file
diff --git a/packaging/choco/installable/tools-template/chocolateyinstall.ps1 b/packaging/choco/installable/tools-template/chocolateyinstall.ps1
new file mode 100644
index 0000000..73f6606
--- /dev/null
+++ b/packaging/choco/installable/tools-template/chocolateyinstall.ps1
@@ -0,0 +1,18 @@
+$ErrorActionPreference = 'Stop'
+$toolsDir = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)"
+$fileLocation = Join-Path $toolsDir '$dynamic:{installer_filename}'
+
+
+$packageArgs = @{
+ packageName = $env:ChocolateyPackageName
+ unzipLocation = $toolsDir
+ fileType = 'exe'
+ file = $fileLocation
+ checksum64 = '$dynamic:{installer_sha256}'
+ checksumType64 = 'sha256'
+ silentArgs = '/VERYSILENT /SUPPRESSMSGBOXES /NORESTART /SP-' # Inno Setup
+ validExitCodes = @(0)
+}
+
+Install-ChocolateyInstallPackage @packageArgs
+Remove-Item $toolsDir\*.exe -ea 0 -force
diff --git a/packaging/choco/installable/tools/chocolateybeforemodify.ps1 b/packaging/choco/installable/tools/chocolateybeforemodify.ps1
new file mode 100644
index 0000000..5f28270
--- /dev/null
+++ b/packaging/choco/installable/tools/chocolateybeforemodify.ps1
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packaging/choco/installable/tools/chocolateyuninstall.ps1 b/packaging/choco/installable/tools/chocolateyuninstall.ps1
new file mode 100644
index 0000000..4f39ed4
--- /dev/null
+++ b/packaging/choco/installable/tools/chocolateyuninstall.ps1
@@ -0,0 +1,44 @@
+# IMPORTANT: Before releasing this package, copy/paste the next 2 lines into PowerShell to remove all comments from this file:
+# $f='c:\path\to\thisFile.ps1'
+# gc $f | ? {$_ -notmatch "^\s*#"} | % {$_ -replace '(^.*?)\s*?[^``]#.*','$1'} | Out-File $f+".~" -en utf8; mv -fo $f+".~" $f
+
+## NOTE: In 80-90% of the cases (95% with licensed versions due to Package Synchronizer and other enhancements),
+## AutoUninstaller should be able to detect and handle registry uninstalls without a chocolateyUninstall.ps1.
+## See https://docs.chocolatey.org/en-us/choco/commands/uninstall
+## and https://docs.chocolatey.org/en-us/create/functions/uninstall-chocolateypackage
+
+## If this is an MSI, ensure 'softwareName' is appropriate, then clean up comments and you are done.
+## If this is an exe, change fileType, silentArgs, and validExitCodes
+
+$ErrorActionPreference = 'Stop' # stop on all errors
+$packageArgs = @{
+ packageName = $env:ChocolateyPackageName
+ softwareNames = @('VCF 生成器 Lite', 'VCF Generator Lite')
+ fileType = 'EXE'
+ silentArgs = '/VERYSILENT /SUPPRESSMSGBOXES /NORESTART /SP-'
+ validExitCodes = @(0)
+}
+
+$foundKeys = @()
+foreach ($softwareName in $packageArgs['softwareNames']) {
+ [array]$keys = Get-UninstallRegistryKey -SoftwareName $softwareName
+ if ($keys.Count -gt 0) {
+ $foundKeys += $keys
+ }
+}
+
+if ($foundKeys.Count -eq 1) {
+ $foundKeys | ForEach-Object {
+ $packageArgs['file'] = "$($_.UninstallString)" #NOTE: You may need to split this if it contains spaces, see below
+ Uninstall-ChocolateyPackage @packageArgs
+ }
+}
+elseif ($foundKeys.Count -eq 0) {
+ Write-Warning "$packageName has already been uninstalled by other means."
+}
+elseif ($foundKeys.Count -gt 1) {
+ Write-Warning "$($foundKeys.Count) matches found!"
+ Write-Warning "To prevent accidental data loss, no programs will be uninstalled."
+ Write-Warning "Please alert package maintainer the following keys were matched:"
+ $foundKeys | ForEach-Object { Write-Warning "- $($_.DisplayName)" }
+}
diff --git a/packaging/choco/installable/vcf-generator-lite.install.template.nuspec b/packaging/choco/installable/vcf-generator-lite.install.template.nuspec
new file mode 100644
index 0000000..76f377f
--- /dev/null
+++ b/packaging/choco/installable/vcf-generator-lite.install.template.nuspec
@@ -0,0 +1,35 @@
+
+
+
+
+
+ vcf-generator-lite.install
+ $dynamic:{version_windows}
+ $dynamic:{repository_url}
+ $dynamic:{author}
+
+
+
+ $dynamic:{display_name} (Install)
+ $dynamic:{author}
+ $dynamic:{repository_url}
+ $dynamic:{icon_url_cdn}
+ $dynamic:{copyright}
+ $dynamic:{license_url}
+ true
+ $dynamic:{repository_url}
+ $dynamic:{bug_tracker_url}
+ vcf-generator make-vcf vcf vcard-generator make-vcard vcard contacts
+ $dynamic:{summary}
+ $dynamic:{description}
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packaging/choco/meta/tools/chocolateybeforemodify.ps1 b/packaging/choco/meta/tools/chocolateybeforemodify.ps1
new file mode 100644
index 0000000..eadc55f
--- /dev/null
+++ b/packaging/choco/meta/tools/chocolateybeforemodify.ps1
@@ -0,0 +1,9 @@
+# This runs before upgrade or uninstall.
+# Use this file to do things like stop services prior to upgrade or uninstall.
+# NOTE: It is an anti-pattern to call chocolateyUninstall.ps1 from here. If you
+# need to uninstall an MSI prior to upgrade, put the functionality in this
+# file without calling the uninstall script. Make it idempotent in the
+# uninstall script so that it doesn't fail when it is already uninstalled.
+# NOTE: For upgrades - like the uninstall script, this script always runs from
+# the currently installed version, not from the new upgraded package version.
+
diff --git a/packaging/choco/meta/tools/chocolateyinstall.ps1 b/packaging/choco/meta/tools/chocolateyinstall.ps1
new file mode 100644
index 0000000..40675d1
--- /dev/null
+++ b/packaging/choco/meta/tools/chocolateyinstall.ps1
@@ -0,0 +1,136 @@
+# IMPORTANT: Before releasing this package, copy/paste the next 2 lines into PowerShell to remove all comments from this file:
+# $f='c:\path\to\thisFile.ps1'
+# gc $f | ? {$_ -notmatch "^\s*#"} | % {$_ -replace '(^.*?)\s*?[^``]#.*','$1'} | Out-File $f+".~" -en utf8; mv -fo $f+".~" $f
+
+$ErrorActionPreference = 'Stop' # stop on all errors
+$toolsDir = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)"
+# Internal packages (organizations) or software that has redistribution rights (community repo)
+# - Use `Install-ChocolateyInstallPackage` instead of `Install-ChocolateyPackage`
+# and put the binaries directly into the tools folder (we call it embedding)
+$fileLocation = Join-Path $toolsDir '{installer_filename}'
+# If embedding binaries increase total nupkg size to over 1GB, use share location or download from urls
+#$fileLocation = '\\SHARE_LOCATION\to\INSTALLER_FILE'
+# Community Repo: Use official urls for non-redist binaries or redist where total package size is over 200MB
+# Internal/Organization: Download from internal location (internet sources are unreliable)
+$url = '' # download url, HTTPS preferred
+$url64 = '' # 64bit URL here (HTTPS preferred) or remove - if installer contains both (very rare), use $url
+
+$packageArgs = @{
+ packageName = $env:ChocolateyPackageName
+ unzipLocation = $toolsDir
+ fileType = 'EXE_MSI_OR_MSU' #only one of these: exe, msi, msu
+ url = $url
+ url64bit = $url64
+ file = $fileLocation
+
+ softwareName = 'vcf-generator-lite*' #part or all of the Display Name as you see it in Programs and Features. It should be enough to be unique
+
+ # Checksums are required for packages which will be hosted on the Chocolatey Community Repository.
+ # To determine checksums, you can get that from the original site if provided.
+ # You can also use checksum.exe (choco install checksum) and use it
+ # e.g. checksum -t sha256 -f path\to\file
+ checksum = ''
+ checksumType = 'sha256' #default is md5, can also be sha1, sha256 or sha512
+ checksum64 = ''
+ checksumType64= 'sha256' #default is checksumType
+
+ # MSI
+# silentArgs = "/qn /norestart /l*v `"$($env:TEMP)\$($packageName).$($env:chocolateyPackageVersion).MsiInstall.log`"" # ALLUSERS=1 DISABLEDESKTOPSHORTCUT=1 ADDDESKTOPICON=0 ADDSTARTMENU=0
+ validExitCodes= @(0, 3010, 1641)
+ # OTHERS
+ # Uncomment matching EXE type (sorted by most to least common)
+ #silentArgs = '/S' # NSIS
+ #silentArgs = '/VERYSILENT /SUPPRESSMSGBOXES /NORESTART /SP-' # Inno Setup
+ #silentArgs = '/s' # InstallShield
+ #silentArgs = '/s /v"/qn"' # InstallShield with MSI
+ #silentArgs = '/s' # Wise InstallMaster
+ #silentArgs = '-s' # Squirrel
+ #silentArgs = '-q' # Install4j
+ #silentArgs = '-s' # Ghost
+ # Note that some installers, in addition to the silentArgs above, may also need assistance of AHK to achieve silence.
+ #silentArgs = '' # none; make silent with input macro script like AutoHotKey (AHK)
+ # https://community.chocolatey.org/packages/autohotkey.portable
+ #validExitCodes= @(0) #please insert other valid exit codes here
+}
+
+# Install-ChocolateyPackage @packageArgs # https://docs.chocolatey.org/en-us/create/functions/install-chocolateypackage
+#Install-ChocolateyZipPackage @packageArgs # https://docs.chocolatey.org/en-us/create/functions/install-chocolateyzippackage
+## If you are making your own internal packages (organizations), you can embed the installer or
+## put on internal file share and use the following instead (you'll need to add $file to the above)
+# Install-ChocolateyInstallPackage @packageArgs # https://docs.chocolatey.org/en-us/create/functions/install-chocolateyinstallpackage
+
+## Main helper functions - these have error handling tucked into them already
+## see https://docs.chocolatey.org/en-us/create/functions
+
+## Install an application, will assert administrative rights
+## - https://docs.chocolatey.org/en-us/create/functions/install-chocolateypackage
+## - https://docs.chocolatey.org/en-us/create/functions/install-chocolateyinstallpackage
+## add additional optional arguments as necessary
+##Install-ChocolateyPackage $packageName $fileType $silentArgs $url [$url64 -validExitCodes $validExitCodes -checksum $checksum -checksumType $checksumType -checksum64 $checksum64 -checksumType64 $checksumType64]
+
+## Download and unpack a zip file - https://docs.chocolatey.org/en-us/create/functions/install-chocolateyzippackage
+##Install-ChocolateyZipPackage $packageName $url $toolsDir [$url64 -checksum $checksum -checksumType $checksumType -checksum64 $checksum64 -checksumType64 $checksumType64]
+
+## Install Visual Studio Package - https://docs.chocolatey.org/en-us/create/functions/install-chocolateyvsixpackage
+#Install-ChocolateyVsixPackage $packageName $url [$vsVersion] [-checksum $checksum -checksumType $checksumType]
+#Install-ChocolateyVsixPackage @packageArgs
+
+## see the full list at https://docs.chocolatey.org/en-us/create/functions
+
+## downloader that the main helpers use to download items
+## if removing $url64, please remove from here
+## - https://docs.chocolatey.org/en-us/create/functions/get-chocolateywebfile
+#Get-ChocolateyWebFile $packageName 'DOWNLOAD_TO_FILE_FULL_PATH' $url $url64
+
+## Installer, will assert administrative rights - used by Install-ChocolateyPackage
+## use this for embedding installers in the package when not going to community feed or when you have distribution rights
+## - https://docs.chocolatey.org/en-us/create/functions/install-chocolateyinstallpackage
+#Install-ChocolateyInstallPackage $packageName $fileType $silentArgs '_FULLFILEPATH_' -validExitCodes $validExitCodes
+
+## Unzips a file to the specified location - auto overwrites existing content
+## - https://docs.chocolatey.org/en-us/create/functions/get-chocolateyunzip
+#Get-ChocolateyUnzip "FULL_LOCATION_TO_ZIP.zip" $toolsDir
+
+## Runs processes asserting UAC, will assert administrative rights - used by Install-ChocolateyInstallPackage
+## - https://docs.chocolatey.org/en-us/create/functions/start-chocolateyprocessasadmin
+#Start-ChocolateyProcessAsAdmin 'STATEMENTS_TO_RUN' 'Optional_Application_If_Not_PowerShell' -validExitCodes $validExitCodes
+
+## To avoid quoting issues, you can also assemble your -Statements in another variable and pass it in
+#$appPath = "$env:ProgramFiles\appname"
+##Will resolve to C:\Program Files\appname
+#$statementsToRun = "/C `"$appPath\bin\installservice.bat`""
+#Start-ChocolateyProcessAsAdmin $statementsToRun cmd -validExitCodes $validExitCodes
+
+## add specific folders to the path - any executables found in the chocolatey package
+## folder will already be on the path. This is used in addition to that or for cases
+## when a native installer doesn't add things to the path.
+## - https://docs.chocolatey.org/en-us/create/functions/install-chocolateypath
+#Install-ChocolateyPath 'LOCATION_TO_ADD_TO_PATH' 'User_OR_Machine' # Machine will assert administrative rights
+
+## Add specific files as shortcuts to the desktop
+## - https://docs.chocolatey.org/en-us/create/functions/install-chocolateyshortcut
+#$target = Join-Path $toolsDir "$($packageName).exe"
+# Install-ChocolateyShortcut -shortcutFilePath "" -targetPath "" [-workingDirectory "C:\" -arguments "C:\test.txt" -iconLocation "C:\test.ico" -description "This is the description"]
+
+## Outputs the bitness of the OS (either "32" or "64")
+## - https://docs.chocolatey.org/en-us/create/functions/get-osarchitecturewidth
+#$osBitness = Get-ProcessorBits
+
+## Set persistent Environment variables
+## - https://docs.chocolatey.org/en-us/create/functions/install-chocolateyenvironmentvariable
+#Install-ChocolateyEnvironmentVariable -variableName "SOMEVAR" -variableValue "value" [-variableType = 'Machine' #Defaults to 'User']
+
+## Set up a file association
+## - https://docs.chocolatey.org/en-us/create/functions/install-chocolateyfileassociation
+#Install-ChocolateyFileAssociation
+
+## Adding a shim when not automatically found - Chocolatey automatically shims exe files found in package directory.
+## - https://docs.chocolatey.org/en-us/create/functions/install-binfile
+## - https://docs.chocolatey.org/en-us/create/create-packages#how-do-i-exclude-executables-from-getting-shims
+#Install-BinFile
+
+##PORTABLE EXAMPLE
+#$toolsDir = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)"
+# despite the name "Install-ChocolateyZipPackage" this also works with 7z archives
+#Install-ChocolateyZipPackage $packageName $url $toolsDir $url64
+## END PORTABLE EXAMPLE
diff --git a/packaging/choco/meta/tools/chocolateyuninstall.ps1 b/packaging/choco/meta/tools/chocolateyuninstall.ps1
new file mode 100644
index 0000000..3d7af68
--- /dev/null
+++ b/packaging/choco/meta/tools/chocolateyuninstall.ps1
@@ -0,0 +1,78 @@
+# IMPORTANT: Before releasing this package, copy/paste the next 2 lines into PowerShell to remove all comments from this file:
+# $f='c:\path\to\thisFile.ps1'
+# gc $f | ? {$_ -notmatch "^\s*#"} | % {$_ -replace '(^.*?)\s*?[^``]#.*','$1'} | Out-File $f+".~" -en utf8; mv -fo $f+".~" $f
+
+## NOTE: In 80-90% of the cases (95% with licensed versions due to Package Synchronizer and other enhancements),
+## AutoUninstaller should be able to detect and handle registry uninstalls without a chocolateyUninstall.ps1.
+## See https://docs.chocolatey.org/en-us/choco/commands/uninstall
+## and https://docs.chocolatey.org/en-us/create/functions/uninstall-chocolateypackage
+
+## If this is an MSI, ensure 'softwareName' is appropriate, then clean up comments and you are done.
+## If this is an exe, change fileType, silentArgs, and validExitCodes
+
+$ErrorActionPreference = 'Stop' # stop on all errors
+$packageArgs = @{
+ packageName = $env:ChocolateyPackageName
+ softwareName = 'vcf-generator-lite*' #part or all of the Display Name as you see it in Programs and Features. It should be enough to be unique
+ fileType = 'EXE_MSI_OR_MSU' #only one of these: MSI or EXE (ignore MSU for now)
+ # MSI
+ silentArgs = "/qn /norestart"
+ validExitCodes= @(0, 3010, 1605, 1614, 1641) # https://msdn.microsoft.com/en-us/library/aa376931(v=vs.85).aspx
+ # OTHERS
+ # Uncomment matching EXE type (sorted by most to least common)
+ #silentArgs = '/S' # NSIS
+ #silentArgs = '/VERYSILENT /SUPPRESSMSGBOXES /NORESTART /SP-' # Inno Setup
+ #silentArgs = '/s' # InstallShield
+ #silentArgs = '/s /v"/qn"' # InstallShield with MSI
+ #silentArgs = '/s' # Wise InstallMaster
+ #silentArgs = '-s' # Squirrel
+ #silentArgs = '-q' # Install4j
+ #silentArgs = '-s -u' # Ghost
+ # Note that some installers, in addition to the silentArgs above, may also need assistance of AHK to achieve silence.
+ #silentArgs = '' # none; make silent with input macro script like AutoHotKey (AHK)
+ # https://community.chocolatey.org/packages/autohotkey.portable
+ #validExitCodes= @(0) #please insert other valid exit codes here
+}
+
+[array]$key = Get-UninstallRegistryKey -SoftwareName $packageArgs['softwareName']
+
+if ($key.Count -eq 1) {
+ $key | % {
+ $packageArgs['file'] = "$($_.UninstallString)" #NOTE: You may need to split this if it contains spaces, see below
+
+ if ($packageArgs['fileType'] -eq 'MSI') {
+ # The Product Code GUID is all that should be passed for MSI, and very
+ # FIRST, because it comes directly after /x, which is already set in the
+ # Uninstall-ChocolateyPackage msiargs (facepalm).
+ $packageArgs['silentArgs'] = "$($_.PSChildName) $($packageArgs['silentArgs'])"
+
+ # Don't pass anything for file, it is ignored for msi (facepalm number 2)
+ # Alternatively if you need to pass a path to an msi, determine that and
+ # use it instead of the above in silentArgs, still very first
+ $packageArgs['file'] = ''
+ } else {
+ # NOTES:
+ # - You probably will need to sanitize $packageArgs['file'] as it comes from the registry and could be in a variety of fun but unusable formats
+ # - Split args from exe in $packageArgs['file'] and pass those args through $packageArgs['silentArgs'] or ignore them
+ # - Ensure you don't pass double quotes in $file (aka $packageArgs['file']) - otherwise you will get "Illegal characters in path when you attempt to run this"
+ # - Review the code for auto-uninstaller for all of the fun things it does in sanitizing - https://github.com/chocolatey/choco/blob/bfe351b7d10c798014efe4bfbb100b171db25099/src/chocolatey/infrastructure.app/services/AutomaticUninstallerService.cs#L142-L192
+ }
+
+ Uninstall-ChocolateyPackage @packageArgs
+ }
+} elseif ($key.Count -eq 0) {
+ Write-Warning "$packageName has already been uninstalled by other means."
+} elseif ($key.Count -gt 1) {
+ Write-Warning "$($key.Count) matches found!"
+ Write-Warning "To prevent accidental data loss, no programs will be uninstalled."
+ Write-Warning "Please alert package maintainer the following keys were matched:"
+ $key | % {Write-Warning "- $($_.DisplayName)"}
+}
+
+## OTHER POWERSHELL FUNCTIONS
+## https://docs.chocolatey.org/en-us/create/functions
+#Uninstall-ChocolateyZipPackage $packageName # Only necessary if you did not unpack to package directory - see https://docs.chocolatey.org/en-us/create/functions/uninstall-chocolateyzippackage
+#Uninstall-ChocolateyEnvironmentVariable - https://docs.chocolatey.org/en-us/create/functions/uninstall-chocolateyenvironmentvariable
+#Uninstall-BinFile # Only needed if you used Install-BinFile - see https://docs.chocolatey.org/en-us/create/functions/uninstall-binfile
+## Remove any shortcuts you added in the install script.
+
diff --git a/packaging/choco/meta/vcf-generator-lite.template.nuspec b/packaging/choco/meta/vcf-generator-lite.template.nuspec
new file mode 100644
index 0000000..bba6bdb
--- /dev/null
+++ b/packaging/choco/meta/vcf-generator-lite.template.nuspec
@@ -0,0 +1,36 @@
+
+
+
+
+
+ vcf-generator-lite
+ $dynamic:{version_windows}
+ $dynamic:{repository_url}
+ $dynamic:{author}
+
+
+
+ $dynamic:{display_name}
+ $dynamic:{author}
+ $dynamic:{repository_url}
+ $dynamic:{icon_url_cdn}
+ $dynamic:{copyright}
+ $dynamic:{license_url}
+ true
+ $dynamic:{repository_url}
+ $dynamic:{bug_tracker_url}
+ vcf-generator make-vcf vcf vcard-generator make-vcard vcard contacts
+ $dynamic:{summary}
+
+ $dynamic:{release_notes}
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index e2ccce7..4c462f1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -46,6 +46,7 @@ format = "ruff format"
build-installer.script = "scripts.build_app:build_installer"
build-portable.script = "scripts.build_app:build_portable"
build-zipapp.script = "scripts.build_app:build_zipapp"
+build-chocopkgs.script = "scripts.build_app:build_chocolatey_packages"
build-wheel = "uv build --wheel"
[tool.uv]
diff --git a/scripts/build_app/__init__.py b/scripts/build_app/__init__.py
index 39f1fde..ff38247 100644
--- a/scripts/build_app/__init__.py
+++ b/scripts/build_app/__init__.py
@@ -1,3 +1,4 @@
+from scripts.build_app.chocolatey import build_chocolatey_packages as build_chocolatey_packages
from scripts.build_app.innosetup import build_installer as build_installer
from scripts.build_app.portable import build_portable as build_portable
from scripts.build_app.wheel import build_wheel as build_wheel
diff --git a/scripts/build_app/chocolatey.py b/scripts/build_app/chocolatey.py
new file mode 100644
index 0000000..bb0618d
--- /dev/null
+++ b/scripts/build_app/chocolatey.py
@@ -0,0 +1,130 @@
+import hashlib
+import shutil
+import subprocess
+from collections.abc import Mapping
+from contextlib import contextmanager
+from pathlib import Path
+from string import Template
+
+from scripts.app_metadata import app_metadata, app_version_variants
+from scripts.build_app.common import PATH_DIST, PATH_PACKAGING, require_external_tool
+from scripts.build_app.innosetup import PATH_DIST_INSTALLER, ensure_installer_dist
+
+PATH_PACKAGING_CHOCOLATEY = PATH_PACKAGING.joinpath("choco")
+
+
+class ChocolateyTemplate(Template):
+ delimiter = "$dynamic:"
+
+
+def fill_template_file(
+ template_file: Path,
+ generated_nuspec: Path,
+ template_filler: Mapping[str, str],
+ *,
+ encoding: str = "utf-8",
+):
+ with template_file.open(encoding=encoding) as f:
+ content = f.read()
+
+ try:
+ formatted_content = ChocolateyTemplate(content).substitute(template_filler)
+ except KeyError as e:
+ raise KeyError(f"Missing template key in {template_file}") from e
+ except ValueError as e:
+ raise ValueError(f"Invalid template {template_file}") from e
+
+ with generated_nuspec.open(mode="w", encoding=encoding) as f:
+ f.write(formatted_content)
+
+
+def file_sha256(file_path: Path, block_size: int = 65536):
+ sha256_hash = hashlib.sha256()
+ with file_path.open("rb") as f:
+ for chunk in iter(lambda: f.read(block_size), b""):
+ sha256_hash.update(chunk)
+ return sha256_hash.hexdigest()
+
+
+def get_chocolatey_template_filler() -> Mapping[str, str]:
+ with Path("LICENSE").open(encoding="utf8") as license_file:
+ license_content = license_file.read()
+ with Path("README_choco.md").open(encoding="utf8") as readme_file:
+ readme_content = readme_file.read()
+
+ return {
+ "display_name": app_metadata.display_name,
+ "version_windows": app_version_variants.windows,
+ "version_wheel": app_version_variants.wheel,
+ "license_url": "https://gitee.com/hellotool/VCFGeneratorLiteWithTkinter/blob/master/LICENSE",
+ "license_content": license_content,
+ "repository_url": app_metadata.repository or "",
+ "bug_tracker_url": app_metadata.bug_tracker or "",
+ "release_url": app_metadata.bug_tracker or "",
+ "author": app_metadata.author or "",
+ "icon_url_cdn": "https://cdn.jsdelivr.net/gh/hellotool/VCFGeneratorLiteWithTkinter@f1f5dac3e98e9a375bc68951cade995d611bb094/assets/images/icon.svg",
+ "summary": app_metadata.summary or "",
+ "description": readme_content,
+ "release_notes": app_metadata.release_notes or "",
+ "copyright": app_metadata.copyright or "",
+ "installer_sha256": file_sha256(PATH_DIST_INSTALLER),
+ "installer_filename": PATH_DIST_INSTALLER.name,
+ }
+
+
+def get_chocolatey_template_encoding(path: Path) -> str:
+ if path.name.endswith(".ps1") or path.name == "LICENSE.txt":
+ return "utf-8-sig"
+ return "utf-8"
+
+
+@contextmanager
+def prepare_chocolatey_package(template_filler: Mapping[str, str], package_dir: Path):
+ tools_template_dir = package_dir / "tools-template"
+ tools_generated_dir = package_dir / "tools-generated"
+ template_nuspec_path = next(package_dir.glob("*.template.nuspec"))
+ generated_nuspec_path = template_nuspec_path.with_stem(
+ template_nuspec_path.stem[: -len(".template")] + ".generated"
+ )
+
+ if tools_generated_dir.exists():
+ shutil.rmtree(tools_generated_dir)
+
+ if tools_template_dir.exists() and tools_template_dir.is_dir():
+ shutil.copytree(tools_template_dir, tools_generated_dir)
+
+ for file_path in tools_generated_dir.rglob("*"):
+ if not file_path.is_file():
+ continue
+
+ fill_template_file(
+ file_path,
+ file_path,
+ template_filler,
+ encoding=get_chocolatey_template_encoding(file_path),
+ )
+ else:
+ tools_generated_dir.mkdir(exist_ok=True)
+
+ if generated_nuspec_path.is_file():
+ generated_nuspec_path.unlink()
+
+ fill_template_file(template_nuspec_path, generated_nuspec_path, template_filler)
+
+ yield generated_nuspec_path
+
+ shutil.rmtree(tools_generated_dir)
+ generated_nuspec_path.unlink()
+
+
+def pack_with_chocolatey(spec_path: Path):
+ choco_path = require_external_tool("choco", "Chocolatey")
+ subprocess.run([choco_path, "pack", spec_path, "--output-directory", PATH_DIST.absolute()], check=True) # noqa: S603
+
+
+def build_chocolatey_packages():
+ ensure_installer_dist()
+ template_filler: Mapping[str, str] = get_chocolatey_template_filler()
+ for package_dir in PATH_PACKAGING_CHOCOLATEY.iterdir():
+ with prepare_chocolatey_package(template_filler, package_dir) as nuspec_file_path:
+ pack_with_chocolatey(nuspec_file_path)