Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions BongoCat.iss
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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 |

Expand Down
174 changes: 92 additions & 82 deletions bongocat.py
Original file line number Diff line number Diff line change
@@ -1,56 +1,62 @@
# bongocat.py
"""Always‑on‑top, borderless, **scroll‑to‑resize** BongoCat 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():
Expand All @@ -69,141 +75,146 @@ 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("<ButtonPress-1>", self._start_move)
self.label.bind("<B1-Motion>", self._do_move)
# Scroll
for ev in ("<MouseWheel>", "<Button-4>", "<Button-5>"):
self.label.bind(ev, self._on_mouse_wheel)
# Escape
self.root.bind("<Escape>", lambda e: self.close())

def _start_keyboard_listener(self):
self.listener = keyboard.Listener(on_press=self._on_key_press, daemon=True)
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
Expand All @@ -218,18 +229,19 @@ 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):
up = (ev.num == 4) or (getattr(ev, "delta", 0) > 0)
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()
Expand All @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion bongocat.spec
Original file line number Diff line number Diff line change
Expand Up @@ -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={},
Expand Down
Binary file added frame_meow.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added meow.wav
Binary file not shown.
3 changes: 3 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pillow
pynput
pystray