[object Object]

← back to Ram Sniper

initial scaffold: standalone RAM Sniper tool + README

bed7c85fe5afb013703434bf3ff5f7010108d0a0 · 2026-09-09 10:13:51 -0700 · Steve

Single-file cross-platform RAM/CPU watchdog (Tkinter GUI, psutil-preferred
with stdlib fallback). Navbar live RAM%/CPU% + ON/OFF toggle, RAM-hog list,
kill-selected, optional auto-snipe. Runs on stock Python; --test is headless.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U2uvgXWs4FjLd6eDBDNUxP

Files touched

Diff

commit bed7c85fe5afb013703434bf3ff5f7010108d0a0
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Sep 9 10:13:51 2026 -0700

    initial scaffold: standalone RAM Sniper tool + README
    
    Single-file cross-platform RAM/CPU watchdog (Tkinter GUI, psutil-preferred
    with stdlib fallback). Navbar live RAM%/CPU% + ON/OFF toggle, RAM-hog list,
    kill-selected, optional auto-snipe. Runs on stock Python; --test is headless.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01U2uvgXWs4FjLd6eDBDNUxP
---
 .gitignore   |  12 ++
 RAMSniper.py | 464 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 README.md    |  60 ++++++++
 3 files changed, 536 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..eaebc11
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,12 @@
+node_modules/
+.env*
+tmp/
+*.log
+.DS_Store
+dist/
+build/
+.next/
+__pycache__/
+*.pyc
+.venv/
+venv/
diff --git a/RAMSniper.py b/RAMSniper.py
new file mode 100644
index 0000000..0c1cbfc
--- /dev/null
+++ b/RAMSniper.py
@@ -0,0 +1,464 @@
+#!/usr/bin/env python3
+"""
+RAM Sniper  —  a tiny standalone memory/CPU watchdog.
+
+WHAT IT DOES
+    • A top "navbar" showing live RAM % and CPU % (updates every 2s when ON).
+    • A list of the biggest RAM-hogging processes, largest first.
+    • "Kill Selected" snipes the process you pick (asks you first).
+    • "Auto-Snipe" (optional, OFF by default) auto-kills anything whose RAM
+      goes over the threshold you set — the real "sniper" mode.
+
+HOW TO RUN
+    Mac:      open Terminal, then:   python3 RAMSniper.py
+              (or double-click if .py is set to open with Python Launcher)
+    Windows:  double-click the file, OR:   py RAMSniper.py
+              (rename to RAMSniper.pyw to hide the black console window)
+
+    It needs nothing installed — Python's built-in Tkinter draws the window.
+    For the most accurate numbers, optionally:   pip install psutil
+    (works fine without it; falls back to the OS's own tools.)
+
+    Sanity check without the window:   python3 RAMSniper.py --test
+
+Single file. Nothing phones home. Nothing is written to disk.
+"""
+
+import os
+import sys
+import signal
+import subprocess
+import platform
+
+APP_TITLE = "RAM Sniper"
+REFRESH_MS = 2000          # how often to poll while ON
+TOP_N = 20                 # how many processes to list
+DEFAULT_THRESHOLD = 15.0   # auto-snipe RAM % per-process trigger
+
+IS_WIN = platform.system() == "Windows"
+IS_MAC = platform.system() == "Darwin"
+
+# ---------------------------------------------------------------------------
+# Stats layer:  prefer psutil (accurate), else per-OS fallbacks (best-effort).
+# Every function returns simple numbers so the UI never has to know the source.
+# ---------------------------------------------------------------------------
+try:
+    import psutil  # optional
+    HAVE_PSUTIL = True
+except Exception:
+    HAVE_PSUTIL = False
+
+
+def _run(cmd):
+    """Run a shell command, return stdout text (empty string on any failure)."""
+    try:
+        return subprocess.run(cmd, capture_output=True, text=True, timeout=4).stdout
+    except Exception:
+        return ""
+
+
+def total_ram_bytes():
+    if HAVE_PSUTIL:
+        return psutil.virtual_memory().total
+    if IS_MAC:
+        out = _run(["sysctl", "-n", "hw.memsize"])
+        return int(out.strip() or 0)
+    if IS_WIN:
+        import ctypes
+        class MS(ctypes.Structure):
+            _fields_ = [("dwLength", ctypes.c_ulong),
+                        ("dwMemoryLoad", ctypes.c_ulong),
+                        ("ullTotalPhys", ctypes.c_ulonglong),
+                        ("ullAvailPhys", ctypes.c_ulonglong),
+                        ("ullTotalPageFile", ctypes.c_ulonglong),
+                        ("ullAvailPageFile", ctypes.c_ulonglong),
+                        ("ullTotalVirtual", ctypes.c_ulonglong),
+                        ("ullAvailVirtual", ctypes.c_ulonglong),
+                        ("ullAvailExtendedVirtual", ctypes.c_ulonglong)]
+        m = MS(); m.dwLength = ctypes.sizeof(MS)
+        ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(m))
+        return m.ullTotalPhys
+    # Linux
+    try:
+        with open("/proc/meminfo") as f:
+            for line in f:
+                if line.startswith("MemTotal:"):
+                    return int(line.split()[1]) * 1024
+    except Exception:
+        pass
+    return 0
+
+
+def ram_used_percent():
+    """Return system RAM used, as a percent 0..100."""
+    if HAVE_PSUTIL:
+        return psutil.virtual_memory().percent
+    if IS_WIN:
+        import ctypes
+        class MS(ctypes.Structure):
+            _fields_ = [("dwLength", ctypes.c_ulong),
+                        ("dwMemoryLoad", ctypes.c_ulong),
+                        ("ullTotalPhys", ctypes.c_ulonglong),
+                        ("ullAvailPhys", ctypes.c_ulonglong),
+                        ("ullTotalPageFile", ctypes.c_ulonglong),
+                        ("ullAvailPageFile", ctypes.c_ulonglong),
+                        ("ullTotalVirtual", ctypes.c_ulonglong),
+                        ("ullAvailVirtual", ctypes.c_ulonglong),
+                        ("ullAvailExtendedVirtual", ctypes.c_ulonglong)]
+        m = MS(); m.dwLength = ctypes.sizeof(MS)
+        ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(m))
+        return float(m.dwMemoryLoad)
+    if IS_MAC:
+        # Derive used% from vm_stat page counts.
+        page = 4096
+        ps = _run(["sysctl", "-n", "vm.pagesize"]).strip()
+        if ps.isdigit():
+            page = int(ps)
+        out = _run(["vm_stat"])
+        pages = {}
+        for line in out.splitlines():
+            if ":" in line:
+                k, _, v = line.partition(":")
+                v = v.strip().rstrip(".")
+                if v.isdigit():
+                    pages[k.strip()] = int(v)
+        total = total_ram_bytes()
+        if total and pages:
+            free = (pages.get("Pages free", 0) +
+                    pages.get("Pages inactive", 0) +
+                    pages.get("Pages speculative", 0) +
+                    pages.get("Pages purgeable", 0)) * page
+            used = max(0, total - free)
+            return round(100.0 * used / total, 1)
+        return 0.0
+    # Linux
+    try:
+        info = {}
+        with open("/proc/meminfo") as f:
+            for line in f:
+                k, _, v = line.partition(":")
+                info[k.strip()] = int(v.split()[0]) * 1024
+        total = info.get("MemTotal", 0)
+        avail = info.get("MemAvailable", info.get("MemFree", 0))
+        if total:
+            return round(100.0 * (total - avail) / total, 1)
+    except Exception:
+        pass
+    return 0.0
+
+
+_last_cpu = {"idle": None, "total": None}
+
+
+def cpu_used_percent():
+    """Return system CPU used, as a percent 0..100 (non-blocking / instant)."""
+    if HAVE_PSUTIL:
+        # interval=None => since the previous call; primed at startup.
+        return psutil.cpu_percent(interval=None)
+    if IS_WIN:
+        out = _run(["wmic", "cpu", "get", "loadpercentage"])
+        for tok in out.split():
+            if tok.strip().isdigit():
+                return float(tok.strip())
+        return 0.0
+    if IS_MAC:
+        # Sum recent %cpu across processes, normalise by core count. Approximate.
+        out = _run(["ps", "-A", "-o", "%cpu"])
+        vals = []
+        for line in out.splitlines()[1:]:
+            line = line.strip()
+            try:
+                vals.append(float(line))
+            except ValueError:
+                pass
+        ncpu = os.cpu_count() or 1
+        return round(min(100.0, sum(vals) / ncpu), 1)
+    # Linux: delta of /proc/stat between calls.
+    try:
+        with open("/proc/stat") as f:
+            parts = f.readline().split()[1:]
+        nums = list(map(int, parts))
+        idle = nums[3] + (nums[4] if len(nums) > 4 else 0)
+        total = sum(nums)
+        pi, pt = _last_cpu["idle"], _last_cpu["total"]
+        _last_cpu["idle"], _last_cpu["total"] = idle, total
+        if pi is None:
+            return 0.0
+        dt = total - pt
+        di = idle - pi
+        if dt <= 0:
+            return 0.0
+        return round(100.0 * (dt - di) / dt, 1)
+    except Exception:
+        return 0.0
+
+
+def top_processes(n=TOP_N):
+    """Return list of dicts: pid, name, rss_mb, ram_pct — biggest RAM first."""
+    total = total_ram_bytes() or 1
+    rows = []
+    if HAVE_PSUTIL:
+        for p in psutil.process_iter(["pid", "name", "memory_info"]):
+            try:
+                rss = p.info["memory_info"].rss
+                rows.append({
+                    "pid": p.info["pid"],
+                    "name": p.info["name"] or "?",
+                    "rss_mb": rss / (1024 * 1024),
+                    "ram_pct": 100.0 * rss / total,
+                })
+            except Exception:
+                continue
+    elif IS_WIN:
+        out = _run(["tasklist", "/fo", "csv", "/nh"])
+        for line in out.splitlines():
+            cols = [c.strip('"') for c in line.split('","')]
+            if len(cols) >= 5:
+                name = cols[0].strip('"')
+                try:
+                    pid = int(cols[1])
+                    mem_kb = int(cols[4].replace(",", "").replace(".", "")
+                                 .replace(" K", "").replace("K", "").strip() or 0)
+                except ValueError:
+                    continue
+                rss = mem_kb * 1024
+                rows.append({"pid": pid, "name": name,
+                             "rss_mb": rss / (1024 * 1024),
+                             "ram_pct": 100.0 * rss / total})
+    else:  # Mac / Linux
+        out = _run(["ps", "-axo", "pid=,rss=,comm="])
+        for line in out.splitlines():
+            line = line.strip()
+            if not line:
+                continue
+            try:
+                pid_s, rss_s, comm = line.split(None, 2)
+                pid = int(pid_s)
+                rss = int(rss_s) * 1024  # ps rss is in KB
+            except ValueError:
+                continue
+            rows.append({"pid": pid, "name": os.path.basename(comm),
+                         "rss_mb": rss / (1024 * 1024),
+                         "ram_pct": 100.0 * rss / total})
+    rows.sort(key=lambda r: r["rss_mb"], reverse=True)
+    return rows[:n]
+
+
+def kill_pid(pid):
+    """Terminate a process. Returns (ok, message)."""
+    if pid in (0, os.getpid()):
+        return False, "Refusing to kill an invalid PID or myself."
+    try:
+        if IS_WIN:
+            r = subprocess.run(["taskkill", "/PID", str(pid), "/F"],
+                               capture_output=True, text=True)
+            return (r.returncode == 0), (r.stdout or r.stderr).strip()
+        os.kill(pid, signal.SIGTERM)
+        return True, f"Sent SIGTERM to {pid}."
+    except PermissionError:
+        return False, f"Permission denied killing {pid} (needs admin/root)."
+    except ProcessLookupError:
+        return False, f"Process {pid} already gone."
+    except Exception as e:
+        return False, f"{type(e).__name__}: {e}"
+
+
+# ---------------------------------------------------------------------------
+# Headless self-test:   python3 RAMSniper.py --test
+# ---------------------------------------------------------------------------
+def selftest():
+    if HAVE_PSUTIL:
+        cpu_used_percent()  # prime
+    print(f"{APP_TITLE} self-test on {platform.system()} "
+          f"({'psutil' if HAVE_PSUTIL else 'stdlib fallback'})")
+    print(f"  Total RAM : {total_ram_bytes()/(1024**3):.1f} GB")
+    print(f"  RAM used  : {ram_used_percent():.1f} %")
+    print(f"  CPU used  : {cpu_used_percent():.1f} %")
+    print("  Top 5 RAM hogs:")
+    for r in top_processes(5):
+        print(f"    {r['pid']:>7}  {r['ram_pct']:5.1f}%  "
+              f"{r['rss_mb']:8.1f} MB  {r['name']}")
+
+
+# ---------------------------------------------------------------------------
+# GUI
+# ---------------------------------------------------------------------------
+def launch_gui():
+    import tkinter as tk
+    from tkinter import ttk, messagebox
+
+    if HAVE_PSUTIL:
+        cpu_used_percent()  # prime the psutil delta
+
+    root = tk.Tk()
+    root.title(APP_TITLE)
+    root.geometry("720x520")
+    root.minsize(620, 420)
+
+    BG = "#0f1216"; FG = "#e7edf3"; ACCENT = "#00b4d8"
+    HOT = "#ff5c5c"; OKC = "#3ddc84"; MUTE = "#8a97a3"
+    root.configure(bg=BG)
+
+    state = {"on": False, "auto": tk.BooleanVar(value=False),
+             "threshold": tk.DoubleVar(value=DEFAULT_THRESHOLD),
+             "job": None}
+
+    # ---- NAVBAR ----------------------------------------------------------
+    nav = tk.Frame(root, bg="#151b22", height=64)
+    nav.pack(fill="x", side="top")
+    nav.pack_propagate(False)
+
+    tk.Label(nav, text="🎯 RAM SNIPER", bg="#151b22", fg=ACCENT,
+             font=("Helvetica", 15, "bold")).pack(side="left", padx=16)
+
+    ram_lbl = tk.Label(nav, text="RAM  --%", bg="#151b22", fg=FG,
+                       font=("Helvetica", 14, "bold"))
+    ram_lbl.pack(side="left", padx=14)
+    cpu_lbl = tk.Label(nav, text="CPU  --%", bg="#151b22", fg=FG,
+                       font=("Helvetica", 14, "bold"))
+    cpu_lbl.pack(side="left", padx=14)
+
+    def toggle():
+        state["on"] = not state["on"]
+        if state["on"]:
+            power_btn.config(text="● ON", bg=OKC, fg="#08210f")
+            tick()
+        else:
+            power_btn.config(text="○ OFF", bg="#2a333d", fg=FG)
+            if state["job"]:
+                root.after_cancel(state["job"]); state["job"] = None
+            status.config(text="Paused.")
+
+    power_btn = tk.Button(nav, text="○ OFF", command=toggle, bg="#2a333d",
+                          fg=FG, font=("Helvetica", 12, "bold"),
+                          relief="flat", padx=16, pady=4, cursor="hand2")
+    power_btn.pack(side="right", padx=16)
+
+    src = "psutil" if HAVE_PSUTIL else "built-in"
+    tk.Label(nav, text=src, bg="#151b22", fg=MUTE,
+             font=("Helvetica", 9)).pack(side="right", padx=6)
+
+    # ---- CONTROLS --------------------------------------------------------
+    ctl = tk.Frame(root, bg=BG); ctl.pack(fill="x", padx=14, pady=(10, 4))
+    tk.Checkbutton(ctl, text="Auto-snipe processes over",
+                   variable=state["auto"], bg=BG, fg=FG, selectcolor="#151b22",
+                   activebackground=BG, activeforeground=FG,
+                   font=("Helvetica", 11),
+                   command=lambda: _confirm_auto()).pack(side="left")
+    tk.Entry(ctl, textvariable=state["threshold"], width=5,
+             bg="#151b22", fg=FG, insertbackground=FG,
+             relief="flat").pack(side="left", padx=(6, 2))
+    tk.Label(ctl, text="% RAM", bg=BG, fg=FG,
+             font=("Helvetica", 11)).pack(side="left")
+
+    def _confirm_auto():
+        if state["auto"].get():
+            if not messagebox.askyesno(
+                    "Enable Auto-Snipe?",
+                    "Auto-Snipe will automatically KILL any process whose RAM "
+                    "goes over the threshold, with no further prompts.\n\n"
+                    "Turn it on?"):
+                state["auto"].set(False)
+
+    kill_btn = tk.Button(ctl, text="🔫 Kill Selected",
+                         command=lambda: do_kill(), bg=HOT, fg="white",
+                         font=("Helvetica", 11, "bold"), relief="flat",
+                         padx=14, pady=3, cursor="hand2")
+    kill_btn.pack(side="right")
+
+    # ---- PROCESS LIST ----------------------------------------------------
+    style = ttk.Style()
+    try:
+        style.theme_use("clam")
+    except Exception:
+        pass
+    style.configure("Treeview", background="#12171d", fieldbackground="#12171d",
+                    foreground=FG, rowheight=24, borderwidth=0)
+    style.configure("Treeview.Heading", background="#151b22", foreground=ACCENT,
+                    font=("Helvetica", 10, "bold"))
+    style.map("Treeview", background=[("selected", ACCENT)],
+              foreground=[("selected", "#04121a")])
+
+    cols = ("pid", "name", "ram_mb", "ram_pct")
+    tree = ttk.Treeview(root, columns=cols, show="headings")
+    tree.heading("pid", text="PID")
+    tree.heading("name", text="Process")
+    tree.heading("ram_mb", text="RAM (MB)")
+    tree.heading("ram_pct", text="RAM %")
+    tree.column("pid", width=80, anchor="e")
+    tree.column("name", width=320, anchor="w")
+    tree.column("ram_mb", width=120, anchor="e")
+    tree.column("ram_pct", width=90, anchor="e")
+    tree.tag_configure("hot", foreground=HOT)
+    tree.pack(fill="both", expand=True, padx=14, pady=8)
+
+    status = tk.Label(root, text="Press ON to start.", bg=BG, fg=MUTE,
+                      anchor="w", font=("Helvetica", 10))
+    status.pack(fill="x", padx=14, pady=(0, 8))
+
+    def do_kill():
+        sel = tree.selection()
+        if not sel:
+            status.config(text="Select a process first."); return
+        item = tree.item(sel[0])["values"]
+        pid, name = int(item[0]), item[1]
+        if messagebox.askyesno("Snipe process?",
+                               f"Kill {name}  (PID {pid}) ?"):
+            ok, msg = kill_pid(pid)
+            status.config(text=msg, fg=OKC if ok else HOT)
+            refresh()
+
+    def refresh():
+        procs = top_processes()
+        thr = 0.0
+        try:
+            thr = float(state["threshold"].get())
+        except Exception:
+            thr = DEFAULT_THRESHOLD
+        auto_hits = []
+        tree.delete(*tree.get_children())
+        for r in procs:
+            hot = r["ram_pct"] >= thr
+            tree.insert("", "end",
+                        values=(r["pid"], r["name"],
+                                f"{r['rss_mb']:.0f}", f"{r['ram_pct']:.1f}"),
+                        tags=("hot",) if hot else ())
+            if hot:
+                auto_hits.append(r)
+        return auto_hits
+
+    def tick():
+        ram = ram_used_percent()
+        cpu = cpu_used_percent()
+        ram_lbl.config(text=f"RAM  {ram:.0f}%",
+                       fg=HOT if ram >= 85 else FG)
+        cpu_lbl.config(text=f"CPU  {cpu:.0f}%",
+                       fg=HOT if cpu >= 85 else FG)
+        hits = refresh()
+        if state["auto"].get() and hits:
+            for r in hits:
+                if r["pid"] == os.getpid():
+                    continue
+                ok, msg = kill_pid(r["pid"])
+                status.config(text=f"AUTO-SNIPED {r['name']} ({r['pid']}): {msg}",
+                              fg=OKC if ok else HOT)
+        else:
+            status.config(text=f"Watching {TOP_N} processes · "
+                               f"threshold {state['threshold'].get():.0f}% · "
+                               f"{'AUTO-SNIPE ON' if state['auto'].get() else 'manual'}",
+                          fg=MUTE)
+        if state["on"]:
+            state["job"] = root.after(REFRESH_MS, tick)
+
+    root.mainloop()
+
+
+if __name__ == "__main__":
+    if "--test" in sys.argv:
+        selftest()
+    else:
+        try:
+            launch_gui()
+        except Exception as e:
+            print(f"Could not open the window: {e}")
+            print("Try the headless check:  python3 RAMSniper.py --test")
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..5159dda
--- /dev/null
+++ b/README.md
@@ -0,0 +1,60 @@
+# RAM Sniper
+
+A standalone, single-file cross-platform RAM/CPU watchdog with a GUI. Built to hand
+to someone as one file that runs on a stock Python install — no `pip install` required.
+
+## What it does
+
+- **Navbar** with live **RAM %** and **CPU %**, plus an **ON/OFF** toggle button.
+- **Process list** (Treeview) of the biggest RAM hogs — PID / Process / RAM MB / RAM % —
+  sorted largest-first, refreshing every 2s.
+- **🔫 Kill Selected** — terminates the highlighted process (with a confirm dialog).
+- **Auto-Snipe** (OFF by default) — when enabled (and confirmed), auto-kills any process
+  whose RAM usage crosses a threshold (default **15%**).
+
+## Run it
+
+```sh
+python3 RAMSniper.py
+```
+
+That's it. Tkinter ships with the standard Python installer on **macOS** and **Windows**,
+so the GUI works out of the box — genuinely one-file-and-run.
+
+Headless self-test (no window, prints the stats it reads):
+
+```sh
+python3 RAMSniper.py --test
+```
+
+## How the stats are read (psutil-preferred, stdlib fallback)
+
+The tool prefers [`psutil`](https://pypi.org/project/psutil/) when it's installed
+(faster, cleaner numbers), and **falls back to platform stdlib tools** when it isn't —
+so it still works on a bare Python:
+
+| Platform | Fallback source |
+|----------|-----------------|
+| macOS    | `sysctl`, `vm_stat`, `ps` |
+| Windows  | `ctypes` `GlobalMemoryStatusEx`, `wmic`/`tasklist`/`taskkill` |
+| Linux    | `/proc/meminfo`, `/proc/stat`, `/proc/<pid>` |
+
+**Optional speed-up:** for tighter/faster readings, install psutil once —
+
+```sh
+pip install psutil
+```
+
+— but it is *not* required. The whole point of the tool is that it runs without it.
+
+## Safety
+
+- `kill_pid()` refuses PID 0 and refuses to kill the Sniper's own process.
+- Kill and Auto-Snipe-enable both prompt for confirmation before acting.
+- Auto-Snipe is **OFF** on every launch; it never persists an armed state.
+
+## Tunables (top of `RAMSniper.py`)
+
+- `REFRESH_MS = 2000` — refresh cadence in milliseconds.
+- `TOP_N = 20` — how many processes to list.
+- Auto-Snipe threshold default — `15` (% RAM).

(oldest)  ·  back to Ram Sniper  ·  (newest)