Mini Browser is a compact, interactive web browser for the WHY2025 badge, written in C using SDL3 and libcurl.
Version 4.4 completes the road towards a browser that feels like a modern mobile browser while staying small. It adds find in page, a focus ring for links and fields, text zoom, a simplified (reader) view for long articles, pages saved for offline reading, sharing a page as a QR code, a Page Information screen with a lock for secure pages, and a built-in help page: press WHY+/ (the /? key) to see every key.
Version 4.3 added a loading line with progress, a stop key, clear error pages, an instant Back/Forward cache, compressed (gzip) pages, connection reuse, cookies that are kept after a restart, a 2 MB disk cache, downloads, and images that load after the text. Version 4.1 added an omnibox for addresses and searches with suggestions, a persistent history of visited pages and up to five tabs.
Version 3.0 built on the Unicode, forms, cookies and HTTP/TLS foundations of the 2.x releases with PNG, JPEG and GIF image support, inline image rendering, image sizing and scaling, multiple display modes and fullscreen BadgeVMS operation. 4.x retains the 56,352-glyph Unicode font, word-aware wrapping, RTL/Bidi rendering, contextual Arabic shaping, bookmarks, history, GET/POST forms, cookies and complete-page screenshots.
ASCII uses the built-in bitmap font. Additional Unicode characters can be loaded from APPS:[mini_browser]unifont_cjk.bin. The tested generated font contains 56,352 usable glyphs across 101 ranges selected from GNU Unifont 17.0.02 Plane-0 and supplementary-plane sources. The MBCJ v1 range table is read from the file header at runtime rather than being hard-coded in the browser source.
Coverage includes broad Latin extensions (including Icelandic), Greek, Cyrillic, Armenian, Hebrew, Arabic and Arabic presentation forms, Indic scripts, Thai, Lao, Tibetan, Georgian, Ethiopic, Cherokee, Runic, Hangul/Korean, Hiragana, Katakana, Bopomofo, CJK Extension A, CJK ideographs, Yi, punctuation, currency, mathematical and technical symbols, fullwidth forms and supplementary symbol/emoji ranges.
Wrapping is UTF-8 safe, pixel aware and word aware. Complete words stay together where possible. CJK text and overlong unbroken tokens fall back to UTF-8-safe glyph-level wrapping. ASCII and 16x16 Unicode glyphs have different rendered widths, and Mini Browser accounts for that when laying out lines.
If the external Unicode font is not installed, the built-in ASCII renderer remains available. Unordered lists intentionally use the ASCII * marker rather than requiring a Unicode bullet.
Mini Browser renders Hebrew and Arabic right-to-left and supports mixed LTR/RTL lines. Arabic letters are contextually shaped into their joining forms using glyphs included in the Unicode font.
The implementation is deliberately compact for BadgeVMS and is not a claim of complete support for every Unicode Bidirectional Algorithm control or every complex-script shaping rule.
HTML b and strong elements are rendered as genuinely bold text. Mini Browser no longer shows literal ** markers for these elements. Bold is produced by an additional one-pixel horizontal glyph pass and works for both ASCII and Unicode glyphs.
Many single-codepoint Plane-1 emoji can be rendered from the external GNU Unifont bitmap file, for example: 😀 😃 😂 😎 🤖 👍 🐶 🍕 🚀 🌍 💡 💻
These are monochrome bitmap glyphs, not color emoji. Complex ZWJ sequences, skin-tone composition, gender sequences and regional-indicator flag composition are not implemented. Variation-selector handling is limited.
Mini Browser supports simple GET and POST forms. Supported input types include text, search, url, hidden and submit. Submit buttons made with the button element are also supported.
[1] q: [2] [Search]
Type 1 and press Enter to edit the field. Enter the search text and press Enter again to store it. Then type 2 and press Enter to submit.
GET and POST form data is URL encoded as application/x-www-form-urlencoded. Hidden fields are submitted, disabled fields are ignored, and an activated named submit button is included where appropriate. POST request bodies are bounded to 2048 bytes. Up to 4 forms with up to 8 fields per form are supported.
Textarea, select, checkboxes, radio buttons, file uploads and JavaScript-driven forms are not currently supported.
Mini Browser understands useful structure from ordinary HTML, including:
Unordered list items use an ASCII * marker for compatibility with installations that do not include the external Unicode font.
Mini Browser 3.0 adds bounded JPEG, PNG and GIF image rendering. Images are sized/scaled for the badge display. Press WHY+O to open the display mode options and choose between four rendering modes.
Mini Browser now uses the BadgeVMS fullscreen window path via SDL_WINDOW_FULLSCREEN, removing the normal BadgeVMS window decorations and allowing the browser to use the display area directly.
Known limitation: in Mode 4, complete full-page screenshots are currently limited to five images. This is a documented resource limitation for 3.0.
Press WHY+L to open an empty address bar, like Ctrl+L in a desktop browser. Type a web address or a few words and press Enter:
WHY+E still opens the bar with https:// already filled in, as before. A word without a dot typed after it, such as esp32, now also becomes a search instead of a failing address.
While you type, up to five suggestions appear under the bar: bookmarks first (yellow, marked *), then visited pages, matching the address or the page title. Down and Up select a suggestion, Enter opens it, and Esc closes the bar and returns to the current page.
Mini Browser can keep up to five pages open in tabs. WHY+T opens a new tab next to the current one with a new tab page that lists your bookmarks and recently visited pages, and opens the omnibox. Each tab keeps its own page, scroll position and Back/Forward list. With more than one tab open, the bar shows which tab you are on, for example [2/3] Page title.
Tabs in the background keep their text; their images are loaded again when the tab returns to the front.
While a page loads, the bar shows how much has arrived (for example Loading 12 KB of 40 KB - Esc stops) with a progress strip under it. The current page stays visible until the new one is ready. When the page has images, the bar shows Image 2 of 5 while they arrive.
Press Esc to stop loading:
A page that cannot be loaded is shown as a clear error page with what went wrong, for example This site can't be reached (the server IP address could not be found, refused to connect, took too long to respond), a secure connection or certificate problem, This page can't be found (HTTP 404) or This page isn't working (HTTP 5xx), with a code such as ERR_NAME_NOT_RESOLVED or HTTP ERROR 404. Press R or Enter to try again, or WHY+B to go back.
Stop and the precise error messages need the BadgeVMS 4.3 firmware (see Network behaviour); on older firmware pages load as before.
The text of a page is shown as soon as it has arrived; its images follow one by one while you read, and the bar shows [img 2/5] meanwhile. Esc stops the remaining images (and no longer quits the browser while they load). Scrolling and other keys keep working.
Cookies with Expires or Max-Age are kept after a restart in APPS:[mini_browser]cookies.txt; session cookies stay in memory only, as in other browsers. Up to 32 cookies are kept, scoped to the site that set them.
Pages and images are cached on flash in FLASH0:[MBCACHE] (2 MB, least recently used entries go first). A cached copy that is still fresh is used without a request; otherwise the browser asks the server with If-None-Match / If-Modified-Since and a 304 Not Modified answer shows the cached copy. WHY+R always asks the server. POST results, error pages and no-store responses are never cached.
A link to a file the browser cannot show (zip, pdf, mp3, ...) or a response sent as an attachment asks Download?: Enter or Y saves it in FLASH0:[DOWNLOADS] (max 4 MB per file), Esc or N cancels. WHY+D lists the downloads; WHY+X on that page deletes them. WHY+X on the Page Information page clears all cookies and the disk cache.
Press WHY+F and type a word: the matches light up while you type (the current one in orange) and the bar shows Find: word 3 of 12. Enter closes the find bar and keeps the highlights; n and Shift+N (or Down and Up in the find bar) go to the next and previous match, and Esc turns them off. Upper and lower case are treated the same.
Tab and Shift+Tab move a cyan ring over the links and form fields of the page, scrolling it into view; Enter opens the one with the ring. Typing a number and Enter still works as before.
WHY+= and WHY+- change the page text between 100%, 150% and 200%; WHY+0 returns to 100%. The page is wrapped again for the new size, the bar shows the size when it is not 100% (for example 150% Page title) and the Options page (WHY+O) shows it too. The bar and menus keep their size. The zoom is for the current session only.
Many pages surround their text with menus, sidebars, share buttons and footers. On a long article Mini Browser offers a Show simplified view chip at the bottom of the screen; WHY+V then shows only the article: its title, text and images. WHY+V again returns to the full page. The chip goes away when you scroll down.
The article is found by scoring the containers of the page (paragraphs, commas, text length, link density and class names such as article, content, nav or sidebar), in the spirit of Firefox's Readability. Pages that need JavaScript often still read well in the simplified view.
WHY+P saves the text of the page (or of its simplified view) as a text file in FLASH0:[SAVED], to read later without a network. Saving the same page again replaces the earlier copy. The Downloads page (WHY+D) lists saved pages under SAVED PAGES; open one by its number. WHY+X on that page deletes the downloads and the saved pages.
WHY+U shows a QR code of the page address, to open it on a phone, and sends the line SHARE <address> on the USB serial port. A user:password@ part of an address is never shared. Any key returns to the page.
WHY+/ (the /? key) opens a help page that lists every key, grouped by task, together with where Mini Browser keeps its files. It scrolls and zooms like any page; WHY+/ or WHY+B returns to the page you came from.
Links and visible form controls receive action numbers. Type an action number and press Enter to activate it.
Use Up and Down to scroll; holding an arrow gives continuous scrolling. J and K provide one-line scrolling, PageUp/PageDown a screen and Home/End the top and bottom of the page. Tab and Shift+Tab move the focus ring over links and fields (4.4); Enter opens the one with the ring. Esc stops a page that is loading or its images (4.3), clears find matches (4.4), and quits when nothing else is open. R reloads on an error page (4.3).
Mini Browser supports two screenshot modes. WHY+S captures the visible 716x716 browser viewport. WHY+Z captures the complete rendered page, including content below the viewport. Full-page capture reuses the normal renderer as a slice-based scratch surface, so it does not allocate a giant 716xN framebuffer or change the current scroll position. Both modes use RGB24 data from the SDL renderer, lightweight RLE5 compression, per-record CRC32 and XOR forward-error correction, transmitted over USB serial.
The Mini Browser repository includes the standalone receiver badge_screenshot.py. Start it on a serial-connected macOS or Linux computer and then press WHY+S on the badge:
./badge_screenshot.py /dev/cu.wchusbserial10
For a complete rendered page:
./badge_screenshot.py /dev/cu.wchusbserial10 --full-page
On Linux the device will commonly be something such as /dev/ttyUSB0. The receiver validates the transfer, repairs a single missing or damaged data record per FEC group where possible, verifies the complete CRC32, decodes the RLE stream and writes a PNG.
A custom firmware is not required for screenshots triggered physically with WHY+S. The screenshot-enabled Mini Browser and a USB serial connection are sufficient.
The receiver can also request the screenshot from the host:
./badge_screenshot.py /dev/cu.wchusbserial10 --request ./badge_screenshot.py /dev/cu.wchusbserial10 --full-page --request
The --request mode requires the custom firmware because stock BadgeVMS firmware does not implement the host-to-badge serial keyboard protocol.
A customized WHY2025 firmware is available at github.com/mactjaap/firmware. It adds a serial keyboard bridge to the normal BadgeVMS keyboard path, allowing a macOS/Linux computer to send keyboard events to Mini Browser over USB serial.
The firmware repository includes badge_keyboard.py for interactive computer-keyboard control of the badge. This also allows browser accelerator commands such as WHY+E, WHY+H, WHY+R, WHY+B, WHY+G, WHY+K, WHY+O, WHY+S, WHY+Z and WHY+Q to be generated remotely.
The same repository contains automated Mini Browser test scripts. They can drive navigation, open configured websites, perform searches, exercise browser commands and automatically request and save screenshots after test steps. This provides repeatable regression testing without manually operating the badge for every page.
The regression suite mini-browser-3.0-regression-selectable.py also covers the 4.1 omnibox and history (tests 57-63), tabs (tests 64-67), the 4.3 Back/Forward cache, stop key, error pages and gzip (tests 68-72), the disk cache, downloads and cookies (tests 73-77), background images (test 78) and the 4.4 find, focus ring, zoom, simplified view, saved pages, share, Page Information, WHY+K bookmarks and help (tests 79-87). Each loaded page stays on screen for three seconds so the run can be followed on the badge; --view N changes the pause and --fast removes it.
./mini-browser-3.0-regression-selectable.py --list ./mini-browser-3.0-regression-selectable.py --test 57-63 ./mini-browser-3.0-regression-selectable.py --test 68-78 ./mini-browser-3.0-regression-selectable.py --test 79-87 ./mini-browser-3.0-regression-selectable.py --fast
The custom firmware is optional for normal Mini Browser use and physical WHY+S screenshots. It is required when a host must send keyboard commands to the badge, including fully automated tests and badge_screenshot.py --request.
The repository includes badge_open_url.py, a small host-side tool for sending URLs directly to Mini Browser over the custom firmware's serial keyboard bridge. This is useful for testing because URLs can be pasted on the computer instead of being typed manually on the badge.
The script requires Python 3 and pyserial:
python3 -m pip install pyserial
To paste a URL interactively:
./badge_open_url.py
Paste the URL at the prompt and press Enter. The script sends WHY+E, enters the URL through the normal BadgeVMS keyboard path and presses Enter.
A URL can also be supplied directly:
./badge_open_url.py https://example.com/
To open the URL currently on the macOS or Linux clipboard:
./badge_open_url.py --clipboard
The script can also run a list of URLs as a simple Mini Browser presentation. Put one URL per line in a text file. Blank lines and lines beginning with # are ignored.
For example, urls.txt:
# Mini Browser demonstration https://example.com/ https://news.ycombinator.com/ https://wiby.me/
Run the presentation with:
./badge_open_url.py -f urls.txt
Each page is displayed for 30 seconds by default. Change the interval with -s:
./badge_open_url.py -f urls.txt -s 10
Use -z to request a complete WHY+Z screenshot of every page in the list:
./badge_open_url.py -f urls.txt -z
This combines the URL presentation with badge_screenshot.py --full-page --request. The screenshot receiver can be specified explicitly:
./badge_open_url.py -f urls.txt -z --screenshot-script ./badge_screenshot.py
When badge_screenshot.py is in the same directory as badge_open_url.py, it is normally found automatically. Screenshots are written through the screenshot receiver; the presentation script uses a screenshots directory by default.
A useful automated demonstration and screenshot run is therefore:
./badge_open_url.py -f urls.txt -s 15 -z
The script auto-detects common macOS and Linux serial devices. A device can also be selected explicitly:
./badge_open_url.py --device /dev/cu.wchusbserial10 https://example.com/
On Linux the device will commonly be something such as /dev/ttyUSB0 or /dev/ttyACM0.
Because URL entry and host-requested screenshots use the serial keyboard bridge, badge_open_url.py requires the customized WHY2025 firmware described above.
Press WHY+I to open Page Information for the current page. Since 4.4 it starts with a summary:
Below the summary follows the HTTP/TLS Inspector. It shows bounded metadata retained from the actual request without making an extra HEAD request: page title and size, link/form/action counts, request method and URL, transport, cookies sent, HTTP status and Content-Type, cookie-jar usage and browser limits.
The trimmed BadgeVMS libcurl exposes only a small set of CURLINFO fields. Connection address/port, negotiated HTTP version, certificate details and certificate-verification result are therefore reported as unavailable rather than guessed. WHY+B or WHY+I returns to the page.
Mini Browser keeps up to 32 cookies (values up to 255 bytes). It supports Domain, Path, Secure, replacement, and deletion with Max-Age=0 or an Expires date in the past, and sends matching cookies on later requests to the same site. Since 4.3 cookies with Expires or Max-Age are saved in APPS:[mini_browser]cookies.txt and come back after a restart; session cookies stay in memory only. The badge has no network clock: expiry times are counted from the Date header of the first web page that sends one. WHY+X on the Page Information page clears all cookies.
Mini Browser stores up to 32 bookmarks and keeps up to 32 entries for Back and Forward. Going Back and then navigating somewhere new discards the old forward branch. Reloading does not create a duplicate Back/Forward entry.
Since 4.3 the last three pages you left are kept in a Back/Forward cache, with their scroll position and (up to 3 MB) their images: going Back or Forward to one of them, or leaving the bookmarks, history or Page Information page, shows it at once without loading it again. WHY+R always reloads from the network, and POST results are not cached.
Since 4.1 Mini Browser also keeps a persistent history of visited pages: the last 150 pages, newest first, each address once with its title. POST results are not recorded. Press WHY+Y to open the history page, type a number and press Enter to open a page, and press WHY+Y again to return. WHY+X on the history page clears the history.
The history is saved every five new pages and when Mini Browser quits, so the last few pages can be missing after the badge is switched off without quitting the browser. Bookmarks and history are stored at:
APPS:[mini_browser]bookmarks.txt APPS:[mini_browser]history.txt
WHY+K adds or removes the current page as a bookmark, WHY+M lists them. Other files Mini Browser keeps:
APPS:[mini_browser]cookies.txt cookies kept after a restart (4.3) APPS:[mini_browser]downloads.txt list of downloads (4.3) APPS:[mini_browser]saved.txt list of saved pages (4.4) FLASH0:[MBCACHE] disk cache, 2 MB (4.3) FLASH0:[DOWNLOADS] downloaded files (4.3) FLASH0:[SAVED] saved pages as text files (4.4)
Mini Browser uses libcurl and HTTP/1.1 where available and follows redirects to a bounded limit. Connection failures and HTTP errors are shown as readable error pages.
The BadgeVMS 4.3 firmware extends its curl with what a browser needs: progress reporting and stopping a transfer, gzip/deflate decoding (using the inflater in the ESP32-P4 ROM), keeping the connection open between requests to the same site, and distinct errors for name resolution, connection, TLS, certificate and timeout failures. With that firmware Mini Browser asks for compressed pages (Accept-Encoding: gzip, deflate) and reuses one connection for a page and its images, which saves a TLS handshake per request. On older firmware it requests uncompressed data (Accept-Encoding: identity) and works as before, without the loading progress and stop key.
Mini Browser does not implement JavaScript, a CSS layout engine, audio/video, complex form controls, file uploads, a complete HTML5 DOM/parser, color emoji or complex emoji composition. Version 3.0 provides bounded PNG, JPEG and GIF image rendering rather than a general-purpose modern browser graphics engine.
The tested font asset combines unifont_jp-17.0.02.hex and unifont_upper-17.0.02.hex into unifont_cjk.bin. The generated MBCJ v1 file contains 56,352 usable glyphs across 101 ranges and is 1,804,488 bytes (about 1.72 MiB). GNU Unifont is dual-licensed under the SIL Open Font License 1.1 and GNU GPL version 2 or later with the GNU Font Embedding Exception. Include the applicable licensing and attribution material when redistributing the generated font asset.
Mini Browser 4.4 - WHY2025 BadgeVMS