Turn an old phone (or any device with a browser) into a live dashboard for your PC.
Turn that old phone in your drawer into a live dashboard for your computer. Your PC runs a small Python agent that collects the data and serves a web page; the phone (or any device with a browser) just opens that page.
The page has screens that rotate on their own (or with a tap):
- Hardware: CPU and GPU usage, temperature, power and clock, per-core load, VRAM, RAM and swap. The last 5 minutes of usage show faintly behind each card, and of each temperature next to its number.
- AI: for each AI coding tool installed on the PC (Claude Code, Codex and Gemini CLI): usage limits (5-hour and weekly windows, where the tool keeps them), tokens used today and this week per model, and your sessions (and which ones are working).
- PC: the app in focus, the heaviest apps, fans, network (with a chart of the last 5 minutes, download and upload), free disk space and drive temperatures. The running game and what's playing also show here when game mode or the Music screen is turned off.
- Music: while something plays, the album cover, title, artist, album, the player (with its icon) and the progress, with previous, play/pause and next buttons.
- Photos: the photos in a folder you pick on the PC, one at a time (see Photos and a message).
- Game: while a game runs, game mode shows only this screen: the game's art and icon, how long you've been playing, and GPU/CPU usage, temperature, clock and power, VRAM and RAM. When the game closes, the normal screens come back.
A message you write in the SpareScreen window can scroll along the bottom of every screen.
When a Claude Code, Codex or Gemini CLI session finishes, the page jumps to the AI screen, shows the session name and plays a short chime (sound only works after the first tap on the screen, a browser rule).
Runs on Windows, Linux and macOS, with any device that has a browser. Free and open source (MIT license). Ideas and next steps in ROADMAP.md.
| Windows 11 | Everything, day to day: USB and Wi-Fi, sensors, sleep and hibernation, music buttons, photos, updates from the window. |
| Linux | The executable on Ubuntu 20.04, 22.04 and 24.04, Debian 12 and Fedora 41 (in containers): it starts, pairs a phone over Wi-Fi and updates itself. Sensors, music and sleep not yet tried on a real machine. |
| macOS | Not tried yet: should work, treat it as experimental. |
| Phones | Galaxy A55 (Android 14, Chrome) and Galaxy J5 Prime (Android 8, Samsung Internet 7.2, an engine from 2017), over USB and Wi-Fi. iPhone and iPad not tried yet. |
Grab the file for your system from the latest release and run it. No Python needed.
| System | File | How to run |
|---|---|---|
| Windows | sparescreen-windows-x64.exe |
Double-click it. Windows SmartScreen may warn about an unknown app: More info > Run anyway. |
| Linux | sparescreen-linux-x64 |
chmod +x sparescreen-linux-x64 once, then ./sparescreen-linux-x64. Ubuntu 20.04, Debian 11 or newer (any 64-bit Linux from about 2020 on). |
| macOS (Apple Silicon) | sparescreen-macos-arm64 |
chmod +x sparescreen-macos-arm64, then run it from Terminal. The first time macOS blocks it: System Settings > Privacy & Security > Open Anyway. |
When you start it by hand it opens the SpareScreen window on the PC (see below). The files aren't code-signed yet, which is why the systems warn the first time.
On Windows, double-clicking it runs SpareScreen in the background with an icon in the tray (next to the clock): closing the window doesn't stop it. Click the icon to open the window again; right-click it for the dashboard, the phone address and pairing code, Start with Windows and Quit. Windows 11 puts new icons behind the ^ arrow; drag it onto the taskbar to keep it in sight. Typed in a terminal, it runs right there, as before.
A small window (an Edge or Chrome app window; a browser tab where neither is installed) with three tabs, kept live. A dot on a tab means something there needs you:
- Phone: the Wi-Fi address and the pairing code; when a new device asks to pair, the window jumps here and shows the code in big digits. Below, the devices: phones on USB (with battery level and temperature), the paired Wi-Fi device (with a Remove button) and every screen that has the dashboard open right now.
- Games: the game now (and how it was recognized), your games, the programs running now to mark as games, and the ones marked "not a game" (see Games and apps).
- Display: the folder for the Photos screen and the scrolling message (see Photos and a message).
- Status: what works and what's missing, with a button to fix it where possible: install or open LibreHardwareMonitor (CPU sensors on Windows), turn on FPS in games, install adb (USB cable), plus GPU, Wi-Fi, AI tools, now playing and updates.
At the bottom, Start with Windows (or "Start when I log in" elsewhere) and Check for updates; at the top, Open dashboard.
It answers only to this PC, and only to the window SpareScreen opens itself (from the tray
icon, or the panel.html#key=... address it prints in the terminal): it carries a key kept
in ~/.sparescreen/panel.key. Other devices, other websites, and apps on a phone plugged
in over USB can't read it or press its buttons.
SpareScreen checks GitHub for a new release a minute after it starts and then every 6 hours. When there is one, the SpareScreen window says so at the top (and the phone's settings screen mentions it):
- Downloaded file: click Update. SpareScreen downloads the file for your system,
checks it against the release's
SHA256SUMS, makes sure it starts, puts it in place of the current one (same name and folder, so Start with Windows keeps working) and restarts. The window and the page on the phone reload by themselves; settings and the paired device stay. The folder must be one you can write to; if not, download the new file by hand. On Windows the new version runs in the background with the tray icon, even if the old one was started from a terminal; on Linux and macOS it takes the old one's place in the same terminal. - From source:
git pullin the SpareScreen folder, then restart it.
The check is one request to GitHub's public API (api.github.com), with nothing about you
or your PC in it. Turn it off with Check for updates at the bottom of the window or in
the phone's settings; Check now on the Status tab checks once anyway.
You need Git and Python 3.9 or newer. Download the project and enter the folder:
git clone https://github.com/gustacassel/SpareScreen.gitcd SpareScreenCreate a Python environment just for it:
python -m venv .venvActivate it (Windows: .venv\Scripts\activate; Linux/macOS: source .venv/bin/activate) and install:
pip install -e .sparescreenYou can connect the phone in two ways: over Wi-Fi or with a USB cable.
The agent prints the address to open on the phone, something like http://192.168.0.10:8765,
and a 6-digit pairing code. The phone must be on the same Wi-Fi network as the PC.
Any device with a browser works: Android, iPhone, iPad, a tablet, another computer.
The first time, the page asks for the pairing code: type it once and that device is remembered (even after the agent restarts). Devices that haven't paired can't see any data or change settings. The PC itself and phones on USB don't need the code.
One Wi-Fi device is paired at a time: pairing a new one disconnects the previous one (it asks for the code again). The SpareScreen window shows who is asking to pair and lets you remove the paired device.
The PC's name instead of its IP. The router can give the PC a new IP, and then the
phone loses the page. So, once connected by IP, the page tries the PC's names from the
phone (yourpc.local, which the PC answers itself over mDNS, and yourpc, which many
routers know) and moves to the first one that works, keeping the pairing. iPhones
resolve .local; Android does from version 12. The SpareScreen window shows which
address worked. If none does, the page stays on the IP and tries again a day later.
Works without Wi-Fi and keeps the phone charging. It uses adb reverse, so the
phone reaches the agent at http://localhost:8765 through the cable.
- Install
adb(Android Platform Tools) on the PC:- Windows:
winget install Google.PlatformTools - macOS:
brew install --cask android-platform-tools - Linux (Debian/Ubuntu):
sudo apt install adb
- Windows:
- On the phone, enable USB debugging: open Settings > About phone, tap Build number 7 times, then turn on Developer options > USB debugging (menu names vary a little between brands).
- Run
sparescreenand plug in the cable. The first time, accept the "Allow USB debugging?" prompt on the phone (check "Always allow"). - The page opens by itself in the phone's browser. Unplug and plug it back in and it comes back by itself.
The cable must carry data (some cheap cables only charge). If the agent can't find
adb, use --adb PATH. On Windows, some phones need the manufacturer's USB driver.
On Windows the easiest way is the tray icon's Start with Windows. Or, on any system, to have SpareScreen start by itself when you log in, in the background with no window:
sparescreen --autostart onAny other options you pass are kept (e.g. sparescreen --stay-awake --autostart on).
--autostart off undoes it and --autostart status shows what is registered. It works
per user, without admin rights: a Run entry on Windows, ~/.config/autostart on Linux
and a LaunchAgent on macOS (from the next login). In the background the output goes to
~/.sparescreen/sparescreen.log. With the downloaded executable it's the same:
sparescreen-windows-x64.exe --autostart on.
In the background SpareScreen shows a tray icon (quit from its menu). It's built in on
Windows; on Linux and macOS it shows up if pystray and pillow are installed
(pip install pystray pillow) and the desktop supports tray icons. --no-tray turns it off.
| Option | Default | What it does |
|---|---|---|
--port |
8765 |
HTTP port |
--host |
0.0.0.0 |
Address to listen on. USB only: use 127.0.0.1 so nothing is exposed on the network |
--interval |
1 |
Seconds between readings |
--no-usb |
Don't look for phones on USB | |
--adb |
auto | Path to adb |
--no-open |
Don't open the page on the phone when it connects | |
--stay-awake |
Turns on the phone's "stay awake while plugged in" setting (saved on the phone) | |
--no-pairing |
Let any device on the network see the data without a code (trusted networks only) | |
--reset-pairing |
Forget every paired device before starting | |
--background |
Run without a window (tray icon where possible), output to ~/.sparescreen/sparescreen.log |
|
--no-tray |
No tray icon in the background | |
--dev |
Development mode (see below) | |
--demo |
Show made-up data instead of this PC's (for screenshots, demos or trying it out) | |
--autostart |
on, off or status: start SpareScreen when you log in (see above) |
|
--version |
Print the version |
- Open the address in the browser (automatic over USB).
- Tap the screen to go full screen. Each tap goes to the next screen (or closes an open notification).
- Long press (hold ~1 second) switches the theme.
- The gear icon in the top corner opens settings: theme, which screens rotate,
how often to switch, Claude alerts, sound, fan names, which fan shows on the CPU card,
how long each photo stays, game mode, burn-in protection and night mode. Everything is saved on the PC
(
~/.sparescreen/config.json), so it survives reloading the page. Opened on the PC itself, the settings also show the phone address and the pairing code. - To keep the screen on: use
--stay-awakeover USB, or keep the phone charging and turn on Developer options > Stay awake.
Themes: Graphite, OLED, Terracotta, Neon, Terminal and Light.
For testing you can force things in the address: …:8765/?screen=pc pins one screen
(hardware, ai, pc, music, photos or game) and …:8765/?theme=neon forces a theme.
In the SpareScreen window on the PC, the Display tab:
- Photos: pick a folder (Choose... opens the system's folder picker; you can also type or paste the path). Its photos, subfolders included, show on the Photos screen one at a time in random order, the next one fading in; how long each stays (5 s to 1 min) is in the phone's settings. Photos added to or removed from the folder show up by themselves within half a minute. JPG, PNG, WebP and GIF; HEIC (iPhone) photos aren't shown, because old browsers can't open them. With no folder, or an empty one, the Photos screen stays out of the rotation.
- Message: a line of text (up to 200 characters) that scrolls along the bottom of every screen. Leave it empty for none.
The photos never leave your network: only paired devices get them, by an id (never the
PC's paths). Big photos are made screen-sized on the PC before they're sent, and turned the
way the camera meant: easier on an old phone's memory, and old browsers ignore the camera's
rotation. That uses Pillow, installed with SpareScreen on Windows; on Linux and macOS
pip install pillow does it, and without it the photos go as they are.
The folder picker uses Windows' own dialog, AppleScript on macOS, and zenity or
kdialog on Linux (without them, type the path).
A dashboard shows the same things in the same place for hours, which can burn into OLED/AMOLED screens (many older phones have one).
- Burn-in protection (on by default): every minute the whole page moves a few pixels, too little to notice.
- Night mode (off by default): during the night hours you pick (22–7, 23–7 or 0–8), the screen either dims or shows only a dim clock that moves around every minute. Tap it to see the dashboard for a minute. Alerts (session finished, battery hot) still show up on top.
- Phone screen (USB) (Keep by default): night mode only darkens the page; the
backlight stays on. With the phone on USB, SpareScreen can rest the real screen while
the PC is locked, and during the night hours when night mode is on: Dim turns the
brightness all the way down (and puts yours back, auto-brightness included), Off
turns the screen off and on again. Less heat for a phone that's always plugged in, and
less burn-in. With Off, a phone with a PIN or pattern lock shows its lock screen when
it wakes; for a dashboard-only phone, set the screen lock to None or Swipe.
Locked PC detection: Windows, and Linux with
loginctl.
Just before the PC goes to sleep, SpareScreen tells the phone: instead of "Disconnected", it shows a dim clock with The PC is asleep, and over USB the phone's screen rests right away as if the PC were locked (with Phone screen (USB) on Dim or Off). When the PC wakes up, the dashboard comes back by itself within a few seconds, over Wi-Fi too: the page notices a connection that went silent and opens a new one.
| Windows | Linux | macOS | |
|---|---|---|---|
| "PC asleep" screen and resting the USB phone | ✅ | ✅ with gdbus (systemd-logind), best effort |
❌ shows "Disconnected" |
| Dashboard back by itself after waking | ✅ | ✅ | ✅ |
Leaving an old phone plugged in all day (and maybe inside a warm PC case) is hard on its battery: heat and age are what make lithium batteries swell. So the top bar shows the phone's own battery:
- Over USB: level and temperature, read with
adbevery 30 seconds. It turns yellow from 38 °C, and from 42 °C (or when Android itself reports "overheat") it turns red and shows an alert with a chime, so you can unplug the phone or give it more air. - Over Wi-Fi: only the level, and only where the browser reports it; browsers don't expose the temperature.
If the back of the phone ever starts to bulge, unplug it and stop using it.
-
Game running, decided in this order:
- never a program you marked Not a game in the SpareScreen window;
- any program in your games: with the game open, click It's a game next to it under Games > Running now in the SpareScreen window (for games installed anywhere, emulators, Minecraft Java...);
- any program running from a store's game library folder (Steam
steamapps/common, Epic Games, GOG, Xbox, Riot, EA, Ubisoft, Battle.net), named after its folder; - with FPS in games turned on (Windows): the full-screen app in front, if PresentMon sees it drawing like a game for a few seconds (browsers, video players and Windows' own programs are never checked). It's named after its window title, uses the window's own icon, and stays a game while it runs, even in the background.
Your lists stay on the PC (they hold file paths); the phone only gets the game's name.
-
In focus: the app of the active window. Windows: yes. Linux on X11: yes with
xdotoolorxpropinstalled (usually already there; on Debian/Ubuntu thex11-utilspackage). Linux on Wayland: Sway and Hyprland only; GNOME, KDE and other Wayland desktops don't let apps see the focused window, so the row is hidden. macOS: yes, via the built-inlsappinfo(no permission prompt). -
Top apps: processes added up per executable (all Chrome tabs count as one), sorted by CPU. The % is of the whole processor.
-
FPS: on Windows, once turned on in the SpareScreen window (see Game mode below).
While a game runs, the page shows only the Game screen (no rotation): the game's art, its icon, the game name, how long it has been running, and the numbers that matter while playing: GPU and CPU usage, temperature, clock and power, VRAM and RAM. When the game closes, the usual screens come back. Turn it off in the settings (Game mode) to keep the normal rotation; the game still shows on the PC screen.
- Art, icon and name, from what each store already keeps on disk (nothing is downloaded):
- Steam: the library image (
appcache/librarycache), found through the game'sappmanifest_*.acf. - Xbox / Game Pass: the splash screen, logo and display name named in the game's
Content/MicrosoftGame.config(orappxmanifest.xml). - GOG: the game's own icon (
goggame-*.ico) and name (goggame-*.info). - Epic: the display name from the launcher's manifests (it doesn't keep the art in a readable form).
- Your games and games detected automatically: their window's icon (Minecraft's block, not Java's cup).
- Otherwise the icon inside the game's
.exe(Windows). Without art, the icon fills the art area, big.
- Steam: the library image (
- FPS (Windows, optional): current FPS, 1% low and frame time over the game art. Turn
it on with Enable next to "FPS in games" in the SpareScreen window. It downloads
PresentMon (free and open source, from
Intel; one pinned version, checked against its published checksum) into
~/.sparescreen/tools, and asks Windows once for admin rights to add you to the Performance Log Users group, which lets programs read frame timings without being admin. Sign out and back in to finish. From then on SpareScreen starts PresentMon only while a game runs, without prompts. Works with any graphics API (DirectX, Vulkan, OpenGL). To undo: remove yourself from that group (compmgmt.msc> Local Users and Groups) and delete the tools folder.
Shows what the PC is playing (Spotify, a browser tab, any player that reports to the system): the Music screen with the album cover, title, artist, album, player and progress (or, with that screen turned off, a small block on the PC screen). Paused media is shown dimmed; with nothing playing the Music screen leaves the rotation.
Covers only come from the PC itself, never from the internet. The player's icon is Windows only.
The Music screen has previous, play/pause and next buttons that go to the player being shown. Only buttons the player accepts are active (Windows and Linux players say which; on macOS all three are offered), and only paired devices can press them, since they act on the PC. Pressing one doesn't change screens.
- Windows: works out of the box. It reads the same info as the volume flyout (System
Media Transport Controls), through the
winrt-Windows.Media.Controlpackages that are installed automatically on Windows. Covers come from the player; the icon from the player's program (or its Store app logo). The buttons use the same media session. - Linux: reads MPRIS players over D-Bus. Install
playerctlfor best results; otherwisedbus-send(part of D-Bus, usually present) is used. Covers only when the player gives a local file (many do; Spotify and browsers give a web address, which is not fetched). The buttons go through the same tool (MPRISPlayPause,Next,Previous). - macOS: best effort. Install
media-control(brew install media-control, works on macOS 15.4 and later) ornowplaying-cli. Without either, the block is hidden. Covers withmedia-controlonly. The buttons use the same tool. AppleScript is not used, so there are no permission prompts.
| Windows | Linux | macOS | |
|---|---|---|---|
| CPU usage, RAM, disks, network | ✅ | ✅ | ✅ |
| CPU temperature | LibreHardwareMonitor | ✅ hwmon | with a free tool (see below) |
| Fans, drive temps | LibreHardwareMonitor | ✅ hwmon | ❌ |
| GPU NVIDIA | ✅ nvidia-smi |
✅ nvidia-smi |
❌ |
| GPU AMD / Intel | LibreHardwareMonitor | AMD ✅ (amdgpu), Intel ❌ |
❌ |
- NVIDIA: read from
nvidia-smi, which ships with the driver. Nothing to set up. - Windows (CPU temperature, fans, drives, AMD/Intel GPUs): Windows only exposes
these sensors to programs that ship their own driver, so SpareScreen reads them from
LibreHardwareMonitor.
The SpareScreen window does it for you: Install (with winget, in a console you can
watch) and Open it, which turns on its web server and "start minimized to the tray"
in its settings and starts it (Windows asks for admin rights, as LibreHardwareMonitor
always needs them). By hand:
- Install it:
winget install LibreHardwareMonitor.LibreHardwareMonitor - Run it as administrator.
- Turn on Options > Remote Web Server > Run (port 8085 by default; SpareScreen reads the port from its settings).
- So you don't have to remember: also turn on Options > Start Minimized, Minimize To Tray and Run On Windows Startup (the last one avoids the admin prompt at every login).
- Install it:
- Linux: CPU temperature (
k10temp/coretemp) and fans come straight fromhwmon; AMD GPUs from theamdgpudriver's sysfs files. No root needed. - macOS: Apple has no public, permission-free API for temperatures. If
smctemp (Intel and Apple Silicon),
macmon (Apple Silicon) or
osx-cpu-temp (Intel) is installed and on your
PATH (e.g.
brew install smctemp), SpareScreen reads the CPU temperature from it. Otherwise the card shows "no temperature sensor".powermetricsisn't used because it needs sudo. - Fans: motherboard fans show up as "Fan 1", "Fan 2"... (the header number). Fans at 0 rpm are hidden, since they're usually empty headers. The GPU fan shows "stopped" when the card turns it off at idle. Every board wires things differently, so SpareScreen doesn't guess which fan is which: in settings you can name them (e.g. "Fan 2" → "Radiator") and pick which one shows on the CPU card (none by default).
- Hot or not depends on the chip. Temperatures turn yellow 15 °C and red 5 °C below the
point where that part starts to slow itself down, read from the hardware where it says:
NVIDIA cards report it through
nvidia-smi, Intel CPUs through LibreHardwareMonitor ("Distance to TjMax"), NVMe drives give their warning temperature, and Linux sensors their own "high" limit. AMD CPUs don't report one, so it comes from a small table by family (e.g. Ryzen 7000: 95 °C). Without any of these, fixed values are used (CPU yellow at 75 °C, red at 90 °C). If a part stays in the red for 30 seconds, the page shows an alert with a chime, once until it cools down (Heat alerts in the settings).
The AI screen shows a card for each tool you're using: one with a session open (or from the last hours) or tokens today. A tool you only used earlier this week shrinks to one line under the cards ("CODEX · 940K this week"), and one you haven't used all week isn't shown, so the tool you actually use gets the room. When none is in use today, they all keep their cards. The "session finished" alert works for every tool, shown or not. Everything is read from the tools' own local files; nothing is sent anywhere.
| Claude Code | Codex CLI | Gemini CLI | |
|---|---|---|---|
| Found at | ~/.claude (or CLAUDE_CONFIG_DIR) |
~/.codex (or CODEX_HOME) |
~/.gemini (or GEMINI_CLI_HOME) |
| Sessions and status | open sessions, working or idle | sessions from the last 12 h, working or idle | sessions from the last 12 h, working or idle |
| Tokens today / this week per model | ✅ | ✅ | ✅ |
| 5-hour and weekly limits | through a status line (see below) | ✅ read from its session files | ❌ not saved on disk |
Codex writes its plan's usage limits into its session files after each message, so they need no setup. They show up after your first Codex message of the day.
Gemini CLI saves each chat (with the tokens of every answer) but not its quota, so its card shows tokens and sessions only. It doesn't write down whether it's working either: SpareScreen works it out from the last message, and counts a session as finished about 20 seconds after an answer with nothing new (Gemini CLI may still be running tools then), so its "session finished" alert comes a little late.
Claude Code doesn't write its 5-hour and weekly limits to disk, so SpareScreen has two
ways to get them. Both save to ~/.sparescreen/claude-limits.json, and the card shows
when they were last updated.
Option 1: limits check (works with the desktop app and IDEs). Turn on
Settings > Check Claude limits. While you are using Claude Code, every 10 minutes the
agent runs one tiny request in Claude Code's headless mode (claude -p, cheapest model,
no tools, nothing saved to your history) and reads the rate_limit_event that Claude
Code reports. It only runs while a session is active, so it never starts a new 5-hour
window on its own. It needs the claude command on the PATH and spends a very small
amount of your plan's usage. Off by default.
Option 2: status line (terminal only). Claude Code hands these numbers to its
status line, so SpareScreen ships a status line command that saves them. In
~/.claude/settings.json:
"statusLine": {
"type": "command",
"command": "PATH/TO/SpareScreen/.venv/Scripts/sparescreen-statusline.exe"
}(On Linux/macOS: .venv/bin/sparescreen-statusline.) The status line also shows the
model, context usage and limits in Claude Code's own bar, e.g. Opus · ctx 34% · 5h 23% · week 41%.
If you already have a status line, you don't have to give it up. Put your current command
after -- and SpareScreen runs it for you. It gets the same input from Claude Code, its
output (including multi-line output) is shown unchanged, and SpareScreen still captures
the usage limits in the background. If your command fails, prints nothing or takes longer
than 5 seconds, SpareScreen's own short status line is shown instead. Commands that need
pipes or other shell features can be passed as one quoted argument, e.g.
-- "my-status | head -1". You can also put the command in the
SPARESCREEN_STATUSLINE_CMD environment variable instead of after --.
{
"statusLine": {
"type": "command",
"command": "sparescreen-statusline -- bash ~/.claude/my-status.sh"
}
}With the downloaded executable, use sparescreen-windows-x64.exe statusline (or the Linux/macOS
file) as the command, with the same -- form.
Limitations:
- Only on Pro and Max plans.
- The status line only runs in the terminal; the desktop app and IDEs don't run it (use option 1 there).
- Model-specific weekly limits (e.g. "Week · Opus") show up when Claude Code reports them. The card also shows the tokens used this week per model, counted from this PC's history (usage on claude.ai or other computers is not included).
Run sparescreen --dev. The agent restarts itself when a .py file changes, and the
page on the phone reloads by itself when something in sparescreen/web/ changes or the
agent restarts. No need to touch the phone.
The page is a frame (index.html) that stays full screen and loads the dashboard
(app.html) inside it. Only the inner page reloads, so the phone stays full screen.
Run the tests (standard library only, no extra install):
python -m unittest discover -s tests -t .They also run on GitHub Actions on Windows, Linux and macOS for every push.
The Python code is fully typed (every function's parameters and return) and checked with mypy in CI, for each system's code paths:
pip install mypy types-psutilpython -m mypy --platform win32(--platform linux and --platform darwin too). Shared data shapes live in
sparescreen/shapes.py; every module starts with from __future__ import annotations,
so the annotations use modern syntax while the code still runs on Python 3.9.
sparescreen --demo shows made-up data, handy when working on the page; it keeps its own
settings and paired devices in ~/.sparescreen/demo, so it never changes yours.
Releasing: python packaging/bump.py 1.2.3 sets the version (it lives only in
sparescreen/__init__.py) and dates the changelog's Unreleased entry. Commit, push and
publish the GitHub release v1.2.3; the release workflow builds the executables.
python packaging/make_icons.py redraws the app icons when the logo changes.
The page must keep working on old browsers (Chrome 49 / iOS 10): plain JavaScript, no modern syntax (a test checks this). See CLAUDE.md for the project guidelines.
- The phone can't open the page (Windows): the first time, Windows asks whether Python may accept connections. Allow it on private networks. If your Wi-Fi is set as public, switch it to private or allow port 8765 in the firewall.
- Privacy on Wi-Fi: only paired devices get data, but the connection itself is plain
HTTP on your local network: someone on the same network could read it, or pretend to be
the PC (or its
.localname) to catch the phone's pairing. On a network you don't trust, use USB only with--host 127.0.0.1. - Lost the pairing code? It's in the SpareScreen window (and printed when the agent
starts). A new one is made every time it starts and after every pairing, so a code
works once. The paired device keeps working;
--reset-pairingforgets it. - "Too many attempts": after 5 wrong codes, pairing pauses for a minute (twice as long each time it happens again, up to an hour) and the code changes.
- Red dot in the top corner / "Disconnected" screen: the phone lost its connection to the agent (the dot right away, the full screen after 30 seconds, with the time of the last data). The page reconnects by itself when the agent is back, and reloads itself when SpareScreen was restarted or updated.
- The phone still shows an old page after an update (no new screens or buttons): it was opened before 0.12 and doesn't know how to reload itself. Reload it once by hand; from then on it does it by itself.
sparescreen/sources.py: CPU, RAM, network and disk usage viapsutil.sparescreen/sensors.py: temperatures, fans, drives and GPU (nvidia-smi, LibreHardwareMonitor, hwmon, amdgpu).sparescreen/claude.py: reads~/.claude(orCLAUDE_CONFIG_DIR). Open sessions come fromsessions/*.json; tokens come from the last 8 days ofprojects/**/*.jsonlhistory, reading only the new part every 5 seconds and adding up 15-minute buckets. These are Claude Code's internal formats and may change between versions.sparescreen/codex.py: reads~/.codex/sessions/**/rollout-*.jsonl(orCODEX_HOME): sessions, cumulative token counts turned into per-day usage, and the usage limits Codex stores with them. Same output shape asclaude.py, so the page draws both the same way.sparescreen/gemini.py: reads Gemini CLI's chats in~/.gemini/tmp/*/chats/(orGEMINI_CLI_HOME): tokens per answer (counted once per message, as answers are written again when they change) and sessions. Same output shape again.sparescreen/statusline.py: the status line that saves the limits for the agent.sparescreen/claude_probe.py: the optional limits check (headlessclaude -p, only while Claude Code is in use).sparescreen/server.py: standard-library HTTP server. The page gets the data through Server-Sent Events on/events, once per second.sparescreen/history.py: the last 5 minutes of usage, temperatures and network speed (a point every 2 s, averaged), sent once on/historyso a page that just opened starts with full charts; the page adds the live points itself (web/app/23-history.js).sparescreen/usb.py: every 3 seconds checksadb devices, sets upadb reverseon connected phones and opens the page on them; every 30 seconds reads their battery (dumpsys battery) for the top bar.sparescreen/activity.py: app in focus and top apps;sparescreen/games.pydecides which one is the game (your lists, store folders, automatic) and finds its art and icon.sparescreen/media.py: now playing (Windows media controls, MPRIS on Linux, optional tools on macOS).sparescreen/gameart.pyandsparescreen/icons.py: Steam's cached game art and program icons.sparescreen/photos.py: the Photos screen's folder (scanned every 30 s), its photos served at/photosand/photo/<id>(paired devices only, screen-sized with Pillow), and the folder picker.sparescreen/images.py: covers, game art and icons kept in memory and served at/art/<name>(paired devices only, like the data).sparescreen/config.py: preferences saved in~/.sparescreen/config.json(POST/config).sparescreen/pairing.py: pairing code, device tokens (only their hashes are stored, in~/.sparescreen/devices.json) and the lockout after wrong codes.sparescreen/cli.py: the command line (python -m sparescreenand the executables run it).sparescreen/autostart.py:--autostartper OS.sparescreen/update.py: the check for a newer release and the in-place update of the executable (download,SHA256SUMS, a--versiontest run, swap and restart).sparescreen/tray.py: the tray icon in background mode (pystray).sparescreen/panel.py,sparescreen/appwindow.pyandweb/panel.*: the SpareScreen window on the PC (checks, devices, pairing) and how it opens as an app window.sparescreen/power.py: the PC about to sleep (Windows power notifications, logind'sPrepareForSleepon Linux), so the phone shows "PC asleep".sparescreen/lhm.py: finds, sets up, starts and installs LibreHardwareMonitor on Windows.sparescreen/fps.py: game FPS from PresentMon (Windows): setup, and frame times turned into FPS, 1% low and frame time while a game runs.sparescreen/names.py: the PC's names the phone may reach it by (tested from the phone through/whoami; a one-time code carries the pairing to the new address).packaging/entry.pyand.github/workflows/release.yml: the PyInstaller executables built for every release.sparescreen/dev.py:--devmode (a supervisor that restarts the agent, plus the code version).sparescreen/hwinfo.py: short CPU, GPU and memory names.sparescreen/demo.py: made-up data for--demo, same shape as the real sources.sparescreen/web/: the page (plain HTML, CSS and JS, built for old browsers).index.htmlis just the frame; the dashboard isapp.html,style.cssand its script, kept inweb/app/one file per part (screens, settings, alerts, pairing...) and joined in order intoapp.jsby the agent when the page asks for it.
SpareScreen stands on the work of these free and open source projects. Thank you!
Inside SpareScreen (installed with it, or bundled in the executables):
| Project | What it does here | License |
|---|---|---|
| psutil | CPU, memory, disks, network and processes | BSD-3-Clause |
| pywinrt | Now playing and album covers on Windows | MIT |
| pystray | The tray icon | LGPL-3.0 |
| Pillow | Images for the tray icon | MIT-CMU |
| PyInstaller | Builds the executables | GPL-2.0+ with bootloader exception |
| Feather Icons | Some of the dashboard's icons | MIT |
Programs SpareScreen talks to when they're on your PC (not part of SpareScreen; each keeps its own license):
| Project | What for | License |
|---|---|---|
| LibreHardwareMonitor | CPU temperature, clocks, fans and drives on Windows | MPL-2.0 |
| PresentMon | Game FPS on Windows (downloaded when you turn it on) | MIT |
Android Platform Tools (adb) |
The USB cable connection | Apache-2.0 |
nvidia-smi (comes with the NVIDIA driver) |
NVIDIA GPU readings | NVIDIA's driver license |
| playerctl | Now playing on Linux | LGPL-3.0 |
| media-control | Now playing on macOS | see its repository |
| nowplaying-cli | Now playing on macOS (fallback) | GPL-3.0 |
| smctemp | CPU temperature on macOS | GPL-2.0 |
| macmon | CPU temperature on Apple Silicon | MIT |
| osx-cpu-temp | CPU temperature on older Macs | GPL-2.0 |
Also used when present: xdotool/xprop, swaymsg and hyprctl (app in focus on Linux),
dbus-send (now playing on Linux) and lsappinfo (app in focus on macOS).
MIT: use, copy, modify and distribute it freely, including in commercial projects, as long as you keep the license notice.









