Skip to content

Instantly share code, notes, and snippets.

@kompowiec
Last active October 9, 2025 06:58
Show Gist options
  • Save kompowiec/3b271daa5c9f8a0051c9964c55a3f182 to your computer and use it in GitHub Desktop.
Save kompowiec/3b271daa5c9f8a0051c9964c55a3f182 to your computer and use it in GitHub Desktop.
import os
import time
import json
import subprocess
import psutil
import threading
from collections import deque
from pystray import Icon, Menu, MenuItem
from PIL import Image, ImageDraw
# ============================================================
# CONFIGURATION
# ============================================================
CONFIG_FILE = "gonein60s_config.json"
DEFAULT_CONFIG = {
"gone_time": 60, # Seconds before app is forgotten
"ignore_apps": ["explorer.exe", "cmd.exe", "gonein60s.exe"]
}
closed_apps = deque(maxlen=20)
stop_threads = False
last_click_time = 0
config = DEFAULT_CONFIG.copy()
# ============================================================
# CONFIG HANDLING
# ============================================================
def load_config():
global config
if os.path.exists(CONFIG_FILE):
try:
with open(CONFIG_FILE, "r") as f:
config.update(json.load(f))
except Exception:
pass
else:
save_config()
def save_config():
try:
with open(CONFIG_FILE, "w") as f:
json.dump(config, f, indent=4)
except Exception as e:
print("Error saving config:", e)
# ============================================================
# PROCESS MONITORING
# ============================================================
def get_active_processes():
processes = {}
for proc in psutil.process_iter(['pid', 'name']):
try:
processes[proc.info['pid']] = proc.info['name']
except (psutil.NoSuchProcess, psutil.AccessDenied):
continue
return processes
def monitor_processes():
"""Continuously watches for closed processes."""
prev = get_active_processes()
while not stop_threads:
time.sleep(2)
current = get_active_processes()
for pid, name in prev.items():
if pid not in current:
name_lower = name.lower()
if name_lower not in (a.lower() for a in config["ignore_apps"]):
closed_apps.append((time.time(), name, pid))
prev = current.copy()
# ============================================================
# PROCESS ACTIONS
# ============================================================
def restore_process(name):
"""Attempts to restart a closed app by its executable name."""
try:
subprocess.Popen(name, shell=True)
except Exception as e:
print(f"Error restoring {name}: {e}")
def restore_all():
for _, name, _ in list(closed_apps):
restore_process(name)
closed_apps.clear()
def kill_process(name):
"""Kills all processes matching a given executable name."""
for proc in psutil.process_iter(['pid', 'name']):
try:
if proc.info['name'].lower() == name.lower():
proc.kill()
except Exception:
continue
# ============================================================
# AUTO CLEANUP (GONE TIME)
# ============================================================
def cleanup_expired():
"""Removes apps older than the configured gone time."""
while not stop_threads:
time.sleep(3)
now = time.time()
gone_time = config.get("gone_time", 60)
while closed_apps and now - closed_apps[0][0] > gone_time:
closed_apps.popleft()
# ============================================================
# TRAY ICON AND MENU
# ============================================================
def make_icon():
"""Creates a small lightning icon."""
img = Image.new("RGB", (64, 64), (0, 0, 255))
draw = ImageDraw.Draw(img)
draw.polygon([(20, 10), (40, 30), (25, 30), (40, 55), (15, 35), (30, 35)], fill=(255, 255, 0))
return img
def build_menu(icon=None):
"""Rebuilds the dynamic right-click menu."""
# --- Submenu: Recently closed ---
def app_items():
if not closed_apps:
return [MenuItem("No closed apps", None, enabled=False)]
items = []
for i, (_, name, _) in enumerate(closed_apps):
items.append(MenuItem(f"{i + 1} - {name}", lambda _, n=name: restore_process(n)))
return items
# --- Submenu: Kill ---
def kill_items():
active = get_active_processes()
if not active:
return [MenuItem("No running apps", None, enabled=False)]
# Show only unique names
names = sorted(set(active.values()))
return [MenuItem(n, lambda _, nm=n: kill_process(nm)) for n in names]
# --- Submenu: Gone Time ---
def gone_time_items():
options = [30, 60, 120, 300]
return [MenuItem(f"{t}s", lambda _, v=t: set_gone_time(v), checked=lambda item, v=t: config["gone_time"] == v)
for t in options]
# --- Submenu: Ignore Apps ---
def ignore_items():
return [MenuItem(a, None, enabled=False) for a in config["ignore_apps"]] + [
MenuItem("Edit ignore list", lambda: open_ignore_list())
]
# Compose full menu
return Menu(
MenuItem("Recover All", lambda: restore_all()),
Menu.SEPARATOR,
*app_items(),
Menu.SEPARATOR,
MenuItem("Kill Window", Menu(*kill_items())),
MenuItem("Gone Time", Menu(*gone_time_items())),
MenuItem("Ignored Apps", Menu(*ignore_items())),
Menu.SEPARATOR,
MenuItem("About", lambda: os.system('echo GoneIn60s Python Edition v2')),
MenuItem("Exit", lambda: on_exit(icon))
)
def refresh_menu(icon):
"""Periodically refreshes tray menu to reflect new data."""
while not stop_threads:
time.sleep(3)
icon.menu = build_menu(icon)
icon.update_menu()
def on_clicked(icon, item):
"""Handle single and double clicks."""
global last_click_time
now = time.time()
if now - last_click_time < 0.5: # Double-click threshold
restore_all()
last_click_time = now
def set_gone_time(seconds):
config["gone_time"] = seconds
save_config()
def open_ignore_list():
"""Opens the ignore list file for manual editing."""
# Save ignore list as text for easy manual editing
path = "ignore_list.txt"
with open(path, "w") as f:
for name in config["ignore_apps"]:
f.write(name + "\n")
os.system(f"notepad {path}" if os.name == "nt" else f"xdg-open {path}")
def on_exit(icon):
global stop_threads
stop_threads = True
save_config()
icon.stop()
# ============================================================
# MAIN
# ============================================================
def main():
load_config()
threading.Thread(target=monitor_processes, daemon=True).start()
threading.Thread(target=cleanup_expired, daemon=True).start()
icon = Icon("GoneIn60s", make_icon(), "GoneIn60s", build_menu(), on_clicked=on_clicked)
threading.Thread(target=refresh_menu, args=(icon,), daemon=True).start()
icon.run()
if __name__ == "__main__":
main()
@kompowiec
Copy link
Author

deps: pip install pystray pillow psutil
(debian trixie)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment