Skip to content

Commit 631ef69

Browse files
committed
feat: add mime types and "Open With" windows support
1 parent 9ee458e commit 631ef69

8 files changed

Lines changed: 190 additions & 8 deletions

File tree

suap.example.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,12 @@ display-name = "Roseate"
1212
# "original.png" - when there's no platform specific icon suap falls back to
1313
icons = "./assets/icons"
1414

15+
mime_types = [
16+
"image/jpeg",
17+
"image/gif",
18+
"image/png",
19+
"image/webp",
20+
]
21+
1522
[project.cargo]
1623
bin-crate = "roseate" # The name of the cargo binary crate to target

suap/commands/packaging/nsis.py

Lines changed: 104 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
# just so we can get the root path of our library
88
import suap
99

10+
from ...mime_type import MimeType
1011
from ...projects import ProjectData
1112
from ...formats import make_nsis_installer
1213

@@ -24,6 +25,7 @@ def format_config_and_make_nsis_installer(
2425
temp_folder_path: Path,
2526
display_name: Optional[str],
2627
icon_path: Path,
28+
mime_types: list[MimeType],
2729
project_data: ProjectData,
2830
):
2931
logger.debug(
@@ -36,9 +38,8 @@ def format_config_and_make_nsis_installer(
3638
temp_folder_path.mkdir(exist_ok = True)
3739
nsis_installer_script_path = temp_folder_path.joinpath("nsis_installer.nsi")
3840

39-
installer_script_template_path = Path(suap.__file__).parent.joinpath(
40-
"templates", "nsis_installer_script.nsi"
41-
)
41+
templates_path = Path(suap.__file__).parent.joinpath("templates")
42+
installer_script_template_path = templates_path.joinpath("nsis_installer_script.nsi")
4243

4344
logger.debug(
4445
f"Opening and reading NSIS template installer script at '{installer_script_template_path}'..."
@@ -50,19 +51,39 @@ def format_config_and_make_nsis_installer(
5051
logger.debug("Formatting template and creating custom install script...")
5152

5253
semver = project_data.version
54+
display_name = display_name if display_name is not None else project_data.name
5355

5456
replace_map: dict[str, str] = {
5557
"suap-binary-name": binary_name,
5658
"suap-binary-path": str(binary_path.absolute()),
5759
"suap-binary-dist-path": str(binary_dist_path.absolute()),
58-
"suap-display-name": display_name if display_name is not None else project_data.name,
60+
"suap-display-name": display_name,
5961
"suap-icon-path": str(icon_path.absolute()),
6062
"suap-icon-file-name": icon_path.name,
6163

6264
"suap-project-name": project_data.name,
6365
"suap-project-version": f"{semver.major}.{semver.minor}.{semver.patch}" \
6466
f".{semver.prerelease.split('.')[-1] if semver.prerelease is not None else 0}",
6567
"suap-project-description": project_data.description,
68+
69+
"suap-app-capabilities-macro": generate_app_capabilities_macro(
70+
mime_types,
71+
templates_path,
72+
binary_name,
73+
display_name,
74+
icon_path,
75+
project_data,
76+
uninstall = False,
77+
),
78+
"suap-app-capabilities-uni-macro": generate_app_capabilities_macro(
79+
mime_types,
80+
templates_path,
81+
binary_name,
82+
display_name,
83+
icon_path,
84+
project_data,
85+
uninstall = True,
86+
),
6687
}
6788

6889
for (suap_key, value) in replace_map.items():
@@ -78,4 +99,82 @@ def format_config_and_make_nsis_installer(
7899
if not make_nsis_installer(nsis_installer_script_path):
79100
raise Exit(1)
80101

81-
logger.info("Done making NSIS installer, installer executable should be in dist.")
102+
logger.info("Done making NSIS installer, installer executable should be in dist.")
103+
104+
def generate_app_capabilities_macro(
105+
mime_types: list[MimeType],
106+
templates_path: Path,
107+
binary_name: str,
108+
display_name: str,
109+
icon_path: Path,
110+
project_data: ProjectData,
111+
uninstall: bool,
112+
) -> str:
113+
if len(mime_types) == 0:
114+
return ""
115+
116+
app_capabilities_template_path = templates_path.joinpath(
117+
"nsis_app_capabilities_uni_macro.nsi" if uninstall else "nsis_app_capabilities_macro.nsi"
118+
)
119+
120+
with open(app_capabilities_template_path, mode = "r") as file:
121+
app_capabilities_macro_string = file.read()
122+
123+
file_associations_and_types_lines = []
124+
125+
for mime_type in mime_types:
126+
file_extension = mime_type.get_file_extension()
127+
128+
project_name = project_data.name
129+
130+
if file_extension is None:
131+
logger.warning(f"Mime type '{mime_type.mime_type_string}' not found, skipping...")
132+
continue
133+
134+
if not uninstall:
135+
file_associations_and_types_lines.append(
136+
f'WriteRegStr HKCR "Applications\\{binary_name}.exe\\SupportedTypes" "{file_extension}" ""'
137+
)
138+
139+
file_associations_and_types_lines.append(
140+
f'WriteRegStr HKCR "{file_extension}\\OpenWithProgIds" "Cloudy.{project_name}.1" ""'
141+
)
142+
143+
file_associations_and_types_lines.append(
144+
f'WriteRegStr HKLM "SOFTWARE\\Cloudy\\{project_name}\\Capabilities\\FileAssociations" "{file_extension}" "Cloudy.{project_name}.1"'
145+
)
146+
147+
else:
148+
file_associations_and_types_lines.append(
149+
f'DeleteRegValue HKCR "Applications\\{binary_name}.exe\\SupportedTypes" "{file_extension}"'
150+
)
151+
152+
file_associations_and_types_lines.append(
153+
f'DeleteRegValue HKCR "{file_extension}\\OpenWithProgIds" "Cloudy.{project_name}.1"'
154+
)
155+
156+
replace_map: dict[str, str] = {
157+
"suap-binary-name": binary_name,
158+
"suap-display-name": display_name,
159+
"suap-icon-file-name": icon_path.name,
160+
161+
"suap-project-name": project_data.name,
162+
"suap-project-description": project_data.description,
163+
164+
"suap-file-associations-and-types-macro": "\n".join(file_associations_and_types_lines),
165+
}
166+
167+
for (suap_key, value) in replace_map.items():
168+
app_capabilities_macro_string = app_capabilities_macro_string.replace(
169+
f"{{{suap_key}}}", value
170+
)
171+
172+
formatted_macro_string = "".join([
173+
f" {line}" for line in app_capabilities_macro_string.splitlines(keepends = True)
174+
])
175+
176+
logger.debug(
177+
f"Generated and formatted app capabilities macro: \n{formatted_macro_string}"
178+
)
179+
180+
return formatted_macro_string

suap/commands/packaging/package.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from .nsis import format_config_and_make_nsis_installer
1414
from .icons import get_platform_icon_path, format_icon_with_project_name
1515

16+
from ...mime_type import MimeType
1617
from ...config import get_config_data
1718
from ...project_type import ProjectType
1819
from ...platform_format import PlatformFormat, PlatformFormatOption
@@ -41,6 +42,7 @@ def package(
4142
"(e.g: 'bin-name-linux-x86_64', 'bin-name-win-x86_64-setup.exe', 'bin-name-macos-x86_64')."
4243
)
4344
] = None,
45+
remove_temp: bool = True,
4446
):
4547
platform_format: PlatformFormat = platform_format.get_platform_format()
4648

@@ -55,8 +57,12 @@ def package(
5557
if config_data is None:
5658
raise Exit(1)
5759

60+
# TODO: move this stuff to some sort of Config class
5861
display_name: Optional[str] = config_data.get("display-name", None)
5962
icons_config_path: Optional[str] = config_data.get("icons", None)
63+
mime_types: list[MimeType] = [
64+
MimeType(mime_type_string = mime_type) for mime_type in config_data.get("mime_types", [])
65+
]
6066

6167
if icons_config_path is None:
6268
logger.error(
@@ -156,10 +162,12 @@ def package(
156162
temp_folder_path = temp_folder_path,
157163
display_name = display_name,
158164
icon_path = platform_icon_path,
159-
project_data = project_data
165+
mime_types = mime_types,
166+
project_data = project_data,
160167
)
161168

162-
logger.debug("Removing temp dir...")
163-
shutil.rmtree(temp_folder_path, ignore_errors = True)
169+
if remove_temp:
170+
logger.debug("Removing temp dir...")
171+
shutil.rmtree(temp_folder_path, ignore_errors = True)
164172

165173
logger.info("This command is WIP!")

suap/config/data.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class ConfigProjectData(TypedDict):
2020
"version": int,
2121
"display-name": str,
2222
"icons": str,
23+
"mime_types": list[str],
2324
"project": ConfigProjectData
2425
}
2526
)

suap/mime_type.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from typing import Optional
2+
3+
import mimetypes
4+
from dataclasses import dataclass
5+
6+
__all__ = ()
7+
8+
@dataclass
9+
class MimeType:
10+
mime_type_string: str
11+
12+
def get_file_extension(self) -> Optional[str]:
13+
return mimetypes.guess_extension(
14+
self.mime_type_string, strict = True
15+
)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# https://learn.microsoft.com/en-us/windows/win32/shell/app-registration
2+
WriteRegStr HKCR "Applications\{suap-binary-name}.exe" "DefaultIcon" "$INSTDIR\{suap-icon-file-name},0"
3+
WriteRegStr HKCR "Applications\{suap-binary-name}.exe\shell\open" "FriendlyAppName" "{suap-display-name}"
4+
WriteRegStr HKCR "Applications\{suap-binary-name}.exe\shell\open\command" "" '"$INSTDIR\{suap-binary-name}.exe" "%1"'
5+
6+
WriteRegStr HKCR "Cloudy.{suap-project-name}.1" "" "{suap-display-name} File Associations"
7+
WriteRegStr HKCR "Cloudy.{suap-project-name}.1\DefaultIcon" "" "$INSTDIR\{suap-icon-file-name},0"
8+
WriteRegStr HKCR "Cloudy.{suap-project-name}.1\shell\open\command" "" '"$INSTDIR\{suap-binary-name}.exe" "%1"'
9+
10+
# Adds supported and associate files for "OpenWithProgIds"
11+
# and also adds "Capabilities\FileAssociations" for application
12+
{suap-file-associations-and-types-macro}
13+
14+
# Adds application to registered applications so it can show in Windows default apps list:
15+
# https://learn.microsoft.com/en-us/windows/apps/develop/launch/launch-default-apps-settings
16+
WriteRegStr HKLM "SOFTWARE\Cloudy\{suap-project-name}\Capabilities" "ApplicationName" "{suap-display-name}"
17+
WriteRegStr HKLM "SOFTWARE\Cloudy\{suap-project-name}\Capabilities" "ApplicationDescription" "{suap-project-description}"
18+
19+
# This is where it actually registers by pointing to the "Capabilities" reg
20+
WriteRegStr HKLM "SOFTWARE\RegisteredApplications" "{suap-project-name}" "SOFTWARE\Cloudy\{suap-project-name}\Capabilities"
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
DeleteRegKey HKCR "Applications\{suap-binary-name}.exe"
2+
3+
DeleteRegKey HKCR "Cloudy.{suap-project-name}.1"
4+
5+
# Removes application from "OpenWithProgIds" in supported and associate files
6+
{suap-file-associations-and-types-macro}
7+
8+
# Deletes "Capabilities" as well as "Capabilities\FileAssociations" reg
9+
DeleteRegKey HKLM "SOFTWARE\Cloudy\{suap-project-name}"
10+
11+
# Removes application from registered applications (e.g: default apps list)
12+
DeleteRegValue HKLM "SOFTWARE\RegisteredApplications" "{suap-project-name}"

suap/templates/nsis_installer_script.nsi

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ VIAddVersionKey "LegalCopyright" "© 2026 Goldy"
1414

1515
SetCompressor /SOLID Lzma
1616

17+
# Admin is required for some registries to apply, like "HKEY_LOCAL_MACHINE\Software\Cloudy".
18+
RequestExecutionLevel admin
19+
1720
# Defining some MUI specific features, like icons for the installer executables
1821
!define MUI_ICON "{suap-icon-path}"
1922
!define MUI_UNICON "{suap-icon-path}"
@@ -32,6 +35,7 @@ SetCompressor /SOLID Lzma
3235
# installer, it's where the installation
3336
# of the application and uninstaller happens.
3437
Section "MainSection"
38+
SetRegView 64
3539
SetShellVarContext all
3640

3741
# Where we place our application binary file and other files.
@@ -52,9 +56,19 @@ Section "MainSection"
5256
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\{suap-project-name}" "DisplayName" "{suap-display-name}"
5357
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\{suap-project-name}" "DisplayVersion" "{suap-project-version}"
5458
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\{suap-project-name}" "UninstallString" "$INSTDIR\uninstall.exe"
59+
60+
# Allows the application to be called from the Windows Run Dialog or Command Prompt (CMD).
61+
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\App Paths\{suap-binary-name}.exe" "" "$INSTDIR\{suap-binary-name}.exe"
62+
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\App Paths\{suap-binary-name}.exe" "Path" "$INSTDIR"
63+
64+
# Add application to "Open With" dialog if the application has mime types it supports
65+
{suap-app-capabilities-macro}
66+
67+
System::Call "shell32.dll::SHChangeNotify(i 0x08000000, i 0, i 0, i 0)"
5568
SectionEnd
5669

5770
Section "Uninstall"
71+
SetRegView 64
5872
SetShellVarContext all
5973

6074
# Where we remove our application binary file and other files.
@@ -66,4 +80,10 @@ Section "Uninstall"
6680
Delete "$SMPROGRAMS\Cloudy\{suap-project-name}.lnk"
6781

6882
DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\{suap-project-name}"
83+
84+
DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\App Paths\{suap-binary-name}.exe"
85+
86+
{suap-app-capabilities-uni-macro}
87+
88+
System::Call "shell32.dll::SHChangeNotify(i 0x08000000, i 0, i 0, i 0)"
6989
SectionEnd

0 commit comments

Comments
 (0)