diff --git a/AGENTS.md b/AGENTS.md index 625a504..1794d78 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -71,6 +71,20 @@ When adding or removing dependencies: --- +## Pre-commit checklist + +**Always run all three of these commands and confirm they pass before every `git commit`:** + +```sh +uv run ruff format . # auto-format — re-stage any files it changes +uv run ruff check . # lint — must report "All checks passed!" +uv run pytest --tb=short -q # tests — must report 0 failures, 0 errors +``` + +Do not commit if any of these steps fails. Fix the issue first, then re-run all three. + +--- + ## Git workflow ``` diff --git a/MultiChannelWavMixer.py b/MultiChannelWavMixer.py index f848eb5..2189079 100644 --- a/MultiChannelWavMixer.py +++ b/MultiChannelWavMixer.py @@ -12,6 +12,7 @@ import pyloudnorm as pyln import soundfile as sf from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg +from PIL import Image from pydub import AudioSegment from mixer_utils import ( @@ -34,6 +35,12 @@ ctk.set_default_color_theme("blue") # ─── Constants ───────────────────────────────────────────────────────────────── +# Resolve where bundled/source assets (Pics/) live. +if getattr(sys, "frozen", False): + _BUNDLE_DIR = sys._MEIPASS +else: + _BUNDLE_DIR = os.path.dirname(os.path.abspath(__file__)) + # Resolve paths that must work both from source and inside a .app bundle. if getattr(sys, "frozen", False): # Running inside PyInstaller bundle — store user data in Application Support @@ -515,11 +522,21 @@ def set_output_folder(inFilePath=None): lbl_output_folder.configure(text=display) +def _load_ctk_image(filename: str, size: tuple[int, int]) -> ctk.CTkImage | None: + """Load a PNG from Pics/ as a CTkImage, returning None on failure.""" + path = os.path.join(_BUNDLE_DIR, "Pics", filename) + try: + img = Image.open(path) + return ctk.CTkImage(light_image=img, dark_image=img, size=size) + except Exception: + return None + + # ─── Main window ─────────────────────────────────────────────────────────────── root = ctk.CTk() root.title("Multichannel WAV Mixer") -root.geometry("1060x560") -root.minsize(900, 420) +root.geometry("1060x628") +root.minsize(900, 490) def bring_to_front(event=None): @@ -529,6 +546,33 @@ def bring_to_front(event=None): root.bind("", bring_to_front) +# ─── Header ─────────────────────────────────────────────────────────────────── +header = ctk.CTkFrame(root, corner_radius=0, height=66, fg_color="#090c15") +header.pack(side="top", fill="x") +header.pack_propagate(False) + +_logo_img = _load_ctk_image("Logo.png", (76, 53)) +if _logo_img: + ctk.CTkLabel(header, image=_logo_img, text="").pack(side="left", padx=(16, 10), pady=6) + +_title_frame = ctk.CTkFrame(header, fg_color="transparent") +_title_frame.pack(side="left", pady=10) +ctk.CTkLabel( + _title_frame, + text="Multichannel WAV Mixer", + font=ctk.CTkFont(size=19, weight="bold"), + text_color="#daeef8", +).pack(anchor="w") +ctk.CTkLabel( + _title_frame, + text="RME Durec · Stereo Mixdown", + font=ctk.CTkFont(size=11), + text_color="#4a7a92", +).pack(anchor="w") + +# Accent line separating header from toolbar +ctk.CTkFrame(root, corner_radius=0, height=2, fg_color="#1e5f7a").pack(side="top", fill="x") + # ─── State variables ─────────────────────────────────────────────────────────── file_paths: tuple = () tracks: list = [] diff --git a/MultiChannelWavMixer.spec b/MultiChannelWavMixer.spec index fea04d6..adebc73 100644 --- a/MultiChannelWavMixer.spec +++ b/MultiChannelWavMixer.spec @@ -13,6 +13,10 @@ block_cipher = None # ── Data files to bundle ──────────────────────────────────────────────────────── datas = [] +# Application images (logo, header background) +import glob as _glob +datas += [(_f, "Pics") for _f in _glob.glob("Pics/*.png")] + # customtkinter — themes, images, assets datas += collect_data_files("customtkinter") @@ -111,6 +115,7 @@ exe = EXE( target_arch=None, # native arch; use 'universal2' for fat binary codesign_identity=None, entitlements_file=None, + icon="Pics/AppIcon.ico" if sys.platform == "win32" else None, ) coll = COLLECT( @@ -129,7 +134,7 @@ if sys.platform == "darwin": app = BUNDLE( coll, name="MultiChannelWavMixer.app", - icon=None, # set to "path/to/icon.icns" when available + icon="Pics/AppIcon.icns", bundle_identifier="com.macbuchi.multichannelwavmixer", info_plist={ "CFBundleShortVersionString": "1.0.0", diff --git a/Pics/AppIcon.icns b/Pics/AppIcon.icns new file mode 100644 index 0000000..59164b5 Binary files /dev/null and b/Pics/AppIcon.icns differ diff --git a/Pics/AppIcon.ico b/Pics/AppIcon.ico new file mode 100644 index 0000000..0f8a41b Binary files /dev/null and b/Pics/AppIcon.ico differ diff --git a/Pics/App_Logo.png b/Pics/App_Logo.png new file mode 100644 index 0000000..4f57cec Binary files /dev/null and b/Pics/App_Logo.png differ diff --git a/Pics/BG_Header.png b/Pics/BG_Header.png new file mode 100644 index 0000000..db82eb5 Binary files /dev/null and b/Pics/BG_Header.png differ diff --git a/Pics/BG_Header.svg b/Pics/BG_Header.svg new file mode 100644 index 0000000..95bac75 --- /dev/null +++ b/Pics/BG_Header.svg @@ -0,0 +1,55 @@ + + + + diff --git a/Pics/Background.png b/Pics/Background.png new file mode 100644 index 0000000..cb0b61f Binary files /dev/null and b/Pics/Background.png differ diff --git a/Pics/Background.svg b/Pics/Background.svg new file mode 100644 index 0000000..2aaeddd --- /dev/null +++ b/Pics/Background.svg @@ -0,0 +1,61 @@ + + + + diff --git a/Pics/Logo.png b/Pics/Logo.png new file mode 100644 index 0000000..ec3777b Binary files /dev/null and b/Pics/Logo.png differ diff --git a/Pics/Logo.svg b/Pics/Logo.svg new file mode 100644 index 0000000..72738a8 --- /dev/null +++ b/Pics/Logo.svg @@ -0,0 +1,58 @@ + + + + diff --git a/tests/test_app_smoke.py b/tests/test_app_smoke.py new file mode 100644 index 0000000..91d676f --- /dev/null +++ b/tests/test_app_smoke.py @@ -0,0 +1,63 @@ +""" +tests/test_app_smoke.py +----------------------- +Subprocess smoke-test for MultiChannelWavMixer.py. + +Launches the GUI app as a child process, waits 6 seconds, verifies it is +still alive (i.e. no import error or immediate crash), then terminates it. + +Skipped on platforms where the GUI backend is unavailable (Linux CI). +The pytest job in CI only runs on macos-latest, so this will always execute +there while remaining a safe no-op elsewhere. +""" + +from __future__ import annotations + +import os +import subprocess +import sys +import time +from pathlib import Path + +import pytest + +# Root of the repository — one level above this tests/ dir. +_REPO_ROOT = Path(__file__).parent.parent +_APP_SCRIPT = _REPO_ROOT / "MultiChannelWavMixer.py" + +# Seconds the app must stay alive to be considered healthy. +_ALIVE_SECS = 6 + + +@pytest.mark.skipif( + sys.platform not in ("darwin", "win32"), + reason="GUI smoke test requires a platform with a native display (macOS / Windows)", +) +def test_app_starts_and_stays_alive() -> None: + """Launch the app and confirm it does not exit within _ALIVE_SECS seconds.""" + proc = subprocess.Popen( + [sys.executable, str(_APP_SCRIPT)], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + cwd=str(_REPO_ROOT), + env={**os.environ}, + ) + + time.sleep(_ALIVE_SECS) + exit_code = proc.poll() + + if exit_code is None: + # Still running — healthy. + proc.terminate() + try: + proc.wait(timeout=5) + except subprocess.TimeoutExpired: + proc.kill() + return + + # Process exited before we got a chance to check — collect output and fail. + _, stderr = proc.communicate(timeout=5) + error_text = stderr.decode(errors="replace").strip() + pytest.fail( + f"App exited after {_ALIVE_SECS}s with code {exit_code}.\nstderr:\n{error_text[:2000]}" + ) diff --git a/uv.lock b/uv.lock index 5951c73..1f0a26c 100644 --- a/uv.lock +++ b/uv.lock @@ -735,7 +735,7 @@ wheels = [ [[package]] name = "multichannel-wav-mixer" -version = "1.0.2" +version = "1.0.3" source = { virtual = "." } dependencies = [ { name = "audioop-lts" },