Pythonator is a fast desktop supervisor for long-running processes: web servers, Discord/Telegram bots, workers, scrapers, schedulers. It runs Python, Node.js, Bun, Deno, Go, Java, .NET, Ruby, PHP, shell and PowerShell scripts, or any program, on Windows, macOS or Linux directly, with no Docker containers and no WSL. Each service gets its own environment, a live colour console, crash recovery and a built-in code editor in the same window.
Supervision
-
Run any number of services at once, mixing runtimes. Each one runs a script, its runtime's named entry point, or any command:
Runtime Script Entry point Install dependencies Python python -u bot.pypython -m uvicorn …pip installinto the venvNode.js node bot.js(.mjs,.ts)npm run start(pnpm / yarn / bun by lockfile)npm install/addBun bun run server.tsbun run devbun install/bun addDeno deno run -A main.tsdeno task startdeno install/deno addGo go run main.gogo run ./cmd/botgo mod tidy/go getJava java -jar server.jar(or one.javafile)java -cp . com.example.Bot(Maven / Gradle) .NET dotnet run --project Bot.csproj,Bot.dll,app.csdotnet restore/add packageRuby ruby bot.rb(unbuffered)bundle exec pumabundle install/gem installPHP php worker.phpphp -S localhost:8000composer install/requireShell bash run.sh,.bat/.cmdon WindowsPowerShell pwsh -File task.ps1Program any executable: caddy,redis-server,.exe, AppImage, macOS.appRuntimes are found on PATH and in their usual install folders (
~/.bun/bin,/usr/local/go/bin,JAVA_HOME, Git Bash on Windows, Homebrew andjava_homeon macOS, ...), or you set the interpreter per service. Runtime options go to the interpreter itself, e.g.-Xmx4Gfor Java or--max-old-space-size=4096for Node. Adding a language is one entry incore/runtimes.py. -
Restart policies (never / on failure / always) with exponential back-off (1s → 60s) for crash loops, a restart limit (by default: give up after 10 restarts within 10 minutes, with one notification instead of one per crash) and exit codes that mean "don't restart" (e.g. a bad token:
2, 78) -
Services keep running when you close the window. They run in a small background service (
pythonator --daemon, no window); the window and the command line connect to it. Quitting asks whether to keep them running or stop them. If Pythonator itself is killed, the next start stops any leftover process trees (matched by pid and start time, so a recycled pid is never touched) and starts those services again. -
Command line for servers and scripts:
pythonator ls | start | stop | restart | logs [-f] | attach | shutdown. Like tmux: services keep running when you close the window or log out of SSH, andpythonator attach botputs you back in front of one (live output, type into it, Ctrl+C to detach) (see Running without a desktop) -
Clean shutdown of the whole process tree. Every service runs in its own process group: Stop sends SIGTERM (Ctrl+C on Windows, so Python's
KeyboardInterruptand Node'sSIGINThandlers run), waits a configurable grace period, then kills everything it spawned. Workers left behind after a crash are cleaned up too, so ports are free for the next start. -
Optional RAM and CPU limits per service (Configure → Limits), covering every process the service starts. CPU is in percent of one core, like the CPU figure in the header (150 % = 1.5 cores). Each system uses the strongest limit available without admin rights:
System How the limits are enforced Linux (incl. the .deb) The kernel, through a systemd scope ( systemd-run --user --scope -p MemoryMax=… -p CPUQuota=…, cgroups v2)Windows The kernel, through a job object: job-wide memory limit and a hard CPU rate cap macOS, Linux without a systemd user session Pythonator: the tree is stopped when it goes over its memory limit (restart policy applies) and paused in short slices to hold its CPU share The settings dialog says which one applies on your machine, and the console notes when a limit is hit.
-
Schedules for jobs that run and finish:
every 15m, times of day (09:00, 18:30) or cron (0 3 * * mon-fri,@daily). A run is skipped if the previous one is still going. -
Autostart services when Pythonator opens, and keep them running from the system tray (the menu bar on macOS) when the window is closed
-
Start Pythonator at login (Settings), minimized to the tray, so your bots and servers come back after a reboot: a LaunchAgent on macOS, an XDG autostart entry on Linux, the Run key on Windows
-
Live CPU / RAM / uptime / pid per service tree, sampled on a background thread
-
This computer's CPU and RAM at the top right of the toolbar (used / total, turning yellow at 75 % and red at 90 %); hover it for cores, free memory, swap and the peaks since Pythonator started
Environment
- Python: per-service virtual environment, auto-detected (
.venv/venv) in the working directory; create it andpip installwith one click - Node.js:
node_modules/.bin(of the project and its parents) is on PATH; Install dependencies runsnpm install(or pnpm / yarn / bun), or adds the packages you list; pick a specificnodebinary per service - Dependencies button for every language: Create a starter
requirements.txt,package.json,deno.json,go.mod,Gemfileorcomposer.jsonif the project has none, Edit it in the built-in editor, then Save and install dependencies (unsaved edits are saved first) - Version fixes: when an install fails because a pinned version doesn't exist for your Python (say
numpy==1.26.0on 3.13), the console names the closest available version and asks y/n before changing the file and installing again. If a pinned package fails to build, it looks up versions with ready-made wheels first. For npm / pnpm / yarn / bun it offerspackage@latest. - Per-service environment variables (
KEY=value,$VARexpansion), working directory and interpreter .envfiles: point a service at.env(picked automatically when one sits next to the script) to keep tokens out of the config; variables set on the service override it$ shellconsole mode: runpip list,alembic upgrade head,npx prisma migrate deploy, ... inside the service's environment, with output streamed into the console. Stop ends a long-running command too (python -m http.server,npm run dev), and pressing it again kills one that won't quit.- On Windows commands run in PowerShell by default, so
mv,ls,rm,cpandcatfrom README instructions work. Pick cmd or Git Bash in Settings → Console $ shell (bash or PowerShell elsewhere). - A pasted block of several lines runs as one script and stops at the first line that fails.
- When a command doesn't exist, the console says why: Docker not installed,
apt/sudobeing Linux commands on Windows, or a tool installed after Pythonator started (restart it to pick up the new PATH).
- On Windows commands run in PowerShell by default, so
- stdin mode: type into a running process (
input()prompts, REPL-style bot commands)
Console & logs
- Fast ANSI console: 16/256/true colour, bold, italic, underline. Progress bars using
\rcollapse correctly. - Output is read and split into lines on background threads; the GUI only renders what's new, about 12 times a second, within a time budget measured on your machine. A process printing 800k lines/s can't freeze the window or make the editor lag: the console shows the newest lines and notes how many it skipped (they're all in the log file)
- Find in console, follow mode, timestamps, wrap
- Every line is persisted to rotating log files (size and count configurable). History search covers all of them off the GUI thread.
- Double-click a stack-trace line (Python, Node, Go, Java, .NET, Ruby, PHP, shell) to open the file at that line in the editor
- Clickable links: URLs in the output open in your browser, and when a web server or API starts, in any
language and also from a
$console command, an Open button appears in the header. It uses the address the server prints (Running on http://127.0.0.1:5000,Listening on :8080,Tomcat started on port 8080), or the port the process actually listens on when it prints nothing
Files & editor
- Folder tree of the selected service's project: open files, create files and folders, rename (F2), move to
trash, copy paths, run a script as a new service. Cache folders like
__pycache__and.gitare hidden. - Tabs, highlighting for Python, JavaScript/TypeScript, Go, Java, C#, Ruby, PHP, Rust, shell, batch and PowerShell, line numbers, smart indent,
Ctrl+/comment toggle, find & replace, go to line - Python completion via jedi (optional), using the service's own venv
- Atomic saves, reloads files changed on disk, and saves your edits automatically on Start/Restart (F5)
Reliability
- A bug in Pythonator can't take your services down with it: unexpected errors are written to
logs/pythonator.logand shown in a dialog, and the app keeps running - A service that can't start (missing interpreter, deleted folder, bad permissions) says why in its console instead of failing silently; supervision loops and stats sampling recover from any error
- The config is written atomically with a
.bakcopy. A damaged config is restored from the backup, and one that can't be read is never overwritten. - Very long output lines are split instead of eating memory; a full disk stops log files, never the service
- The editor refuses binary, non-UTF-8 and huge files rather than corrupting them
Requires Python 3.11+ on Windows, macOS (11 or later, Apple silicon or Intel) or Linux. For other languages, install their runtime as usual (Node.js, Go, a JDK, the .NET SDK, Ruby, PHP, ...); Pythonator finds it on PATH or in its standard install folder.
git clone https://github.com/cfunkz/Pythonator.git
cd Pythonator
pip install -e . # or: pip install -r requirements.txt
pythonator # or: python -m pythonator / python app.pySmart Python completion uses jedi. pip install -r requirements.txt
includes it; with pip install -e . add it with pip install -e ".[completion]" (or pip install jedi), in the
Python that runs Pythonator, then restart. Your services' venvs don't need it: jedi reads the packages installed
in each service's own venv. Suggestions pop up after . or three letters, or press Ctrl+Space. Without jedi the
editor completes words from the current file, and says so once when you open a Python file.
- New service (or drop a
.py,.js,.go,.rb,.php,.sh, ... file onto the window), pick the runtime and a script, entry point or command - Use the Dependencies button in the header (also in the ⋯ menu). For Python, pick Create venv, then
Install dependencies (your list, or
requirements.txt). Other runtimes only need Install dependencies (your list, orpackage.json,go.mod,Gemfile, ...). Edit package.json (or whichever file the project uses) opens it in the editor, and Create ... makes a starter file if there is none. Add packages, then Save and install dependencies. - Start (F5). Watch the console, edit the code above it, and press F5 to save and restart.
- Set Restart: on failure and Start automatically for anything that should always be up, or a Schedule (Configure → Supervision) for jobs like backups and reports
- Close the window and choose Keep running in the system tray (menu bar on macOS). To have everything come back after a reboot, turn on Settings → Start Pythonator when I log in.
| Shortcut | Action |
|---|---|
| Windows / Linux | macOS |
| --- | --- |
F5 / Shift+F5 |
⌘R / ⌘. (or F5 / ⇧F5) |
Ctrl+Shift+N / Ctrl+E |
⇧⌘N / ⌘E |
Ctrl+S, Ctrl+Shift+S, Ctrl+O, Ctrl+W |
⌘S, ⇧⌘S, ⌘O, ⌘W |
Ctrl+F, Ctrl+H, Ctrl+G |
⌘F, ⌥⌘F, ⌘L |
Ctrl+Space, Ctrl+/, Ctrl+D |
⌃Space, ⌘/, ⌘D |
Ctrl+1 / Ctrl+2 / Ctrl+3 |
⌘1 / ⌘2 / ⌘3 |
⌘, |
|
Ctrl+Q |
⌘Q |
↑ / ↓ in the console input |
same |
Tooltips show each shortcut the way your system writes it.
| Location | |
|---|---|
| Windows | %APPDATA%\Pythonator\ |
| macOS | ~/Library/Application Support/Pythonator/ |
| Linux | ~/.config/pythonator/ |
| Portable | next to the app, if a pythonator.json (or a 1.x bots.json) is already there |
| Override | PYTHONATOR_HOME=/some/dir or pythonator --home /some/dir |
It contains pythonator.json (config, written atomically), logs/ (one rotating log per service, plus
pythonator.log for the window and daemon.log for the background service), requirements/, running.json
(what is running, to clean up after a crash) and, while the background service runs, daemon.json (how to
reach it: a socket name and a random token, readable by your account only). Upgrading from 1.x: your bots.json is migrated automatically on first start and left
untouched as a backup.
On Linux and macOS the config, its backup and the logs/ folder are readable by your account only, because
service environment variables and output often contain tokens. On Windows Pythonator checks the folder's owner
and permissions and can restrict it to your account. Values of secret-looking variables (*_TOKEN, *_KEY,
*PASSWORD*, *SECRET*, the password in a DATABASE_URL, ...) from the service settings and its .env file
are replaced by •••••• in the console and the log files. Prefer a .env file in the project (kept out of git)
over typing secrets into the service settings.
Secrets are not moved to the system keychain: the background service has to start services at login and on headless servers, where no keychain prompt can be answered.
Pythonator runs your services with your own account's rights: it is a supervisor, not a sandbox, so only run code you trust. On top of that it:
- Asks before risky things: starting as root / Administrator, the first “Install dependencies” of a project
(package installs run their own setup code),
$ shellcommands that look destructive (rm -rf /,format c:,curl … | sh,shutdown, …), deleting files from the folder tree (to the trash first; permanent delete is asked separately), and autostarting services when the settings folder could have been changed by another user. - Never runs anything through a shell by accident: services and installs are started from an argument list.
On Windows,
.bat/.cmdfiles (includingnpm.cmd) get every argument quoted for cmd.exe, so an argument likea&calcstays an argument. - Keeps secrets out of logs: what you type at a
Password:/Token:prompt is hidden while you type, logged as••••••••and kept out of the input history; secret env values are masked in output, and env vars are never printed. - Opens only web links (http/https) from console output, and file-tree names can't reach outside the folder.
- Only your account can control it: the background service listens on a socket (Unix) or named pipe
(Windows) that only your account can open, with a random name on Windows, and every connection must present
the random token from
daemon.json. - Cleans up: stopping a service stops its whole process tree (on Windows through a job object that also kills it if the background service dies), and "Stop all services and quit" stops everything it started.
- Signed releases you can verify: release files come with SHA-256 checksums and a build provenance
attestation (
gh attestation verify <file> --repo cfunkz/Pythonator), are built from hash-locked dependencies with SHA-pinned GitHub Actions, and a published version is never rebuilt or replaced. - Update check (off by default, Settings): only asks GitHub for the latest version number and shows a link.
Everything the window does runs in the background service, so a server, a Raspberry Pi or a CI box can run
Pythonator with no display. Set the services up once in the window (or copy pythonator.json from another
machine), then:
pythonator daemon # run the background service in this terminal (Ctrl+C stops it and the services)
pythonator status # every service: state, pid, uptime, restarts, CPU, RAM, address
pythonator start bot api # by name or id; --all for every service (starts the service if needed)
pythonator restart --all
pythonator logs bot -n 100 -f # the last 100 lines, then follow
pythonator attach bot # live output and type into it, like tmux attach; Ctrl+C detaches
pythonator ls # same as status
pythonator stop bot
pythonator shutdown # stop every service and the background serviceExit codes: 0 ok, 1 error, 2 usage, 3 the background service isn't running. The installed apps take the
same commands (/opt/pythonator/Pythonator status, Pythonator.exe status, Pythonator.app/Contents/MacOS/Pythonator status).
In cmd.exe the windowed Pythonator.exe returns to the prompt before its output; use PowerShell or
start /wait Pythonator.exe status.
Start it at boot so services survive logouts and reboots:
-
Linux (systemd, as your user):
~/.config/systemd/user/pythonator.service[Unit] Description=Pythonator [Service] ExecStart=/usr/bin/pythonator daemon Restart=on-failure TimeoutStopSec=90 [Install] WantedBy=default.target
systemctl --user enable --now pythonator, plusloginctl enable-linger $USERso it runs without a login.systemctl --user stop pythonatorstops the services cleanly (SIGTERM). -
macOS (launchd):
~/Library/LaunchAgents/io.github.cfunkz.pythonator.plistwithProgramArguments/Applications/Pythonator.app/Contents/MacOS/Pythonator,daemon, andRunAtLoad/KeepAliveset to true; thenlaunchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/io.github.cfunkz.pythonator.plist. -
Windows (Task Scheduler): a task "At log on" (or "At startup", run whether the user is logged on or not) with the action
"C:\Program Files\Pythonator\Pythonator.exe" daemon.
Opening the window while the background service runs just connects to it. Start the window with --no-daemon
(or PYTHONATOR_NO_DAEMON=1) to run the services inside the window instead, as before; they then stop when it
quits.
pythonator/
├── core/ Qt-free and unit-tested
│ ├── models.py typed dataclasses for services and settings (tolerant JSON parsing)
│ ├── store.py atomic config persistence + 1.x migration
│ ├── launch.py command / venv / node / package-manager / environment resolution
│ ├── runtimes.py registry of languages: how each one runs a script, an entry point, a command, installs
│ ├── shells.py the console $ shell (PowerShell / cmd / bash) and "command not found" hints
│ ├── starters.py starter package.json / go.mod / Gemfile / ... for projects without one
│ ├── fixes.py "that version doesn't exist" install failures and the closest version that does
│ ├── locations.py file:line in stack traces of every supported language
│ ├── errors.py LaunchError, the one error a service start can fail with
│ ├── runner.py one child process: reader, stdin writer and waiter threads, tree kill
│ ├── limits.py per-service RAM / CPU limits: systemd scope, Windows job object, or watchdog + throttle
│ ├── logs.py line assembly, ring buffer, rotating log files, history search
│ ├── ansi.py ANSI SGR parser
│ ├── policy.py restart back-off, restart limit and "don't restart" exit codes
│ ├── state.py running.json: finds and stops leftover process trees after a crash
│ ├── winctrl.py Windows: Ctrl+C to a service's console (the --send-ctrl-c helper)
│ ├── winacl.py Windows: owner / ACL checks and "restrict to me" (icacls)
│ ├── updates.py the opt-in update check (version number only)
│ ├── schedule.py "every 15m", daily times and cron expressions
│ ├── urls.py finding a web server's address (printed links, bare ports, listening sockets)
│ ├── userenv.py a terminal's PATH for apps opened from the macOS Finder / Dock; open-files limit
│ ├── login.py start at login: LaunchAgent (macOS), XDG autostart (Linux), Run key (Windows)
│ ├── macos.py App Nap and .app bundles, without PyObjC
│ └── stats.py background psutil sampler (service trees and the whole computer)
├── supervisor.py QObject that owns services, applies policies, emits signals
├── daemon/ the background service: run.py (pythonator --daemon), server.py (local socket +
│ token), client.py (the window's RemoteSupervisor), cli.py (status/start/stop/logs ...),
│ protocol.py (JSON lines)
└── ui/ PyQt6 widgets: main window, service list, folder tree, console, editor, dialogs, theme
├── keys.py shortcuts per platform (⌘R / ⌘. on macOS) and their labels
├── languages.py syntax highlighting for Go, Java, C#, Ruby, PHP, Rust, shell, batch, PowerShell
└── errors.py last-resort error handler: logs, shows a dialog, never lets the app die
GUI thread ─ renders new console lines every 80 ms, list/stats every 1 s, never blocks on I/O
▲ queued Qt signal (exit) ▲ drain()
per process: reader thread ─► LogStore (lines + file write) writer thread ◄─ stdin queue
waiter thread ─► ExitInfo ─► Supervisor ─► restart policy / QTimer back-off
stats thread ─► psutil, one sample per second per process tree
Platform notes. On Linux and macOS, services run in their own session, so Stop signals the whole process
group. On Windows every service gets a hidden console and a job object: Stop sends Ctrl+C to that console (through
a tiny helper, Pythonator --send-ctrl-c PID, so Pythonator's own console is never affected), and the job object
kills the whole tree after the grace period, or at once if Pythonator is killed.
On macOS:
- An app opened from the Finder, Dock or a login item only gets
/usr/bin:/bin:/usr/sbin:/sbinas its PATH. Pythonator asks your login shell ($SHELL -ilc) for the PATH you have in Terminal once at start, so Homebrew, nvm, pyenv, asdf, ... tools are found. SetPYTHONATOR_KEEP_PATH=1to skip this. - App Nap is turned off while Pythonator runs, so a window hidden in the menu bar still restarts crashed services and starts scheduled jobs on time. The Mac can still sleep as usual.
- The open-files limit is raised from macOS's default of 256, for Pythonator and the services it starts.
/usr/bin/javaand/usr/bin/python3are skipped when they are only placeholders that offer to install Java or Apple's command line tools; Homebrew's keg-only OpenJDK and Ruby are found too.
pip install pyinstaller pillow
python packaging/build.pybuilds the packages for the system it runs on into release/:
| Packages | |
|---|---|
| Windows | Pythonator-<version>-windows-x64-setup.exe (needs Inno Setup 6) and a portable .zip |
| macOS | Pythonator-<version>-macos-arm64.dmg or -x86_64.dmg, for the Mac it's built on |
| Linux | pythonator_<version>_amd64.deb or _arm64.deb (installs to /opt/pythonator, adds pythonator and a menu entry) and a .tar.gz |
PyInstaller can only build for the system it runs on, so releases are built on GitHub. The version is written
in one place, __version__ in pythonator/__init__.py; the app, pyproject.toml, the
builds and the release all read it from there. To release, change it, commit and push to main:
# pythonator/__init__.py: __version__ = "0.0.3" -> "0.0.4"
git commit -am "Pythonator 0.0.4" && git pushThe Release workflow sees the new version and builds the Windows installer and
.zip, Linux .deb and .tar.gz files for x64 and arm64 (Raspberry Pi), and .dmg files for Apple silicon and Intel
Macs. It starts each app once, then tags the commit v0.0.4 and publishes everything with checksums as a GitHub
release. A version like 0.1.0-beta.1 makes a pre-release. Nothing is tagged if a build fails, so fix it and
push again.
- Tagging by hand still works:
git tag vX.Y.Z && git push origin vX.Y.Z. The tag must be on a commit whosepythonator/__init__.pyhas that version, otherwise the run stops and says so. - A released version is never replaced: pushing its tag again fails. Release a new version instead. Turn on
Settings > General > Releases > Immutable releases (and protect
v*tags under Settings > Rules) so nobody can change a published release later. - Code signing runs when its secrets are set; see
packaging/sign.pyfor the secret names (Windows: Azure Artifact Signing or a .pfx; macOS: Developer ID + notarization). - Run workflow on the Actions tab builds the packages as downloadable artifacts without releasing them; tick
publish to release the current version on
mainif it isn't released yet. - A push that changes
pythonator/__init__.pywithout a new version does nothing.
Until releases are signed with a Developer ID and notarized, macOS blocks the app the first time. Open it once,
then go to System Settings > Privacy & Security, scroll down and click Open Anyway (macOS 15 Sequoia and
later no longer offer right-click > Open for unsigned apps). Or run
xattr -dr com.apple.quarantine /Applications/Pythonator.app.
The app bundles Pythonator only. Your services still need their own runtime (Python, Node.js, ...), or an interpreter path set in their settings.
pip install -e ".[dev]"
ruff check .
mypy # strict
pytest # core, process supervision and headless UI tests (QT_QPA_PLATFORM=offscreen)
# CI runs them on Windows, macOS (Apple silicon and Intel) and LinuxPyQt6 6.12.0 ships a stub file mypy can't parse; type-check with any other PyQt6 version.




