TonieToolbox 0.5.1__py3-none-any.whl → 0.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,445 @@
1
+ #!/usr/bin/python3
2
+ """
3
+ Integration for Windows "classic" context menu.
4
+ This module generates Windows registry entries to add a 'TonieToolbox' cascade menu.
5
+ """
6
+ import os
7
+ import sys
8
+ import json
9
+ from .constants import SUPPORTED_EXTENSIONS, CONFIG_TEMPLATE, ICON_BASE64
10
+ from .artwork import base64_to_ico
11
+ from .logger import get_logger
12
+
13
+ logger = get_logger(__name__)
14
+
15
+ class WindowsClassicContextMenuIntegration:
16
+ """
17
+ Class to generate Windows registry entries for TonieToolbox "classic" context menu integration.
18
+ Adds a 'TonieToolbox' cascade menu for supported audio files, .taf files, and folders.
19
+ """
20
+ def __init__(self):
21
+ self.exe_path = os.path.join(sys.prefix, 'Scripts', 'tonietoolbox.exe')
22
+ self.exe_path_reg = self.exe_path.replace('\\', r'\\')
23
+ self.output_dir = os.path.join(os.path.expanduser('~'), '.tonietoolbox')
24
+ self.icon_path = os.path.join(self.output_dir, 'icon.ico').replace('\\', r'\\')
25
+ self.cascade_name = 'TonieToolbox'
26
+ self.entry_is_separator = '"CommandFlags"=dword:00000008'
27
+ self.show_uac = '"CommandFlags"=dword:00000010'
28
+ self.separator_below = '"CommandFlags"=dword:00000040'
29
+ self.separator_above = '"CommandFlags"=dword:00000020'
30
+ self.error_handling = r' && if %ERRORLEVEL% neq 0 (echo Error: Command failed with error code %ERRORLEVEL% && pause && exit /b %ERRORLEVEL%) else (echo Command completed successfully && ping -n 2 127.0.0.1 > nul)'
31
+ self.show_info_error_handling = r' && if %ERRORLEVEL% neq 0 (echo Error: Command failed with error code %ERRORLEVEL% && pause && exit /b %ERRORLEVEL%) else (echo. && echo Press any key to close this window... && pause > nul)'
32
+ self.config = self._apply_config_template()
33
+ self.upload_url = ''
34
+ self.log_level = self.config.get('log_level', 'SILENT')
35
+ self.log_to_file = self.config.get('log_to_file', False)
36
+ self.basic_authentication_cmd = ''
37
+ self.client_cert_cmd = ''
38
+ self.upload_enabled = self._setup_upload()
39
+
40
+ print(f"Upload enabled: {self.upload_enabled}")
41
+ print(f"Upload URL: {self.upload_url}")
42
+ print(f"Authentication: {'Basic Authentication' if self.basic_authentication else ('None' if self.none_authentication else ('Client Cert' if self.client_cert_authentication else 'Unknown'))}")
43
+
44
+ self._setup_commands()
45
+
46
+ def _build_cmd(self, base_args, file_placeholder='%1', output_to_source=True ,use_upload=False, use_artwork=False, use_json=False, use_compare=False, use_info=False, is_recursive=False, is_split=False, is_folder=False, shell='cmd.exe', keep_open=False, log_to_file=False):
47
+ """Dynamically build command strings for registry entries."""
48
+ exe = self.exe_path_reg
49
+ cmd = f'{shell} /{"k" if keep_open else "c"} "echo Running TonieToolbox'
50
+ if use_info:
51
+ cmd += ' info'
52
+ elif is_split:
53
+ cmd += ' split'
54
+ elif use_compare:
55
+ cmd += ' compare'
56
+ elif is_recursive:
57
+ cmd += ' recursive folder convert'
58
+ elif is_folder:
59
+ cmd += ' folder convert'
60
+ elif use_upload and use_artwork and use_json:
61
+ cmd += ' convert, upload, artwork and JSON'
62
+ elif use_upload and use_artwork:
63
+ cmd += ' convert, upload and artwork'
64
+ elif use_upload:
65
+ cmd += ' convert and upload'
66
+ else:
67
+ cmd += ' convert'
68
+ cmd += ' command... && "'
69
+ cmd += f'{exe}" {base_args}'
70
+ if log_to_file:
71
+ cmd += ' --log-file'
72
+ if is_recursive:
73
+ cmd += ' --recursive'
74
+ if output_to_source:
75
+ cmd += ' --output-to-source'
76
+ if use_info:
77
+ cmd += ' --info'
78
+ if is_split:
79
+ cmd += ' --split'
80
+ if use_compare:
81
+ cmd += ' --compare "%1" "%2"'
82
+ else:
83
+ cmd += f' "{file_placeholder}"'
84
+ if use_upload:
85
+ cmd += f' --upload "{self.upload_url}"'
86
+ if self.basic_authentication_cmd:
87
+ cmd += f' {self.basic_authentication_cmd}'
88
+ elif self.client_cert_cmd:
89
+ cmd += f' {self.client_cert_cmd}'
90
+ if getattr(self, "ignore_ssl_verify", False):
91
+ cmd += ' --ignore-ssl-verify'
92
+ if use_artwork:
93
+ cmd += ' --include-artwork'
94
+ if use_json:
95
+ cmd += ' --create-custom-json'
96
+ if use_info or use_compare:
97
+ cmd += ' && echo. && pause && exit > nul"'
98
+ else:
99
+ cmd += f'{self.error_handling}"'
100
+ return cmd
101
+
102
+ def _get_log_level_arg(self):
103
+ """Return the correct log level argument for TonieToolbox CLI based on self.log_level."""
104
+ level = str(self.log_level).strip().upper()
105
+ if level == 'DEBUG':
106
+ return '--debug'
107
+ elif level == 'INFO':
108
+ return '--info'
109
+ return '--silent'
110
+
111
+ def _setup_commands(self):
112
+ """Set up all command strings for registry entries dynamically."""
113
+ log_level_arg = self._get_log_level_arg()
114
+ # Audio file commands
115
+ self.convert_cmd = self._build_cmd(f'{log_level_arg}', log_to_file=self.log_to_file)
116
+ self.upload_cmd = self._build_cmd(f'{log_level_arg}', use_upload=True, log_to_file=self.log_to_file)
117
+ self.upload_artwork_cmd = self._build_cmd(f'{log_level_arg}', use_upload=True, use_artwork=True, log_to_file=self.log_to_file)
118
+ self.upload_artwork_json_cmd = self._build_cmd(f'{log_level_arg}', use_upload=True, use_artwork=True, use_json=True, log_to_file=self.log_to_file)
119
+
120
+ # .taf file commands
121
+ self.show_info_cmd = self._build_cmd(log_level_arg, use_info=True, keep_open=True, log_to_file=self.log_to_file)
122
+ self.extract_opus_cmd = self._build_cmd(log_level_arg, is_split=True, log_to_file=self.log_to_file)
123
+ self.upload_taf_cmd = self._build_cmd(log_level_arg, use_upload=True, log_to_file=self.log_to_file)
124
+ self.upload_taf_artwork_cmd = self._build_cmd(log_level_arg, use_upload=True, use_artwork=True, log_to_file=self.log_to_file)
125
+ self.upload_taf_artwork_json_cmd = self._build_cmd(log_level_arg, use_upload=True, use_artwork=True, use_json=True, log_to_file=self.log_to_file)
126
+ #self.compare_taf_cmd = self._build_cmd(log_level_arg, use_compare=True, keep_open=True, log_to_file=self.log_to_file)
127
+
128
+ # Folder commands
129
+ self.convert_folder_cmd = self._build_cmd(f'{log_level_arg}', is_recursive=True, is_folder=True, log_to_file=self.log_to_file)
130
+ self.upload_folder_cmd = self._build_cmd(f'{log_level_arg}', is_recursive=True, is_folder=True, use_upload=True, log_to_file=self.log_to_file)
131
+ self.upload_folder_artwork_cmd = self._build_cmd(f'{log_level_arg}', is_recursive=True, is_folder=True, use_upload=True, use_artwork=True, log_to_file=self.log_to_file)
132
+ self.upload_folder_artwork_json_cmd = self._build_cmd(f'{log_level_arg}', is_recursive=True, is_folder=True, use_upload=True, use_artwork=True, use_json=True, log_to_file=self.log_to_file)
133
+
134
+ def _apply_config_template(self):
135
+ """Apply the default configuration template if config.json is missing or invalid. Extracts the icon from base64 if not present."""
136
+ config_path = os.path.join(self.output_dir, 'config.json')
137
+ icon_path = os.path.join(self.output_dir, 'icon.ico')
138
+ if not os.path.exists(icon_path):
139
+ base64_to_ico(ICON_BASE64, icon_path)
140
+ if not os.path.exists(config_path):
141
+ with open(config_path, 'w') as f:
142
+ json.dump(CONFIG_TEMPLATE, f, indent=4)
143
+ logger.debug(f"Default configuration created at {config_path}")
144
+ return CONFIG_TEMPLATE
145
+ else:
146
+ logger.debug(f"Configuration file found at {config_path}")
147
+ return self._load_config()
148
+
149
+ def _load_config(self):
150
+ """Load configuration settings from config.json"""
151
+ config_path = os.path.join(self.output_dir, 'config.json')
152
+ if not os.path.exists(config_path):
153
+ raise FileNotFoundError(f"Configuration file not found: {config_path}")
154
+
155
+ with open(config_path, 'r') as f:
156
+ config = json.loads(f.read())
157
+
158
+ return config
159
+
160
+ def _setup_upload(self):
161
+ """Set up upload functionality based on config.json settings"""
162
+ # Always initialize authentication flags
163
+ self.basic_authentication = False
164
+ self.client_cert_authentication = False
165
+ self.none_authentication = False
166
+ config = self.config
167
+ try:
168
+ upload_config = config.get('upload', {})
169
+ self.upload_urls = upload_config.get('url', [])
170
+ self.ignore_ssl_verify = upload_config.get('ignore_ssl_verify', False)
171
+ self.username = upload_config.get('username', '')
172
+ self.password = upload_config.get('password', '')
173
+ self.basic_authentication_cmd = ''
174
+ self.client_cert_cmd = ''
175
+ if self.username and self.password:
176
+ self.basic_authentication_cmd = f'--username {self.username} --password {self.password}'
177
+ self.basic_authentication = True
178
+ self.client_cert_path = upload_config.get('client_cert_path', '')
179
+ self.client_cert_key_path = upload_config.get('client_cert_key_path', '')
180
+ if self.client_cert_path and self.client_cert_key_path:
181
+ self.client_cert_cmd = f'--client-cert {self.client_cert_path} --client-cert-key {self.client_cert_key_path}'
182
+ self.client_cert_authentication = True
183
+ if self.client_cert_authentication and self.basic_authentication:
184
+ logger.warning("Both client certificate and basic authentication are set. Only one can be used.")
185
+ return False
186
+ self.upload_url = self.upload_urls[0] if self.upload_urls else ''
187
+ if not self.client_cert_authentication and not self.basic_authentication and self.upload_url:
188
+ self.none_authentication = True
189
+ return bool(self.upload_url)
190
+ except FileNotFoundError:
191
+ logger.debug("Configuration file not found. Skipping upload setup.")
192
+ return False
193
+ except json.JSONDecodeError:
194
+ logger.debug("Error decoding JSON in configuration file. Skipping upload setup.")
195
+ return False
196
+ except Exception as e:
197
+ logger.debug(f"Unexpected error while loading configuration: {e}")
198
+ return False
199
+
200
+ def _reg_escape(self, s):
201
+ """Escape a string for use in a .reg file (escape double quotes)."""
202
+ return s.replace('"', '\\"')
203
+
204
+ def _generate_audio_extensions_entries(self):
205
+ """Generate registry entries for supported audio file extensions"""
206
+ reg_lines = []
207
+ for ext in SUPPORTED_EXTENSIONS:
208
+ ext = ext.lower().lstrip('.')
209
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.{ext}\\shell]')
210
+ reg_lines.append('')
211
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.{ext}\\shell\\{self.cascade_name}]')
212
+ reg_lines.append('"MUIVerb"="TonieToolbox"')
213
+ reg_lines.append(f'"Icon"="{self.icon_path}"')
214
+ reg_lines.append('"subcommands"=""')
215
+ reg_lines.append('')
216
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.{ext}\\shell\\{self.cascade_name}\\shell]')
217
+ # Convert
218
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.{ext}\\shell\\{self.cascade_name}\\shell\\a_Convert]')
219
+ reg_lines.append('@="Convert File to .taf"')
220
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.{ext}\\shell\\{self.cascade_name}\\shell\\a_Convert\\command]')
221
+ reg_lines.append(f'@="{self._reg_escape(self.convert_cmd)}"')
222
+ reg_lines.append('')
223
+ if self.upload_enabled:
224
+ # Upload
225
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.{ext}\\shell\\{self.cascade_name}\\shell\\b_Upload]')
226
+ reg_lines.append('@="Convert File to .taf and Upload"')
227
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.{ext}\\shell\\{self.cascade_name}\\shell\\b_Upload\\command]')
228
+ reg_lines.append(f'@="{self._reg_escape(self.upload_cmd)}"')
229
+ reg_lines.append('')
230
+ # Upload + Artwork
231
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.{ext}\\shell\\{self.cascade_name}\\shell\\c_UploadArtwork]')
232
+ reg_lines.append('@="Convert File to .taf and Upload + Artwork"')
233
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.{ext}\\shell\\{self.cascade_name}\\shell\\c_UploadArtwork\\command]')
234
+ reg_lines.append(f'@="{self._reg_escape(self.upload_artwork_cmd)}"')
235
+ reg_lines.append('')
236
+ # Upload + Artwork + JSON
237
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.{ext}\\shell\\{self.cascade_name}\\shell\\d_UploadArtworkJson]')
238
+ reg_lines.append('@="Convert File to .taf and Upload + Artwork + JSON"')
239
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.{ext}\\shell\\{self.cascade_name}\\shell\\d_UploadArtworkJson\\command]')
240
+ reg_lines.append(f'@="{self._reg_escape(self.upload_artwork_json_cmd)}"')
241
+ reg_lines.append('')
242
+ return reg_lines
243
+
244
+ def _generate_taf_file_entries(self):
245
+ """Generate registry entries for .taf files"""
246
+ reg_lines = []
247
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.taf\\shell]')
248
+ reg_lines.append('')
249
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.taf\\shell\\{self.cascade_name}]')
250
+ reg_lines.append('"MUIVerb"="TonieToolbox"')
251
+ reg_lines.append(f'"Icon"="{self.icon_path}"')
252
+ reg_lines.append('"subcommands"=""')
253
+ reg_lines.append('')
254
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.taf\\shell\\{self.cascade_name}\\shell]')
255
+ # Show Info
256
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.taf\\shell\\{self.cascade_name}\\shell\\a_ShowInfo]')
257
+ reg_lines.append('@="Show Info"')
258
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.taf\\shell\\{self.cascade_name}\\shell\\a_ShowInfo\\command]')
259
+ reg_lines.append(f'@="{self._reg_escape(self.show_info_cmd)}"')
260
+ reg_lines.append('')
261
+ # Extract Opus Tracks
262
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.taf\\shell\\{self.cascade_name}\\shell\\b_ExtractOpus]')
263
+ reg_lines.append('@="Extract Opus Tracks"')
264
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.taf\\shell\\{self.cascade_name}\\shell\\b_ExtractOpus\\command]')
265
+ reg_lines.append(f'@="{self._reg_escape(self.extract_opus_cmd)}"')
266
+ reg_lines.append('')
267
+ if self.upload_enabled:
268
+ # Upload
269
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.taf\\shell\\{self.cascade_name}\\shell\\c_Upload]')
270
+ reg_lines.append('@="Upload"')
271
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.taf\\shell\\{self.cascade_name}\\shell\\c_Upload\\command]')
272
+ reg_lines.append(f'@="{self._reg_escape(self.upload_taf_cmd)}"')
273
+ reg_lines.append('')
274
+ # Upload + Artwork
275
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.taf\\shell\\{self.cascade_name}\\shell\\d_UploadArtwork]')
276
+ reg_lines.append('@="Upload + Artwork"')
277
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.taf\\shell\\{self.cascade_name}\\shell\\d_UploadArtwork\\command]')
278
+ reg_lines.append(f'@="{self._reg_escape(self.upload_taf_artwork_cmd)}"')
279
+ reg_lines.append('')
280
+ # Upload + Artwork + JSON
281
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.taf\\shell\\{self.cascade_name}\\shell\\e_UploadArtworkJson]')
282
+ reg_lines.append('@="Upload + Artwork + JSON"')
283
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.taf\\shell\\{self.cascade_name}\\shell\\e_UploadArtworkJson\\command]')
284
+ reg_lines.append(f'@="{self._reg_escape(self.upload_taf_artwork_json_cmd)}"')
285
+ reg_lines.append('')
286
+ # Compare TAF Files
287
+ #reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.taf\\shell\\{self.cascade_name}\\shell\\f_CompareTaf]')
288
+ #reg_lines.append('@="Compare with another .taf file"')
289
+ #reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.taf\\shell\\{self.cascade_name}\\shell\\f_CompareTaf\\command]')
290
+ #reg_lines.append(f'@="{self._reg_escape(self.compare_taf_cmd)}"')
291
+ #reg_lines.append('')
292
+ return reg_lines
293
+
294
+ def _generate_folder_entries(self):
295
+ """Generate registry entries for folders"""
296
+ reg_lines = []
297
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\Directory\\shell]')
298
+ reg_lines.append('')
299
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\Directory\\shell\\{self.cascade_name}]')
300
+ reg_lines.append('"MUIVerb"="TonieToolbox"')
301
+ reg_lines.append(f'"Icon"="{self.icon_path}"')
302
+ reg_lines.append('"subcommands"=""')
303
+ reg_lines.append('')
304
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\Directory\\shell\\{self.cascade_name}\\shell]')
305
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\Directory\\shell\\{self.cascade_name}\\shell\\a_ConvertFolder]')
306
+ reg_lines.append('@="Convert Folder to .taf (recursive)"')
307
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\Directory\\shell\\{self.cascade_name}\\shell\\a_ConvertFolder\\command]')
308
+ reg_lines.append(f'@="{self._reg_escape(self.convert_folder_cmd)}"')
309
+ reg_lines.append('')
310
+ if self.upload_enabled:
311
+ # Upload
312
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\Directory\\shell\\{self.cascade_name}\\shell\\b_UploadFolder]')
313
+ reg_lines.append('@="Convert Folder to .taf and Upload (recursive)"')
314
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\Directory\\shell\\{self.cascade_name}\\shell\\b_UploadFolder\\command]')
315
+ reg_lines.append(f'@="{self._reg_escape(self.upload_folder_cmd)}"')
316
+ reg_lines.append('')
317
+ # Upload + Artwork
318
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\Directory\\shell\\{self.cascade_name}\\shell\\c_UploadFolderArtwork]')
319
+ reg_lines.append('@="Convert Folder to .taf and Upload + Artwork (recursive)"')
320
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\Directory\\shell\\{self.cascade_name}\\shell\\c_UploadFolderArtwork\\command]')
321
+ reg_lines.append(f'@="{self._reg_escape(self.upload_folder_artwork_cmd)}"')
322
+ reg_lines.append('')
323
+ # Upload + Artwork + JSON
324
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\Directory\\shell\\{self.cascade_name}\\shell\\d_UploadFolderArtworkJson]')
325
+ reg_lines.append('@="Convert Folder to .taf and Upload + Artwork + JSON (recursive)"')
326
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\Directory\\shell\\{self.cascade_name}\\shell\\d_UploadFolderArtworkJson\\command]')
327
+ reg_lines.append(f'@="{self._reg_escape(self.upload_folder_artwork_json_cmd)}"')
328
+ reg_lines.append('')
329
+ return reg_lines
330
+
331
+ def _generate_uninstaller_entries(self):
332
+ """Generate registry entries for uninstaller"""
333
+ unreg_lines = [
334
+ 'Windows Registry Editor Version 5.00',
335
+ '',
336
+ ]
337
+
338
+ for ext in SUPPORTED_EXTENSIONS:
339
+ ext = ext.lower().lstrip('.')
340
+ unreg_lines.append(f'[-HKEY_CLASSES_ROOT\\SystemFileAssociations\\.{ext}\\shell\\{self.cascade_name}]')
341
+ unreg_lines.append('')
342
+
343
+ unreg_lines.append(f'[-HKEY_CLASSES_ROOT\\SystemFileAssociations\\.taf\\shell\\{self.cascade_name}]')
344
+ unreg_lines.append('')
345
+ unreg_lines.append(f'[-HKEY_CLASSES_ROOT\\Directory\\shell\\{self.cascade_name}]')
346
+
347
+ return unreg_lines
348
+
349
+ def generate_registry_files(self):
350
+ """
351
+ Generate Windows registry files for TonieToolbox context menu integration.
352
+ Returns the path to the installer registry file.
353
+ """
354
+ os.makedirs(self.output_dir, exist_ok=True)
355
+
356
+ reg_lines = [
357
+ 'Windows Registry Editor Version 5.00',
358
+ '',
359
+ ]
360
+
361
+ # Add entries for audio extensions
362
+ reg_lines.extend(self._generate_audio_extensions_entries())
363
+
364
+ # Add entries for .taf files
365
+ reg_lines.extend(self._generate_taf_file_entries())
366
+
367
+ # Add entries for folders
368
+ reg_lines.extend(self._generate_folder_entries())
369
+
370
+ # Write the installer .reg file
371
+ reg_path = os.path.join(self.output_dir, 'tonietoolbox_context.reg')
372
+ with open(reg_path, 'w', encoding='utf-8') as f:
373
+ f.write('\n'.join(reg_lines))
374
+
375
+ # Generate and write the uninstaller .reg file
376
+ unreg_lines = self._generate_uninstaller_entries()
377
+ unreg_path = os.path.join(self.output_dir, 'remove_tonietoolbox_context.reg')
378
+ with open(unreg_path, 'w', encoding='utf-8') as f:
379
+ f.write('\n'.join(unreg_lines))
380
+
381
+ return reg_path
382
+
383
+ def install_registry_files(self, uninstall=False):
384
+ """
385
+ Import the generated .reg file into the Windows registry with UAC elevation.
386
+ If uninstall is True, imports the uninstaller .reg file.
387
+
388
+ Returns:
389
+ bool: True if registry import was successful, False otherwise.
390
+ """
391
+ import subprocess
392
+ reg_file = os.path.join(
393
+ self.output_dir,
394
+ 'remove_tonietoolbox_context.reg' if uninstall else 'tonietoolbox_context.reg'
395
+ )
396
+ if not os.path.exists(reg_file):
397
+ logger.error(f"Registry file not found: {reg_file}")
398
+ return False
399
+
400
+ ps_command = (
401
+ f"Start-Process reg.exe -ArgumentList @('import', '{reg_file}') -Verb RunAs -Wait -PassThru"
402
+ )
403
+ try:
404
+ result = subprocess.run(["powershell.exe", "-Command", ps_command], check=False,
405
+ capture_output=True, text=True)
406
+
407
+ if result.returncode == 0:
408
+ logger.info(f"{'Uninstallation' if uninstall else 'Installation'} registry import completed.")
409
+ return True
410
+ else:
411
+ logger.error(f"Registry import command failed with return code {result.returncode}")
412
+ logger.error(f"STDERR: {result.stderr}")
413
+ return False
414
+
415
+ except subprocess.SubprocessError as e:
416
+ logger.error(f"Failed to import registry file: {e}")
417
+ return False
418
+
419
+ @classmethod
420
+ def install(cls):
421
+ """
422
+ Generate registry files and install them with UAC elevation.
423
+ """
424
+ instance = cls()
425
+ instance.generate_registry_files()
426
+ if instance.install_registry_files(uninstall=False):
427
+ logger.info("Integration installed successfully.")
428
+ return True
429
+ else:
430
+ logger.error("Integration installation failed.")
431
+ return False
432
+
433
+ @classmethod
434
+ def uninstall(cls):
435
+ """
436
+ Generate registry files and uninstall them with UAC elevation.
437
+ """
438
+ instance = cls()
439
+ instance.generate_registry_files()
440
+ if instance.install_registry_files(uninstall=True):
441
+ logger.info("Integration uninstalled successfully.")
442
+ return True
443
+ else:
444
+ logger.error("Integration uninstallation failed.")
445
+ return False
TonieToolbox/logger.py CHANGED
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/python3
1
2
  """
2
3
  Logging configuration for the TonieToolbox package.
3
4
  """
@@ -13,7 +14,7 @@ TRACE = 5 # Custom level for ultra-verbose debugging
13
14
  logging.addLevelName(TRACE, 'TRACE')
14
15
 
15
16
  # Create a method for the TRACE level
16
- def trace(self, message, *args, **kwargs):
17
+ def trace(self: logging.Logger, message: str, *args, **kwargs) -> None:
17
18
  """Log a message with TRACE level (more detailed than DEBUG)"""
18
19
  if self.isEnabledFor(TRACE):
19
20
  self.log(TRACE, message, *args, **kwargs)
@@ -21,7 +22,7 @@ def trace(self, message, *args, **kwargs):
21
22
  # Add trace method to the Logger class
22
23
  logging.Logger.trace = trace
23
24
 
24
- def get_log_file_path():
25
+ def get_log_file_path() -> Path:
25
26
  """
26
27
  Get the path to the log file in the .tonietoolbox folder with timestamp.
27
28
 
@@ -29,7 +30,7 @@ def get_log_file_path():
29
30
  Path: Path to the log file
30
31
  """
31
32
  # Create .tonietoolbox folder in user's home directory if it doesn't exist
32
- log_dir = Path.home() / '.tonietoolbox'
33
+ log_dir = Path.home() / '.tonietoolbox' / 'logs'
33
34
  log_dir.mkdir(exist_ok=True)
34
35
 
35
36
  # Create timestamp string for the filename
@@ -40,14 +41,13 @@ def get_log_file_path():
40
41
 
41
42
  return log_file
42
43
 
43
- def setup_logging(level=logging.INFO, log_to_file=False):
44
+ def setup_logging(level: int = logging.INFO, log_to_file: bool = False) -> logging.Logger:
44
45
  """
45
46
  Set up logging configuration for the entire application.
46
47
 
47
48
  Args:
48
- level: Logging level (default: logging.INFO)
49
- log_to_file: Whether to log to a file (default: False)
50
-
49
+ level (int): Logging level (default: logging.INFO)
50
+ log_to_file (bool): Whether to log to a file (default: False)
51
51
  Returns:
52
52
  logging.Logger: Root logger instance
53
53
  """
@@ -88,13 +88,12 @@ def setup_logging(level=logging.INFO, log_to_file=False):
88
88
 
89
89
  return root_logger
90
90
 
91
- def get_logger(name):
91
+ def get_logger(name: str) -> logging.Logger:
92
92
  """
93
93
  Get a logger with the specified name.
94
94
 
95
95
  Args:
96
- name: Logger name, typically the module name
97
-
96
+ name (str): Logger name, typically the module name
98
97
  Returns:
99
98
  logging.Logger: Logger instance
100
99
  """