Create App Overlays
PolyKybd can draw a shortcut icon on each keycap OLED for whichever app is focused — see Context-Aware Overlays for the user-facing side. This HowTo builds a new overlay set for an application by hand, the same way the shipped overlays were made.
An overlay is just an image you paint in an editor like GIMP, plus a
one-line entry telling the host which app it belongs to. The editable source files for every
built-in overlay are in the host repo under
polyhost/res/overlays/src/ —
a great starting point (and template.xcf there is a blank grid). The authoritative format spec is
polyhost/res/overlay_specification.md.
How an overlay image is laid out
Section titled “How an overlay image is laid out”The host loads one or two PNGs per app. Each PNG is a 720×360 image = a 10×9 grid of 72×40 keycap cells, in row-major key order, and each colour channel of a cell is a different modifier variant of that key:
| File | Red | Green | Blue | Alpha |
|---|---|---|---|---|
*.mods.png (primary) |
Ctrl | Alt | Shift | no modifier |
*.combo.mods.png (combo) |
Ctrl+Shift | Ctrl+Alt | Alt+Shift | GUI |
*.extra.mods.png (extra) |
Ctrl+Alt+Shift | GUI+Shift | GUI+Alt | GUI+Ctrl |
*.gui.mods.png (GUI combos) |
GUI+Ctrl+Shift | GUI+Alt+Shift | GUI+Ctrl+Alt | GUI+Ctrl+Alt+Shift |
*.png (plain) |
— | — | — | grayscale → no modifier only |
So one RGBA image holds four layers of the same keyboard at once — you can see it in GIMP’s Channels panel, each of Red / Green / Blue / Alpha carrying the icons for a different modifier:

White pixels are drawn (lit) on the OLED; everything else is ignored. Encode as straight (non-premultiplied) RGBA — a transparent pixel must still carry its R/G/B bytes, because those channels are the Ctrl/Alt/Shift variants, independent of alpha (see the export step below).
The firmware has these fixed limits:
- All sixteen modifier combinations work, every mix of Ctrl, Shift, Alt and GUI. Draw the GUI
chords in the
extraandGUI combosfiles above. - Repeating the same icon across several variants uses no extra space: the host detects byte-identical images and stores them once on the keyboard, so only different artwork uses space.
- Cells are 72×40, 1-bit monochrome on the OLED — draw simple, high-contrast icons.
- Only the 90 mapped keys carry a cell (
A–Z,0–9,F1–F12, punctuation, the nav cluster, modifiers). Keypad and media keys have none. - Put the icon in the bottom-right of the cell so it never covers the firmware-drawn key letter in the top-left.
Keys without a legend
Section titled “Keys without a legend”The keyboard draws the key’s legend first, clears 3 px around your icon, and then draws the icon. An icon that fills the whole cell, such as a large program mark on Esc, would leave pieces of the legend around it. From firmware 1.9.13 and PolyKybdHost 1.17.52 a key can be drawn without its legend: the keyboard clears the whole cell first.
If you build the set with the overlay generator (scripts/generate_app_overlays.py), mark the key
in the spec:
program_icon: program.pngprogram_icon_region: [72, 40]program_icon_legend: false # the Esc mark, on every modifier layer
bindings: - { key: F5, icon: refresh.png, label: "Refresh", legend: false }The generator writes the marked keys into a polykybd-no-legend text chunk of the PNG. A PNG
painted by hand can carry the same chunk. The chunk is a list of <modifier>:<keycode> pairs,
separated by spaces, both in decimal:
<modifier>is the modifier bitmask:0none,1Ctrl,2Shift,4Alt,8GUI. Add the values for a combination, for example3for Ctrl+Shift.<keycode>is the HID keycode of the key, for example4for A,41for Esc and62for F5.
The chunk 0:62 1:62 0:41 marks F5, Ctrl+F5 and Esc. Use it only where the icon replaces the keycap: on a normal shortcut
icon the legend tells you which key it is. With older firmware the key keeps its legend and the
icon is drawn over it.
Build it by hand
Section titled “Build it by hand”-
Start from the template. Copy
polyhost/res/overlays/src/template.xcf(the blank 720×360 grid) — or an existing app’s…_template-RGBA.xcfthat’s close to what you want — and open it in GIMP. Working from a shipped source is the quickest way to see how the channels and cell grid are organised. -
Look up the app’s real shortcuts. From the app’s own menus, help, or shortcut reference. Note the modifier each one uses — every combination of Ctrl, Shift, Alt and GUI can be drawn, so nothing has to be dropped for lack of a channel.
-
Draw an icon per shortcut. For each shortcut, place a small, clear icon into that key’s cell, on the channel for its modifier (Ctrl → red, Alt → green, Shift → blue, no-modifier → alpha). Keep it bottom-right. The app’s own icons are the easiest to recognise; simple monochrome art stays readable on the 1-bit OLED far better than a detailed logo.
-
Export the PNG(s). Export the image to
polyhost/res/overlays/<app>_template.mods.pngas 720×360 RGBA. Add each further tier only if you drew for it —<app>_template.combo.mods.pngfor the combo layers (Ctrl+Shift / Ctrl+Alt / Alt+Shift / GUI),<app>_template.extra.mods.pngfor Ctrl+Alt+Shift and the two-modifier GUI chords, and<app>_template.gui.mods.pngfor the three- and four-modifier GUI chords. Most apps never need that last one. If you only need plain, unmodified icons, a single grayscale<app>_template.pngis enough. -
Tell the host which app it’s for. Add an entry to
polyhost/res/overlay-mapping.poly.yaml, keyed on the app’s window class / process name (comma-separate aliases), pointingoverlay:at your PNG(s):myapp,myapp.exe:overlay: [myapp_template.mods.png, myapp_template.combo.mods.png]When one process serves many contexts (a browser hosting several web apps, an IDE), narrow it with a sub-map. The host tries them in a fixed order and uses the first match:
Key Matches on How urls-contains:the focused tab’s URL plain substring titles-startswith:the first word(s) of the title whole words titles-endswith:the last word(s) of the title whole words titles-contains:any word, or run of adjacent words, in the title whole words chrome,google-chrome,chromium:overlay: [chrome_template.mods.png] # plain browsingurls-contains:atlassian.net: { overlay: [jira_template.mods.png] }titles-contains:Jira: { overlay: [jira_template.mods.png] } # used when no URL is knownSee the existing Chrome and JetBrains entries for the full pattern.
If the app’s shortcuts differ per platform, give it an
os:branch. Sublime Text does exactly this —Cmdon macOS,Ctrlelsewhere:sublime_text:overlay: [sublime_template.mods.png, sublime_template.combo.mods.png] # the defaultos:macos:overlay: [sublime_mac_template.mods.png, sublime_mac_template.combo.mods.png,sublime_mac_template.extra.mods.png, sublime_mac_template.gui.mods.png]Accepted keys are
windows,macos,linux, pluslinux_gnome/linux_kdewhen a desktop environment needs its own artwork (a plainlinux:branch still covers those two). Anything the host can’t identify falls back to the entry’s ownoverlay:, so the default is never skipped.One file for both platforms:
Section titled “One file for both platforms: CMDCTRL”CMDCTRLWriting the two sets by hand means maintaining the same shortcut list twice, and the two lists become different over time. Instead, a binding file for
scripts/generate_app_overlays.pycan useCMDCTRL— the OS-relative command modifier,Ctrlon Windows/Linux andCmdon macOS:bindings:- { key: P, mods: [CMDCTRL, SHIFT], icon: palette.png, label: command palette }From one spec, the generator renders
<output>.*withCMDCTRLas Ctrl and<output>_mac.*with it as Cmd, then emits theos: macos:branch selecting between them. SpellingsCMDCTRL,CMD_OR_CTRLandCMDORCTRLall work — GTK calls the same idea<Primary>and Electron calls itCmdOrCtrl.A plain
CTRLbinding stays literal Ctrl on every platform, which matters: macOS keeps real Ctrl chords of its own, and apps do too. Both spellings coexist in one file.When one platform diverges:
Section titled “When one platform diverges: only: / except:”only:/except:CMDCTRLsplits Windows from macOS — but Windows and Linux share one PNG, so an action an app remaps on only one of them cannot be represented. Two optional keys scope a binding to the platforms it is actually correct on:bindings:# Right on Windows and macOS; Linux moves this one to a two-stroke chord.- { key: U, mods: [CMDCTRL, SHIFT], icon: output.png, label: Output, except: [linux] }# The other shape: one icon, a different chord per platform.- { key: LEFT, mods: [ALT], icon: goback.png, label: Go back, only: [windows, macos] }- { key: "-", mods: [CTRL, ALT], icon: goback.png, label: Go back, only: [linux] }Valid platforms are
windows,macosandlinux. A binding with neither key applies everywhere. Useonly:orexcept:, never both.VS Code’s Show Output is an example: Linux moves it to the chord
Ctrl+K Ctrl+H, which a keycap cannot draw. Scoping it out leaves Linux a blank key, and Windows and macOS keep the icon.A third artwork set (
<output>_linux.*, plus anos: linux:branch) is generated only when a binding treats Linux differently from the default.CMDCTRLalone never triggers it. -
Test it. Focus the app with the keyboard connected — the overlay loads automatically. Run PolyKybdHost with
--dev 2to watch the overlay HID traffic if a key doesn’t show.
See Contributing for how to open the pull request, and Display Graphics & Fonts for how overlays are rendered on the device.