A Mac captures one virtual display, turns each frame into a grayscale PNG and serves it over HTTP. A shell loop on a Kobo Libra 2 fetches it and paints it on e-ink. What it does, what had to be worked around, and the limits, which are not small.
I did not need another display. I have enough of those.
I wanted a terminal on e-ink. It looks like the console of a spaceship.
So a Mac captures one virtual display. Each frame becomes a grayscale PNG, 1264 by 1680, and goes out over HTTP. On a Kobo Libra 2 a shell loop fetches the PNG and paints it on the e-ink screen. That is the whole project.
It is on GitHub with its limits written down. The limits are most of this post.
1. The pipeline
There are two halves and one Wi-Fi network between them.
Mac Kobo Libra 2virtual display NickelMenu -> start.shscreen-server.py loop.sh, each cycle: screencapture -D<n> Wi-Fi GET /cmd.sh, run on change -> grayscale PNG 1264x1680 HTTP GET /screen.png GET /screen.png fbink -g file=screen.png GET /cmd.sh POST /report
On the Mac, screen-server.py captures the display four times a second by default and keeps the last frame in memory. A request gets that frame at once.
On the Kobo, a menu entry called "Screen push" starts . It copies FBInk and the loop to , paints one test line, sends diagnostics to the Mac and leaves the loop running. The loop fetches the PNG, compares its checksum with the previous one, and paints only when it changed. Then it sleeps 0.3 seconds.
start.sh
/tmp
2. Why screen sharing did not fit
macOS already has a way to show a screen on another device. Screen Sharing serves all displays as one large framebuffer. An e-ink panel in portrait does not want all displays. It wants one.
So the server captures one display by index, with screencapture -D<n>. Two details are now comments in the code.
The stdout mode of screencapture returns nothing when a display index is given, on recent macOS. The server writes to a temporary file and reads it back.
The mouse cursor is left out of a capture by default. It needs a flag, -C.
The display itself is virtual. The setup script looks for DeskPad and finds which capture index the virtual display got. The old VNC path is still in the repository as a fallback script. The pipeline above does not use it.
3. Making a desktop readable
The macOS interface is low-contrast light grey. E-ink renders it almost invisibly.
The server does five things to each frame.
It converts to 8-bit grayscale. FBInk dithers to the panel's own levels.
It rotates the frame 90 degrees counter-clockwise, when asked. A landscape capture placed in a portrait panel is a thin band in the middle. Rotated, a 1680 by 1264 capture fills 1264 by 1680.
It scales the frame to fit the panel and centres it on white.
It applies a fixed levels map. Everything at or below 90 becomes black. Everything at or above 205 becomes white. The middle goes through a gamma of 2.2, so grey text in window titles turns dark.
It sharpens the edges with an unsharp mask.
The map is fixed, not automatic. Automatic contrast stretches the uniform grey of a nearly empty desktop into a solid black band.
On the Kobo side, frames are painted with the GL16 waveform by default. Every sixteenth frame is a full flashing refresh, which clears ghosting. The interval, the waveform and the poll rate sit in one config file that the loop re-reads on every cycle.
4. The command channel
This is the part to read before installing anything.
Inbound SSH to the Kobo is unreliable. The README says to use the command channel instead. Here is what the command channel is.
On every cycle the Kobo asks the Mac for /cmd.sh. When the content differs from the last one it ran, the loop executes it with sh, as root, and posts the output back to /report. The first fetch after the loop starts also runs whatever file is already there.
The server has no authentication, no encryption and no access control. From the README:
any host that can reach the port can read the captured screen
it can fetch the command file
it can append data of any size to the report file on the Mac
whoever can write the command file, or intercept or spoof the plain HTTP between the two devices, can run commands as root on the Kobo
The default is to listen on all interfaces.
The README's advice is short. Run the server only on a trusted private network. Bind it to one interface with --bind. Stop it when not in use. Delete kobo-cmd.sh afterwards.
The command channel is a remote root shell over plain HTTP. The README has a paragraph asking people to be careful with it. Both are in the repository.
5. What does not work
Sleep handling is incomplete. The firmware allows at most 255 minutes before auto-sleep, so the Kobo sleeps after about four hours of idle time. Sleep stops the loop and drops Wi-Fi. A tap wakes the device. Keeping it awake by injecting input events is not implemented.
"Screen push" has to be started by hand after each boot. There is no autostart.
Transport is Wi-Fi only. USB networking is not implemented.
Waveform and orientation settings are not tuned.
A server started by launchd may lack Screen Recording permission. Then the frames are blank.
There is no teardown script. You stop the loop from the menu, stop the server with kill, and restore the power settings from the backup that the setup wrote next to the original file.
FBInk is not in the repository. You install the fbink binary and its library yourself, from the FBInk project's releases.
Only firmware 4.38 on a Libra 2 was used. That is a sample of one.
6. Setup, briefly
You need a Kobo Libra 2 on stock firmware with NickelMenu, your own copy of FBInk with image support, DeskPad or another virtual-display tool, and Python 3 with Pillow on the Mac. The terminal that starts the server needs Screen Recording permission.
Set the Mac's address in kobo-files/config.sh. Then, on the Mac:
./provision.sh --mac
This also turns on macOS Screen Sharing with sudo if it is off. Only the old VNC path uses it. If you do not use that path, switch Screen Sharing off again in System Settings.
With the Kobo connected by USB:
./provision.sh --kobo --mac-ip 192.0.2.10 --eject
The address is an example from the documentation range. Use the Mac's own. The second command copies the files to the device, installs the menu entries and sets three power options, with a backup. Restart the Kobo, then choose "Screen push".
The README has the configuration table and the troubleshooting list.
Site note
The site's terms were updated on 11 November 2026: the venue clause now names the courts of Haifa.