matchpatch 0.4.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.
matchpatch/progress.py ADDED
@@ -0,0 +1,39 @@
1
+ """Structured progress events shared by CLI workers and the GUI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import asdict, dataclass
7
+ from typing import Any
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class ProgressEvent:
12
+ kind: str
13
+ message: str | None = None
14
+ phase: str | None = None
15
+ preset_id: int | None = None
16
+ device_patch: str | None = None
17
+ preset_index: int | None = None
18
+ preset_total: int | None = None
19
+ snapshot: int | None = None
20
+ snapshot_total: int | None = None
21
+ reference_lufs: float | None = None
22
+ lufs: float | None = None
23
+ crest_factor_db: float | None = None
24
+ path: str | None = None
25
+
26
+ def to_json(self) -> str:
27
+ return json.dumps(asdict(self), separators=(",", ":"))
28
+
29
+ @classmethod
30
+ def from_json(cls, value: str) -> ProgressEvent:
31
+ payload: Any = json.loads(value)
32
+
33
+ if not isinstance(payload, dict):
34
+ raise ValueError("Progress event must be a JSON object")
35
+
36
+ try:
37
+ return cls(**payload)
38
+ except TypeError as exc:
39
+ raise ValueError(f"Invalid progress event: {exc}") from exc
matchpatch/workflow.py ADDED
@@ -0,0 +1,361 @@
1
+ """Reusable preset-normalization workflow shared by CLI and GUI front ends."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import csv
6
+ import shutil
7
+ import tempfile
8
+ from collections.abc import Callable
9
+ from dataclasses import dataclass
10
+ from dataclasses import replace as dataclass_replace
11
+ from pathlib import Path
12
+
13
+ from matchpatch.analysis import AnalysisOptions
14
+ from matchpatch.custom_adjustments import load_custom_adjustments_file
15
+ from matchpatch.devices import get_device_profile
16
+ from matchpatch.devices.base import (
17
+ DeviceProfile,
18
+ NormalizationPolicy,
19
+ PatchFileAdjustments,
20
+ validate_snapshot_count,
21
+ )
22
+ from matchpatch.progress import ProgressEvent
23
+
24
+ PROJECT_DIR = Path(__file__).resolve().parents[2]
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class ImportRequest:
29
+ kind: str
30
+ device_display_name: str
31
+ path: Path
32
+
33
+ @property
34
+ def message(self) -> str:
35
+ description = "measurement" if self.kind == "measurement" else "adjusted"
36
+ return (
37
+ f"Please import this {description} file into {self.device_display_name}:\n{self.path}"
38
+ )
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class NormalizationRequest:
43
+ device: str
44
+ input_path: Path
45
+ backend: str
46
+ windows_python: str
47
+ reference_di: Path
48
+ custom_adjustments_path: Path | None = None
49
+ output_path: Path | None = None
50
+ diff_input_path: Path | None = None
51
+ automation: bool = True
52
+ defer_export: bool = False
53
+ preset_set: str | None = None
54
+ limit: int | None = None
55
+ keep_temp: bool = False
56
+ ignore_bad_lufs: bool = True
57
+ target_lufs: float = -16.0
58
+ timeout: float | None = None
59
+ audio_device: str | int | None = None
60
+ sample_rate: int | None = None
61
+ input_mapping: str | None = None
62
+ output_mapping: str | None = None
63
+ blocksize: int | None = None
64
+ steering_output: str | None = None
65
+ steering_channel: int | None = None
66
+ preset_wait: float | None = None
67
+ snapshot_wait: float | None = None
68
+ measurement_wait: float | None = None
69
+ pre_roll: float | None = None
70
+ post_roll: float | None = None
71
+ round_trip_latency: float | None = None
72
+ simulate_fail_presets: str | None = None
73
+ play_recorded_output: bool = False
74
+ record_device_output: bool = False
75
+ playback_toggle_path: Path | None = None
76
+ recorded_output_dir: Path | None = None
77
+ snapshot_plan: tuple[tuple[str, tuple[int, ...]], ...] = ()
78
+ policy: NormalizationPolicy = NormalizationPolicy()
79
+ analysis_options: AnalysisOptions = AnalysisOptions()
80
+
81
+
82
+ @dataclass(frozen=True)
83
+ class NormalizationResult:
84
+ output_path: Path | None
85
+ temp_dir: Path | None
86
+ retained_csv_path: Path | None = None
87
+
88
+
89
+ ProgressCallback = Callable[[ProgressEvent], None]
90
+ ConfirmationCallback = Callable[[ImportRequest], bool]
91
+ AnalysisRunner = Callable[[NormalizationRequest, list[int], Path, ProgressCallback | None], None]
92
+ ProfileProvider = Callable[[str], DeviceProfile]
93
+ TempDirFactory = Callable[[], Path]
94
+
95
+
96
+ def normalize_presets(
97
+ request: NormalizationRequest,
98
+ *,
99
+ run_analysis: AnalysisRunner,
100
+ on_progress: ProgressCallback | None = None,
101
+ confirm_import: ConfirmationCallback | None = None,
102
+ get_profile: ProfileProvider = get_device_profile,
103
+ make_temp_dir: TempDirFactory | None = None,
104
+ ) -> NormalizationResult:
105
+ profile = get_profile(request.device)
106
+ validate_snapshot_count(profile, request.policy.snapshot_count)
107
+ handler = profile.create_patch_file_handler(PROJECT_DIR)
108
+ log_setter = getattr(handler, "set_log_callback", None)
109
+ if log_setter is not None:
110
+ log_setter(lambda message: _emit(on_progress, ProgressEvent("log", message=message)))
111
+ input_path = request.input_path.resolve()
112
+ handler.validate_input(input_path)
113
+
114
+ if not request.reference_di.is_file():
115
+ raise ValueError(f"Reference DI WAV does not exist: {request.reference_di}")
116
+ _validate_custom_adjustments(request)
117
+
118
+ if request.automation:
119
+ if request.output_path is not None:
120
+ raise ValueError("--output must not be specified with --automation")
121
+
122
+ measurement_path = handler.automation_output_path(input_path, "_measurement")
123
+ output_path = (
124
+ None
125
+ if request.defer_export
126
+ else handler.automation_output_path(input_path, "_adjusted")
127
+ )
128
+ _emit(on_progress, ProgressEvent("phase", phase="preparing_measurement"))
129
+ handler.create_measurement_file(input_path, measurement_path)
130
+ _emit(on_progress, ProgressEvent("phase", phase="waiting_for_measurement_import"))
131
+ _confirm(
132
+ confirm_import,
133
+ ImportRequest("measurement", profile.display_name, measurement_path),
134
+ )
135
+ else:
136
+ if request.output_path is None:
137
+ raise ValueError("--output is required unless --automation is used")
138
+
139
+ output_path = request.output_path.resolve()
140
+ handler.validate_output(input_path, output_path)
141
+
142
+ requested_ids = (
143
+ handler.parse_patch_set(request.preset_set) if request.preset_set is not None else None
144
+ )
145
+ assignments = handler.list_assignments(input_path)
146
+ preset_ids = handler.select_preset_ids(input_path, assignments, requested_ids)
147
+
148
+ snapshot_plan = request.snapshot_plan
149
+ if request.diff_input_path is not None:
150
+ previous_input_path = request.diff_input_path.resolve()
151
+ if previous_input_path.suffix.lower() != input_path.suffix.lower():
152
+ raise ValueError("--diff-input must use the same file type as --input")
153
+ diff_snapshot_ids = getattr(handler, "diff_snapshot_ids", None)
154
+ if diff_snapshot_ids is None:
155
+ diff_snapshots = {
156
+ preset_id: tuple(range(1, request.policy.snapshot_count + 1))
157
+ for preset_id in handler.diff_preset_ids(input_path, previous_input_path)
158
+ }
159
+ else:
160
+ diff_snapshots = diff_snapshot_ids(
161
+ input_path,
162
+ previous_input_path,
163
+ request.policy.snapshot_count,
164
+ )
165
+ diff_ids = set(diff_snapshots)
166
+ preset_ids = [preset_id for preset_id in preset_ids if preset_id in diff_ids]
167
+ snapshot_plan = _intersect_snapshot_plans(
168
+ snapshot_plan,
169
+ tuple(
170
+ (handler.format_patch_id(preset_id), diff_snapshots[preset_id])
171
+ for preset_id in preset_ids
172
+ ),
173
+ )
174
+
175
+ if request.limit is not None:
176
+ if request.limit < 1:
177
+ raise ValueError("--limit must be at least 1")
178
+
179
+ preset_ids = preset_ids[: request.limit]
180
+
181
+ if not preset_ids:
182
+ raise ValueError("Patch file contains no measurable presets")
183
+
184
+ temp_dir = (
185
+ make_temp_dir()
186
+ if make_temp_dir is not None
187
+ else Path(tempfile.mkdtemp(prefix="matchpatch_normalization_", dir=PROJECT_DIR))
188
+ )
189
+ success = False
190
+
191
+ try:
192
+ csv_path = temp_dir / "lufs_analysis.csv"
193
+ analysis_request = (
194
+ request
195
+ if not request.record_device_output or request.recorded_output_dir is not None
196
+ else dataclass_replace(request, recorded_output_dir=temp_dir / "recordings")
197
+ )
198
+ if snapshot_plan != analysis_request.snapshot_plan:
199
+ analysis_request = dataclass_replace(analysis_request, snapshot_plan=snapshot_plan)
200
+ _emit(
201
+ on_progress,
202
+ ProgressEvent(
203
+ "phase",
204
+ phase="measuring",
205
+ message="Starting measurement worker...",
206
+ preset_total=len(preset_ids),
207
+ snapshot_total=request.policy.snapshot_count,
208
+ ),
209
+ )
210
+ run_analysis(analysis_request, preset_ids, csv_path, on_progress)
211
+
212
+ measured_rows = _count_csv_rows(csv_path)
213
+
214
+ if measured_rows != len(preset_ids):
215
+ raise RuntimeError(
216
+ f"Windows analysis wrote {measured_rows} rows for {len(preset_ids)} presets"
217
+ )
218
+
219
+ if output_path is not None:
220
+ _emit(
221
+ on_progress,
222
+ ProgressEvent("phase", phase="applying", message="Applying adjustments"),
223
+ )
224
+ handler.apply_analysis_csv(
225
+ input_path,
226
+ output_path,
227
+ csv_path,
228
+ request.ignore_bad_lufs,
229
+ request.target_lufs,
230
+ request.policy,
231
+ request.custom_adjustments_path,
232
+ )
233
+ elif request.defer_export:
234
+ preview_path = temp_dir / f"{input_path.stem}_preview{input_path.suffix}"
235
+ _emit(
236
+ on_progress,
237
+ ProgressEvent("phase", phase="applying", message="Calculating adjustments"),
238
+ )
239
+ try:
240
+ handler.apply_analysis_csv(
241
+ input_path,
242
+ preview_path,
243
+ csv_path,
244
+ request.ignore_bad_lufs,
245
+ request.target_lufs,
246
+ request.policy,
247
+ request.custom_adjustments_path,
248
+ )
249
+ finally:
250
+ preview_path.unlink(missing_ok=True)
251
+ success = True
252
+ finally:
253
+ if not request.keep_temp and success and not request.defer_export:
254
+ shutil.rmtree(temp_dir, ignore_errors=True)
255
+ elif not request.defer_export:
256
+ _emit(
257
+ on_progress,
258
+ ProgressEvent(
259
+ "temp_retained",
260
+ message=f"Kept temporary CSV: {csv_path}",
261
+ path=str(csv_path),
262
+ ),
263
+ )
264
+
265
+ _emit(
266
+ on_progress,
267
+ ProgressEvent(
268
+ "phase",
269
+ phase="completed",
270
+ message=(
271
+ "Measurement completed; ready to export"
272
+ if request.defer_export
273
+ else "Gain-adjusted patch file written"
274
+ ),
275
+ ),
276
+ )
277
+
278
+ if request.automation and output_path is not None:
279
+ _emit(on_progress, ProgressEvent("phase", phase="waiting_for_adjusted_import"))
280
+ _confirm(
281
+ confirm_import,
282
+ ImportRequest("adjusted", profile.display_name, output_path),
283
+ )
284
+
285
+ return NormalizationResult(
286
+ output_path,
287
+ temp_dir if request.keep_temp or not success or request.defer_export else None,
288
+ csv_path if request.keep_temp or not success or request.defer_export else None,
289
+ )
290
+
291
+
292
+ def export_adjusted_file(
293
+ request: NormalizationRequest,
294
+ csv_path: Path,
295
+ output_path: Path,
296
+ *,
297
+ adjustments: PatchFileAdjustments | None = None,
298
+ on_progress: ProgressCallback | None = None,
299
+ get_profile: ProfileProvider = get_device_profile,
300
+ ) -> None:
301
+ profile = get_profile(request.device)
302
+ handler = profile.create_patch_file_handler(PROJECT_DIR)
303
+ log_setter = getattr(handler, "set_log_callback", None)
304
+ if log_setter is not None:
305
+ log_setter(lambda message: _emit(on_progress, ProgressEvent("log", message=message)))
306
+ input_path = request.input_path.resolve()
307
+ output_path = output_path.resolve()
308
+ handler.validate_input(input_path)
309
+ handler.validate_output(input_path, output_path)
310
+ _validate_custom_adjustments(request)
311
+ handler.apply_analysis_csv(
312
+ input_path,
313
+ output_path,
314
+ csv_path,
315
+ request.ignore_bad_lufs,
316
+ request.target_lufs,
317
+ request.policy,
318
+ request.custom_adjustments_path,
319
+ adjustments,
320
+ )
321
+
322
+
323
+ def _validate_custom_adjustments(request: NormalizationRequest) -> None:
324
+ if request.custom_adjustments_path is None:
325
+ return
326
+ if not request.custom_adjustments_path.is_file():
327
+ raise ValueError(
328
+ f"Custom adjustments CSV does not exist: {request.custom_adjustments_path}"
329
+ )
330
+ load_custom_adjustments_file(request.custom_adjustments_path, request.policy.snapshot_count)
331
+
332
+
333
+ def _intersect_snapshot_plans(
334
+ first: tuple[tuple[str, tuple[int, ...]], ...],
335
+ second: tuple[tuple[str, tuple[int, ...]], ...],
336
+ ) -> tuple[tuple[str, tuple[int, ...]], ...]:
337
+ if not first:
338
+ return second
339
+ first_by_patch = {patch.upper(): tuple(snapshots) for patch, snapshots in first}
340
+ result = []
341
+ for patch, snapshots in second:
342
+ first_snapshots = first_by_patch.get(patch.upper(), ())
343
+ selected = tuple(snapshot for snapshot in snapshots if snapshot in first_snapshots)
344
+ if selected:
345
+ result.append((patch, selected))
346
+ return tuple(result)
347
+
348
+
349
+ def _emit(callback: ProgressCallback | None, event: ProgressEvent) -> None:
350
+ if callback is not None:
351
+ callback(event)
352
+
353
+
354
+ def _confirm(callback: ConfirmationCallback | None, request: ImportRequest) -> None:
355
+ if callback is not None and not callback(request):
356
+ raise RuntimeError("Normalization cancelled by user")
357
+
358
+
359
+ def _count_csv_rows(csv_path: Path) -> int:
360
+ with csv_path.open("r", encoding="utf-8-sig", newline="") as csv_file:
361
+ return sum(1 for _ in csv.DictReader(csv_file))
@@ -0,0 +1,134 @@
1
+ Metadata-Version: 2.4
2
+ Name: matchpatch
3
+ Version: 0.4.0
4
+ Summary: Audio processor preset gain normalization and measurement tools
5
+ Project-URL: Homepage, https://github.com/noseglasses/MatchPatch
6
+ Project-URL: Issues, https://github.com/noseglasses/MatchPatch/issues
7
+ Project-URL: Repository, https://github.com/noseglasses/MatchPatch.git
8
+ License-File: LICENSE
9
+ Requires-Python: <3.15,>=3.12
10
+ Provides-Extra: gui
11
+ Requires-Dist: pyside6>=6.8; extra == 'gui'
12
+ Description-Content-Type: text/markdown
13
+
14
+ # MatchPatch
15
+
16
+ <p align="center">
17
+ <img src="docs/assets/matchmatch-logo.png" alt="MatchPatch: Normalize presets. Match volume." width="260">
18
+ </p>
19
+
20
+ [![Quality](https://github.com/noseglasses/MatchPatch/actions/workflows/quality.yml/badge.svg)](https://github.com/noseglasses/MatchPatch/actions/workflows/quality.yml)
21
+ [![Release](https://github.com/noseglasses/MatchPatch/actions/workflows/release.yml/badge.svg)](https://github.com/noseglasses/MatchPatch/actions/workflows/release.yml)
22
+ [![PyPI](https://img.shields.io/pypi/v/matchpatch.svg)](https://pypi.org/project/matchpatch/)
23
+ [![Python](https://img.shields.io/pypi/pyversions/matchpatch.svg)](https://pypi.org/project/matchpatch/)
24
+
25
+ <p align="center">
26
+ <strong><a href="https://youtu.be/Dw1Kez0AnCk">Watch the demo video</a></strong>
27
+ ·
28
+ <strong><a href="https://noseglasses.github.io/MatchPatch/">Read the documentation</a></strong>
29
+ ·
30
+ <strong><a href="https://github.com/noseglasses/MatchPatch/releases/latest">Download MatchPatch</a></strong>
31
+ </p>
32
+
33
+ ![MatchPatch GUI showing a loaded Helix setlist](docs/assets/screenshots/normalization-ongoing.png)
34
+
35
+ **No more unexpected volume jumps when switching sounds.**
36
+
37
+ MatchPatch automatically equalizes the loudness of presets and snapshots in
38
+ guitar processors such as the Line 6 Helix.
39
+
40
+ ## Why MatchPatch?
41
+
42
+ You create a great clean sound. You create a great lead sound. Then you switch
43
+ between them and one is much louder than the other.
44
+
45
+ MatchPatch measures your presets and calculates the gain adjustments needed to
46
+ make them consistent, so your setlist feels balanced before rehearsal or stage
47
+ use.
48
+
49
+ ## Features
50
+
51
+ - Measure preset loudness automatically.
52
+ - Analyze snapshots.
53
+ - Calculate required gain corrections.
54
+ - Modify Helix setlists and presets.
55
+ - Test the workflow without hardware.
56
+ - Configure normal runs from the GUI.
57
+ - Use CLI and worker commands for advanced scripting.
58
+ - Open source.
59
+
60
+ ## Current Support
61
+
62
+ Current normal workflows support:
63
+
64
+ - Line 6 Helix
65
+ - `.hls` Helix setlists
66
+ - `.hlx` Helix presets
67
+ - GUI-first workflows
68
+
69
+ Loopback and simulated modes are available for no-hardware tests. Hardware mode
70
+ is for real Helix measurement.
71
+
72
+ ## Documentation
73
+
74
+ - Online manual: [noseglasses.github.io/MatchPatch](https://noseglasses.github.io/MatchPatch/)
75
+ - Start here: [docs/index.md](docs/index.md)
76
+ - 10-minute guide: [docs/quick-start.md](docs/quick-start.md)
77
+ - Main manual: [docs/musician-guide.md](docs/musician-guide.md)
78
+ - Test without hardware: [docs/workflows/test-without-hardware.md](docs/workflows/test-without-hardware.md)
79
+ - Hardware measurement: [docs/workflows/hardware-measurement.md](docs/workflows/hardware-measurement.md)
80
+ - Reference DI: [docs/concepts/reference-di.md](docs/concepts/reference-di.md)
81
+ - Troubleshooting: [docs/troubleshooting.md](docs/troubleshooting.md)
82
+ - FAQ: [docs/faq.md](docs/faq.md)
83
+ - Glossary: [docs/glossary.md](docs/glossary.md)
84
+
85
+ ## Safety Notes
86
+
87
+ > Warning:
88
+ > Keep backups of your original Helix files.
89
+
90
+ > Warning:
91
+ > Measurement files are for measuring, not for live playing.
92
+
93
+ ## Install And Launch
94
+
95
+ On Windows, download the latest installer from
96
+ [GitHub Releases](https://github.com/noseglasses/MatchPatch/releases/latest),
97
+ run `MatchPatch-Setup-<version>.exe`, then launch MatchPatch from the Start
98
+ Menu. The installed app bundles offline Help, available from the GUI.
99
+
100
+ For source checkouts, use the verified local setup commands below and see
101
+ [Developer Notes](docs/developer-notes.md) and
102
+ [developer commands](docs/dev/commands.md) for fuller setup details.
103
+
104
+ Install the optional GUI support and launch MatchPatch:
105
+
106
+ ```bash
107
+ # Linux or WSL
108
+ scripts/sync-wsl.sh --extra gui
109
+ matchpatch-gui
110
+ ```
111
+
112
+ ```powershell
113
+ # Windows PowerShell
114
+ cd C:\src\MatchPatch-windows
115
+ .\scripts\sync-windows.cmd --extra gui
116
+ .\.venv-windows\Scripts\matchpatch-gui.exe
117
+ ```
118
+
119
+ Hardware measurement from WSL needs a synced native Windows runtime. See
120
+ [developer commands](docs/dev/commands.md) before using real Helix hardware from
121
+ WSL.
122
+
123
+ ## Advanced And Developer Information
124
+
125
+ Technical details live in the developer docs:
126
+
127
+ - [Developer Notes](docs/developer-notes.md)
128
+ - [Architecture](docs/dev/architecture.md)
129
+ - [Commands](docs/dev/commands.md)
130
+ - [File Formats](docs/dev/file-formats.md)
131
+
132
+ ## License
133
+
134
+ MatchPatch is open source software released under the [MIT License](LICENSE).
@@ -0,0 +1,29 @@
1
+ matchpatch/__init__.py,sha256=F9nWOCr_UM22K_V3SBR-iOBKKFs1K1eD5sGbxY38heI,610
2
+ matchpatch/analysis.py,sha256=5965dl0UKoZXCfp1o-LQ_EHIglUynZGe0Sfqx9BYrzY,2776
3
+ matchpatch/app.py,sha256=_hqmVrXQ5-CnG3NPQCib8AkMfZJlHJWurMarWnl9Fzw,1950
4
+ matchpatch/audio.py,sha256=anP23VmmlP5V6AJzSVCbGHdD48Evtmf0ZgYFC-Pq_yA,4885
5
+ matchpatch/cli.py,sha256=UIfgN581QHLoV4pIlIa2aP5Ihzp2Vg0LpmD5qwGY4S4,2043
6
+ matchpatch/config.py,sha256=YjflYuoUfTXD0EpR6tYzHkD61JWQk6FPdA0WWdStMJ4,5992
7
+ matchpatch/custom_adjustments.py,sha256=9vrPBCK8zbAP9vSAudhYCQwkttIg9urUtkA_OasqMDc,2312
8
+ matchpatch/measure.py,sha256=c3gJFFBoiMqR6eO-BktYMnEUCAgcqkgX1p6qhMZNBSk,44255
9
+ matchpatch/measurement_optimizer.py,sha256=3CgDKCWEKRbnsP6lsdxlKaRTPV0bUDh29-w3mIBYSvo,22785
10
+ matchpatch/normalize.py,sha256=ZxoEYGHwlG4g4pVyEJ5HqeonTj397huRUkyMqC1x4wI,30139
11
+ matchpatch/progress.py,sha256=uMxTHpE5BBKP_jFdJU3-dsnDxnfMDu21GV0NKlVtsTE,1124
12
+ matchpatch/workflow.py,sha256=u1uQqTNtp68pvimbZWMqIe3PQPPsVvyqjO9UTMfh8_w,12928
13
+ matchpatch/devices/__init__.py,sha256=nC4WfuzMOQmFPrSgrsIF-hIRBTVDpHl1a5nzFld1-gw,196
14
+ matchpatch/devices/base.py,sha256=FPY-k4l3lW968ULBArlUgGqXxiSyqzqTAemALO03O_w,6677
15
+ matchpatch/devices/helix.py,sha256=ZcXtfU0j2inSGVIBlt7LN6wFxrO7fEmAebLFz35buKk,15763
16
+ matchpatch/devices/registry.py,sha256=SgfqsVHmvT-mItbhiBP8I5NHwNIS05fVd5rBih3DDUc,642
17
+ matchpatch/gui/__init__.py,sha256=PD5RmVUlqzMUz3rQRzvC8YN58R9Yb0805O08wp9OlnM,48
18
+ matchpatch/gui/app.py,sha256=iB1WMgU82yJtXy6eeX_3hH170o0DYEHQgl8iZpW-LH4,6485
19
+ matchpatch/gui/device_panels.py,sha256=bxptD85eDKxVwKnNC6y0izxn5IpQTIrV7Jt60rCucS8,5328
20
+ matchpatch/gui/dialogs.py,sha256=ab7PDn__gzRCZOw-VWkGsCX048rIYD0a6OAaCf7RcY4,5810
21
+ matchpatch/gui/help.py,sha256=g2km4N0--0D_wXwLqtcgGevXs2dIUmnslClpt6qe11E,7464
22
+ matchpatch/gui/main_window.py,sha256=25yBPKgjcP8NPWczY3CaGL9S4I41dvoODRMU8hotKhs,322861
23
+ matchpatch/gui/snapshot_header.py,sha256=ZuZdXkZHZTOlI6kuax7lj_SvAIcNnCsF2bk8N8pZLO8,1927
24
+ matchpatch/gui/worker.py,sha256=CzMBTvMFgQ4wSTtsO8nTb0WkTzLmboUezuD2wVA62P0,4239
25
+ matchpatch-0.4.0.dist-info/METADATA,sha256=tczsa4V3FLx2h9-RHBKyEb2LSQ7GQ47BX6BolboboOY,4799
26
+ matchpatch-0.4.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
27
+ matchpatch-0.4.0.dist-info/entry_points.txt,sha256=kjAMZz7z0FH-8lNWGE6ECXq664vPId54K6bRvc6eSH8,92
28
+ matchpatch-0.4.0.dist-info/licenses/LICENSE,sha256=8pi7wtiIu5UFxxFQwLfTi7DudedhoPhOldqL5Z00LTo,1080
29
+ matchpatch-0.4.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ matchpatch = matchpatch.cli:main
3
+ matchpatch-gui = matchpatch.gui.app:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MatchPatch contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.