diff --git a/BongoCat.iss b/BongoCat.iss index f91bee4..382418b 100644 --- a/BongoCat.iss +++ b/BongoCat.iss @@ -28,6 +28,8 @@ Source: "dist\bongocat.exe"; DestDir: "{app}"; Flags: ignoreversion Source: "frame_idle.png"; DestDir: "{app}"; Flags: ignoreversion Source: "frame1.png"; DestDir: "{app}"; Flags: ignoreversion Source: "frame2.png"; DestDir: "{app}"; Flags: ignoreversion +Source: "frame_meow.png"; DestDir: "{app}"; Flags: ignoreversion +Source: "meow.wav"; DestDir: "{app}"; Flags: ignoreversion Source: "bongoicon.png"; DestDir: "{app}"; Flags: ignoreversion Source: "bongoicon.ico"; DestDir: "{app}"; Flags: ignoreversion diff --git a/README.md b/README.md index e431138..defded4 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ No contribution guides yet, but feel free to ask if you have any questions! ### πŸ§ͺ Option A: Just run it -Download `bongocat.exe` from the [Releases](https://github.com/yourusername/bongocat/releases) page and run it directly. You'll need to place the assets (.png and .ico files) in the same folder. +Download `bongocat.exe` from the [Releases](https://github.com/sofiadparamo/bongocat/releases) page and run it directly. You'll need to place the assets (.png, .wav and .ico files) in the same folder. ### πŸ§™ Option B: Install it properly @@ -55,7 +55,7 @@ Download and run `BongoCatSetup.exe` to: ```powershell # Build the EXE with embedded assets -pyinstaller --onefile --noconsole --icon=bongoicon.ico --add-data "frame_idle.png;." --add-data "frame1.png;." --add-data "frame2.png;." --add-data "bongoicon.png;." bongocat.py +pyinstaller --onefile --noconsole --icon=bongoicon.ico --add-data "frame_idle.png;." --add-data "frame1.png;." --add-data "frame2.png;." --add-data "frame_meow.png;." --add-data "bongoicon.png;." --add-data "meow.wav;." bongocat.py # Generate the installer & "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" .\BongoCat.iss @@ -83,6 +83,8 @@ Use the system tray menu to change options or open the config folder. | `frame_idle.png` | Idle pose | | `frame1.png` | Tap frame 1 | | `frame2.png` | Tap frame 2 | +| `frame_meow.png` | Meowing frame | +| `meow.wav` | Cat meow audio | | `BongoCat.iss` | Inno Setup installer script | | `BongoCat.spec` | PyInstaller spec file | diff --git a/bongocat.py b/bongocat.py index ba35f42..5ec529d 100644 --- a/bongocat.py +++ b/bongocat.py @@ -1,56 +1,62 @@ # bongocat.py -"""Always‑on‑top, borderless, **scroll‑to‑resize** Bongoβ€―Cat overlay for WindowsΒ 11. +"""Always-on-top, borderless, **scroll-to-resize** Bongo Cat overlay for Windows 11. Required assets (same folder as this script): - β€’ frame_idle.png – idle pose - β€’ frame1.png – tap pose 1 - β€’ frame2.png – tap pose 2 - β€’ bongoicon.png – app logo (tray, window, installer) + β€’ frame_idle.png - idle pose + β€’ frame1.png - tap pose 1 + β€’ frame2.png - tap pose 2 + β€’ frame_meow.png - meow pose + β€’ meow.wav - meow sound + β€’ bongoicon.png - app logo (tray, window, installer) Run: `python bongocat.py` or via the bundled EXE. """ from __future__ import annotations import os import sys +import time import threading import json -import winreg +import random +import winsound import tkinter as tk import tkinter.messagebox as messagebox from pathlib import Path -from PIL import Image, ImageTk # pip install pillow +from PIL import Image, ImageTk import pystray -from pynput import keyboard # pip install pynput +from pynput import keyboard -# ───────────── PATH SETUP ────────────── # -# Determine base directory (handles PyInstaller bundle) if getattr(sys, 'frozen', False): BASE_DIR = Path(sys._MEIPASS) else: BASE_DIR = Path(__file__).resolve().parent -# Config paths +DEFAULTS = { + "scale":0.5, + "autostart":False, + "upside_down":False, + "activate_audio":False +} + appdata = os.getenv("APPDATA") or str(Path.home() / "AppData" / "Roaming") -CONFIG_DIR = Path(appdata) / "BongoCat" +CONFIG_DIR = Path(appdata) / "BongoCat" CONFIG_FILE = CONFIG_DIR / "config.json" -# Ensure config dir exists CONFIG_DIR.mkdir(parents=True, exist_ok=True) +if not CONFIG_FILE.exists(): + CONFIG_FILE.write_text(json.dumps(DEFAULTS, indent=2)) -# Asset file paths -LOGO_FILE = BASE_DIR / "bongoicon.png" -IDLE_FRAME_FILE = BASE_DIR / "frame_idle.png" -TAP_FRAME_FILES = [BASE_DIR / "frame1.png", BASE_DIR / "frame2.png"] - -# Default settings -DEFAULTS = {"scale":1.0, "autostart":False} +LOGO_FILE = BASE_DIR / "bongoicon.png" +IDLE_FRAME_FILE = BASE_DIR / "frame_idle.png" +TAP_FRAME_FILES = [BASE_DIR / "frame1.png", BASE_DIR / "frame2.png"] +MEOW_FRAME_FILE = BASE_DIR / "frame_meow.png" +MEOW_SOUND_FILE = BASE_DIR / "meow.wav" -# Idle timeout and scaling constants -IDLE_TIMEOUT_MS = 3000 -START_POSITION = (100, 100) +IDLE_TIMEOUT_MS = 3000 +START_POSITION = (100, 100) TRANSPARENT_COLOR = "magenta" -MIN_SIZE = 64 -SCALE_STEP = 1.1 -# ─────────────────────────────────────── # +MIN_SIZE = 64 +SCALE_STEP = 1.1 + def load_config() -> dict: if CONFIG_FILE.exists(): @@ -69,132 +75,136 @@ def save_config(cfg: dict) -> None: class BongoCatApp: def __init__(self, root: tk.Tk): - # Load config and initialize scale self.cfg = load_config() self._current_scale = self.cfg.get("scale", 1.0) - # Main window setup self.root = root self.root.configure(bg=TRANSPARENT_COLOR) self.root.attributes("-topmost", True) self.root.attributes("-transparentcolor", TRANSPARENT_COLOR) - # Set window icon try: ico_img = Image.open(LOGO_FILE) ico = ImageTk.PhotoImage(ico_img) self.root.iconphoto(False, ico) - self._window_icon_ref = ico # keep reference + self._window_icon_ref = ico except Exception: pass - # Load sprites self.base_idle = Image.open(IDLE_FRAME_FILE) - self.base_tap = [Image.open(p) for p in TAP_FRAME_FILES] + self.base_tap = [Image.open(p) for p in TAP_FRAME_FILES] + self.base_meow = Image.open(MEOW_FRAME_FILE) - # Create system tray icon and menu self._create_tray_icon() - # Image label self.label = tk.Label(root, bg=TRANSPARENT_COLOR, bd=0, highlightthickness=0) self.label.pack() - # Animation state self.frames_idle: ImageTk.PhotoImage | None = None self.frames_tap: list[ImageTk.PhotoImage] = [] + self.frame_meow: ImageTk.PhotoImage | None = None self._current_index = 0 self._is_idle = True + self._is_upside_down = self.cfg.get("upside_down", False) + self._is_meowing = False + self._audio_enabled = self.cfg.get("activate_audio", True) - # Initial draw & positioning self._build_frames() self._show_idle() self.root.geometry(f"{self.frames_idle.width()}x{self.frames_idle.height()}+{START_POSITION[0]}+{START_POSITION[1]}") self.root.overrideredirect(True) - # Bind drag, scroll, keyboard, and escape self._setup_bindings() self._start_keyboard_listener() + self._schedule_meow() + + def _flip(self): + self._is_upside_down = not self._is_upside_down + self._build_frames() def _create_tray_icon(self): - # Prepare tray icon image try: logo = Image.open(LOGO_FILE) except Exception: logo = self.base_idle tray_img = logo.resize((16, 16), Image.LANCZOS) - # Build menu menu = pystray.Menu( pystray.MenuItem("Increase size", lambda i, v: self.root.after(0, lambda: self._change_scale(0.1))), pystray.MenuItem("Decrease size", lambda i, v: self.root.after(0, lambda: self._change_scale(-0.1))), + pystray.MenuItem("Flip", lambda i, v: self.root.after(0, lambda: self._flip())), pystray.MenuItem( - "Start on boot", - lambda i, v: self.root.after(0, self._toggle_autostart_item), - checked=lambda item: self.cfg.get("autostart", False) + "Mute", + lambda i, v: self.root.after(0, self._toggle_audio), + checked=lambda item: not self._audio_enabled ), - pystray.MenuItem("Open config folder", lambda i, v: self._open_config_folder()), pystray.MenuItem("Exit", lambda i, v: self.root.after(0, self.close)) ) - # Run tray icon in background - self.tray = pystray.Icon("BongoCat", tray_img, "BongoΒ Cat", menu) + self.tray = pystray.Icon("BongoCat", tray_img, "Bongo Cat", menu) threading.Thread(target=self.tray.run, daemon=True).start() - def _open_config_folder(self): - try: - CONFIG_DIR.mkdir(parents=True, exist_ok=True) - os.startfile(str(CONFIG_DIR)) - except Exception as e: - messagebox.showerror("Error", f"Cannot open config folder: {e}") - - def _toggle_autostart_item(self): - new = not self.cfg.get("autostart", False) - self._toggle_autostart(new) - self.cfg["autostart"] = new + def _toggle_audio(self): + self._audio_enabled = not self._audio_enabled + self.cfg["activate_audio"] = self._audio_enabled save_config(self.cfg) - def _toggle_autostart(self, enabled: bool): - key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, - r"Software\Microsoft\Windows\CurrentVersion\Run", - 0, winreg.KEY_SET_VALUE) - exe = sys.executable if getattr(sys, "frozen", False) else sys.argv[0] - if enabled: - winreg.SetValueEx(key, "BongoCat", 0, winreg.REG_SZ, exe) + def _schedule_meow(self): + interval = random.randint(200, 600) * 1000 + self.root.after(interval, self._trigger_meow) + + def _trigger_meow(self): + if not self._audio_enabled: + self._schedule_meow() + return + + self._is_meowing = True + self.label.configure(image=self.frame_meow) + + def play(): + time.sleep(0.15) + winsound.PlaySound(str(MEOW_SOUND_FILE), winsound.SND_FILENAME) + + threading.Thread(target=play, daemon=True).start() + + self.root.after(1500, self._end_meow) + + def _end_meow(self): + self._is_meowing = False + if self._is_idle: + self.label.configure(image=self.frames_idle) else: - try: - winreg.DeleteValue(key, "BongoCat") - except FileNotFoundError: - pass - key.Close() + self.label.configure(image=self.frames_tap[self._current_index]) + self._schedule_meow() def _build_frames(self): - # Scale and build PhotoImage frames w = max(MIN_SIZE, int(self.base_idle.width * self._current_scale)) h = max(MIN_SIZE, int(self.base_idle.height * self._current_scale)) def clean(img): - scaled = img.resize((int(img.width * (w/img.width)), int(img.height * (h/img.height))), Image.LANCZOS) + scaled = img.resize((w, h), Image.LANCZOS) if scaled.mode != "RGBA": scaled = scaled.convert("RGBA") *rgb, alpha = scaled.split() alpha = alpha.point(lambda v: 255 if v >= 200 else 0) scaled.putalpha(alpha) + if self._is_upside_down: + scaled = scaled.transpose(Image.ROTATE_180) return scaled self.frames_idle = ImageTk.PhotoImage(clean(self.base_idle)) self.frames_tap = [ImageTk.PhotoImage(clean(img)) for img in self.base_tap] + self.frame_meow = ImageTk.PhotoImage(clean(self.base_meow)) def _show_idle(self): - self.label.configure(image=self.frames_idle) + if not self._is_meowing: + self.label.configure(image=self.frames_idle) def _setup_bindings(self): - # Drag self._dx = self._dy = 0 self.label.bind("", self._start_move) self.label.bind("", self._do_move) - # Scroll for ev in ("", "", ""): self.label.bind(ev, self._on_mouse_wheel) - # Escape self.root.bind("", lambda e: self.close()) def _start_keyboard_listener(self): @@ -202,8 +212,9 @@ def _start_keyboard_listener(self): self.listener.start() self._idle_after_id = self.root.after(IDLE_TIMEOUT_MS, self._go_idle) - # ─── Input handlers ─── # def _on_key_press(self, _): + if self._is_meowing: + return if self._idle_after_id: self.root.after_cancel(self._idle_after_id) self._is_idle = False @@ -218,7 +229,8 @@ def _go_idle(self): def _change_scale(self, delta: float): self._current_scale = max(0.1, self._current_scale + delta) self._build_frames() - self.label.configure(image=(self.frames_idle if self._is_idle else self.frames_tap[self._current_index])) + if not self._is_meowing: + self.label.configure(image=(self.frames_idle if self._is_idle else self.frames_tap[self._current_index])) self._apply_window_size() def _on_mouse_wheel(self, ev): @@ -226,10 +238,10 @@ def _on_mouse_wheel(self, ev): factor = SCALE_STEP if up else 1/SCALE_STEP self._current_scale *= factor self._build_frames() - self.label.configure(image=(self.frames_idle if self._is_idle else self.frames_tap[self._current_index])) + if not self._is_meowing: + self.label.configure(image=(self.frames_idle if self._is_idle else self.frames_tap[self._current_index])) self._apply_window_size() - # ─── Geometry ─── # def _apply_window_size(self): w, h = self.frames_idle.width(), self.frames_idle.height() x, y = self.root.winfo_x(), self.root.winfo_y() @@ -241,15 +253,13 @@ def _start_move(self, e): def _do_move(self, e): self.root.geometry(f"+{e.x_root - self._dx}+{e.y_root - self._dy}") - # ─── Cleanup ─── # def close(self): - # save config self.cfg["scale"] = self._current_scale + self.cfg["upside_down"] = self._is_upside_down + self.cfg["activate_audio"] = self._audio_enabled save_config(self.cfg) - # stop tray if hasattr(self, "tray"): self.tray.stop() - # stop listener if hasattr(self, "listener"): self.listener.stop() self.root.destroy() diff --git a/bongocat.spec b/bongocat.spec index 5ffe256..7777f93 100644 --- a/bongocat.spec +++ b/bongocat.spec @@ -5,7 +5,7 @@ a = Analysis( ['bongocat.py'], pathex=[], binaries=[], - datas=[('frame_idle.png', '.'), ('frame1.png', '.'), ('frame2.png', '.'), ('bongoicon.png', '.')], + datas=[('frame_idle.png', '.'), ('frame1.png', '.'), ('frame2.png', '.'), ('frame_meow.png', '.'), ('bongoicon.png', '.'), ('meow.wav', '.')], hiddenimports=[], hookspath=[], hooksconfig={}, diff --git a/frame_meow.png b/frame_meow.png new file mode 100644 index 0000000..8d02e91 Binary files /dev/null and b/frame_meow.png differ diff --git a/meow.wav b/meow.wav new file mode 100644 index 0000000..b60274f Binary files /dev/null and b/meow.wav differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0b68302 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +pillow +pynput +pystray \ No newline at end of file