package miaou-registry
- [Unreleased]
- [0.5.2] - 2026-05-06
- [0.4.2] - Unreleased
- [0.4.1] - Unreleased
- [0.4.0] - Unreleased
- [0.3.2] - Unreleased
- [0.3.0] - Unreleased
- [0.2.7] - 2026-02-07
- [0.2.6] - 2026-02-06
- [0.2.5] - 2026-02-05
- [0.2.0] - 2026-02-05
- [0.1.4] - 2026-01-22
- [0.1.3] - 2026-01-16
- [0.1.2] - 2026-01-16
- [0.1.1] - 2026-01-16
-
[Unreleased]
- API
- Breaking Changes (2026-01-08)
- Added (2026-01-08)
- Changed (2026-01-08)
- Fixed (2026-01-08)
- Added (2026-01-05)
- Added (2025-12-19)
- Added (2025-12-17)
- Changed (2025-12-17)
- Breaking Changes (2025-12-17)
- Changed (2025-12-15)
- Added (2025-12-11)
- Changed (2025-12-10)
- Breaking Changes (2025-12-12)
- Breaking Changes (2025-12-11)
- [Previous Releases]
Install
dune-project
Dependency
Authors
Maintainers
Sources
md5=60a3b9f181f24572a06a9492532bfdda
sha512=fcc35a275066be2900e6201782faf47503076fa4640f08cf78067835a6f447b74613009e55b2ac799adb7ca46f1bffa261fc5971753f2cc3c6bef327511c7ef6
doc/CHANGELOG.html
Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
[0.5.2] - 2026-05-06
Added
- MIAOU Links demo (
example/demos/miaou_links/): a top-down golf game registered in the galleryGamesgroup, with an 18-hole classic tour plus a roguelite run mode with stamina, shop perks, persistent coins, and per-run score tracking. It demonstrates framebuffer golf rendering, continuous ball physics over terrain-specific friction, wind/gust effects, pre-shot previews, and persistent demo state viaArcade_kit.Score_store(miaou_linksandmiaou_links_coins). Existing simpler golf or physics demos can use it as the richer reference for tile-map courses, shot state machines, and Octant-first pixel output (MIAOU_LINKS_PIXEL_MODEoverride). No public library API is removed or changed. - MIAOU Crypt demo (
example/demos/miaou_crypt/): a pseudo-3-D dungeon crawler registered in the galleryGamesgroup, with seven hand-authored floors, raycast walls, billboarded enemies, boss encounters, pickups, minimap support, and deterministic debug stepping viaMIAOU_CRYPT_DEBUG=1. It demonstrates how to build a first-person game on top ofFramebuffer_widget,Arcade_kit.Particles, andArcade_kit.Score_storewhile bounding framebuffer size and raycast work for terminal performance (MIAOU_CRYPT_PIXEL_MODEoverride). Existing raycast or dungeon experiments can use it as the maintained reference implementation. No public library API is removed or changed. - Shared
Arcade_kit(example/shared/arcade_kit.{ml,mli}): small toolkit used by the gallery's arcade-style demos.Arcade_kit.Particlesis a pre-allocated ring-buffer particle pool withspawn/spawn_burst/tick ~dt ~ax ~ay/iterand zero per-frame allocation in the hot path.Arcade_kit.Hueships seven hand-snapped 12-stop xterm-256 ramps (cyan / magenta / amber / sand / lava / ice / grass) plus matching(r,g,b)approximations for pixel-buffer rendering, dodging the smooth-gradient banding that hits Octant render mode.Arcade_kit.Screen_fxexposesflashandshakeoverlays decaying over a duration.Arcade_kit.Score_storereads/writes per-demo high scores under$XDG_STATE_HOME/miaou/<demo>.score(best-effort, silent on IO error).Arcade_kit.Pixel_mode.resolvereturnsCaps.Octantby default with env-var override — never auto-detects, since auto-Sixel produces fragmented output on Konsole. - MIAOU Force demo (
example/demos/miaou_force/): a three-level R-Type-style horizontal shooter registered in the gallery'sGamesgroup. It demonstrates framebuffer-based arcade rendering, persistent high scores throughArcade_kit.Score_store, deterministic turn-based stepping viaMIAOU_FORCE_TURN_BASED=1, and a dockable Force pod (ddetach/recall,fflip front/back while attached). Existing one-off shooter demos can use this as the richer reference for scroll-driven levels, reusable particle pools, capped entity arrays, and Octant-first pixel output (MIAOU_FORCE_PIXEL_MODEoverride). No public library API is removed or changed. - Solar System demo (
example/demos/solar_system/): an animated visual of the Sun and the eight planets at their real orbital and rotational periods (Mercury 88 d / Earth 365.25 d / Neptune 60 190 d for orbits; Earth 1 d / Jupiter 0.41 d / Venus 243 d for axial spin). Distances are square-root-compressed and radii cube-root-compressed so even Mercury stays visible while Jupiter still feels imposing. The Sun has a quadratic radial glow over a white-hot core, every planet is filled with smooth Lambert shading from the Sun's direction (with rim light + a small bright spin marker that orbits the limb to show rotation), Saturn carries a thin ring, orbit rings are dimmed so they don't fight the planets, and a deterministic sparse star field sits behind everything. A right-side panel (toggleable withTab) shows simulated time, the speed multiplier (1–5set ×1 / ×10 / ×100 / ×1000 / ×10 000 days per real second), each planet's current orbital phase in degrees, and inline help.Space/ppauses,otoggles orbit rings,ltoggles labels,rresets time. The pixel mode is auto-detected (Sixelon capable terminals,Octantotherwise) and overridable viaMIAOU_SOLAR_PIXEL_MODE. Registered under the gallery'sShowcasesgroup. - Geo Quiz demo (
example/demos/geo_quiz/): a city-locator showcase game registered in the gallery'sGamesgroup. The menu mode features a colour-shaded rotating 3-D globe with filled continents and Lambert shading anchored to a fixed-screen sun (Octant 2×4 sub-cell rendering — sand-toned land, navy ocean, paler limb), a five-tier difficulty selector (capitals of large countries → all capitals → cities >1 M → cities >100 K → cities >15 K), and embeddedcoastline.bin(Natural Earthne_50m, 60 K points),landmask.bin(rasterisedne_50m_landpolygons at 720×360, 32 KB) andcities.bin(filtered GeoNames cities15000, 33 K cities) blobs. Round mode draws a colour, aspect-corrected equirectangular world map (Octant sub-pixels viaFramebuffer_widget); the player moves a crosshair with arrow keys,Shift+arrowfor big jumps, or a mouse click, and pressesEnterto lock in the guess. A 30 sTimer.set_timeoutauto-locks a zero-score guess on expiry. Scoring combines a haversine distance score (max 0 (1000·(1 − d/max_d)), withmax_d = 5000 kmfor tiers 1–3 and2500 kmfor tiers 4–5) and a remaining-time bonus capped at 300, with the round-end map showing both the truth and guess pins; the game-over screen renders a per-round bar chart. Two-tier caching (a per-resolution background-bytes cache plus a final-ANSI string cache keyed on cursor + truth + size) keeps input latency low even on large terminals; map and globe sizes are capped to bound encoding work, and the layout adapts to compact / standard / wide breakpoints. - Reusable
Globe_widget(Miaou_widgets_display.Globe_widget): standalone rotating-globe widget used by the Geo Quiz menu. Public API:create ?is_land ~coastline (),advance ~dt,set_rotation ~yaw ~pitch,yaw,render ~cols ~rows. Renders into anOctant_canvas, fills the inscribed disc by inverting the camera-space rotation per cell, looking up a caller-supplied land/sea classifier, and applying separate sand and ocean Lambert ramps to a fixed screen-space sun. Overlays equator and meridian graticule, then projects coastline points with backface culling (z < 0). Also exposeslatlon_to_xyzandhaversine_kmhelpers for callers that need the same sphere math. Self-registers viaMiaou_registry. - Reusable
Prompthelpers (Miaou_core.Prompt): thin wrappers overModal_manager.confirm_with_extractexposingPrompt.confirm,Prompt.input, andPrompt.selectso application code no longer has to assemble the modal-page boilerplate by hand. Each helper takes anon_resultcallback receiving the user's choice (orNone/falsefor cancellation) and renders the matching widget centred. Pure result-mapping helpers (confirm_outcome,input_result,select_result) are exposed for unit testing. - Gallery demos for
Responsive,Select_widget, inline mode, and inline + responsive composition: four new entries underexample/demos/exercising recently-added building blocks.responsive/swaps between 4-column / 2×2-grid / stacked layouts as the terminal narrows.inline_select/puts aSelect_widgetinline on a page rather than as a centred dialog.inline_cli/is a tiny "list current directory" page that, when launched via itsrun.sh(MIAOU_INLINE_MODE=1 dune exec …), runs without taking over the alternate screen — its output stays in scrollback after quit.inline_color_picker/combines both: a 16-colour swatch grid that adapts to width and runs in inline mode via its ownrun.sh. - User keymap overrides (
Miaou_core.Keymap_config): optional line-based config file letting end users rebind page actions without touching application code. Parses entries of the formpage=<name|*> key=<key> action=<id>(with#comments and blank lines), folds key spellings (ctrl+x,Ctrl-X,c-x→C-x;shift+tab→Shift-Tab) so configs are case-insensitive, and resolvespage=*as a global fallback after page-specific rules. Default lookup path is$MIAOU_KEYMAP_FILE, then$XDG_CONFIG_HOME/miaou/keymap.conf, then~/.config/miaou/keymap.conf; a missing file yields an empty keymap silently. The dispatch wiring (consulting overrides before each page's keymap) is intentionally deferred to a follow-up so pages can opt in by exposing named actions. - File browser icons + filetype colours:
File_browser_widgetnow prefixes each entry with a Unicode glyph keyed by file extension (📁 for directories, 🐫 for OCaml, 🦀 for Rust, 🐍 for Python, 📦 for archives, 📝 for markdown, etc.) and applies a per-extension 256-colour foreground. SettingMIAOU_NERD_FONT=1switches to a Nerd Font glyph set for terminals using a Nerd-patched font. The icon table lives in the newMiaou_widgets_layout.File_iconsmodule and is reusable from any custom widget that lists files. Responsive: a tiny utility for picking among layouts based on terminal width.Responsive.pickwalks an ascending list of{max_width; layout}breakpoints and returns the first match (mobile-first ordering). Layouts can beFlex_layout.t,Grid_layout.t, or anything else — the module is fully polymorphic.- Inline mode (
MIAOU_INLINE_MODE=1): a new run mode for the matrix driver in which the TUI does not switch to the alternate screen. The rendered frame stays in the terminal scrollback after exit, making it easy to review what a short-running TUI produced. Mouse tracking is suppressed in this mode (since copy/paste matters more than mouse interaction for inline tools). Programmatic configuration via the newMatrix_config.inline_modefield andMatrix_terminal.set_alt_screen/Terminal_raw.set_alt_screen. Note: this minimum-viable mode renders starting from the top of the viewport; an anchored partial-row mode (rendering only N rows below the current cursor) is planned as a follow-up. Wizard_widget: a generic multi-step wizard, polymorphic over user state. Each step provides its ownrender,validate, andon_key; the wizard owns navigation (Enter advances when validation passes, Shift+Tab returns, Esc cancels), breadcrumb chrome, and finished/cancelled state. Addsexample/demos/wizard/with a 3-step "pick backend → name it → review" flow.Textarea_widgetundo / redo: the multi-line editor now supportsCtrl+Z(undo) andCtrl+Y/Ctrl+Shift+Z(redo). Consecutive character inserts are coalesced into a single undo step so a typed word is reverted in one go; backspace, delete and newline get individual steps. The undo/redo stacks are capped at 200 entries each. New API:Textarea_widget.undo,redo,can_undo,can_redo.Tree_widgetkeyboard navigation: the tree widget now responds toUp/Down/Left/Right/Enter/Home/End, tracks expansion state per path, renders an expand marker (▾/▸ with ASCII fallback) and highlights the cursor row via the theme's selection style. Previously the widget rendered statically. New helpersTree_widget.expand_all,collapse_all,is_expanded, andflatten_visibleare exposed.- Web Viewer for headless sessions (
Web_viewer): standalone HTTP+WebSocket server that runs alongside the headless driver, letting a human observe an AI agent's TUI session in real time via a browser. Serves the existing xterm.js viewer page, broadcasts ANSI frames to all connected viewers, and tracks terminal dimensions so xterm.js resizes to match the headless render size. New viewers receive the current dimensions and last frame on connect (no blank screen). on_framecallback in headless runner:Headless_json_runner.runandRunner_tui.runaccept an optional?on_frame:(rows:int -> cols:int -> string -> unit)callback invoked with the raw ANSI frame and terminal dimensions on every frame emit. This enables external consumers (likeWeb_viewer) to observe frames without modifying the headless protocol.- Viewer auto-reconnect: the xterm.js client (
client.js) now automatically reconnects with a 2-second retry when a viewer WebSocket disconnects, surviving server restarts without requiring a manual browser refresh. - Viewer dimension sync: when the headless driver's terminal size changes, a
{"type":"dimensions","rows":R,"cols":C}JSON message is sent to all viewers. The client resizes xterm.js to match; FitAddon auto-fit is disabled for viewers so the terminal size is controlled by the server.
Added
- Octant rendering mode (
Octant_canvas): high-resolution chart rendering using Unicode 16 octant block characters (2×4 sub-cell pixels per character cell). Gives 8× resolution compared to ASCII mode with per-cell color support. Octant mode is available onSparkline_widget,Line_chart_widget, andBar_chart_widgetvia a new~mode:Octantparameter. - Framebuffer widget (
Framebuffer_widget): direct pixel/cell-based drawing surface embeddable in any layout slot. Supports both character-cell and sub-cell (Octant) pixel addressing, making it easy to build custom visualisations, games, or image renderers. - Terminal capabilities detection (
Terminal_caps): detects whether the connected terminal supports Unicode 16 octant characters. Used internally by the Octant rendering mode to fall back gracefully on older terminals. - Periodic viewer refresh daemon (headless runner): when an
on_framecallback is registered (e.g. byWeb_viewer), a background Eio daemon fiber re-renders the screen every 200 ms and broadcasts the updated frame. This keeps live viewers up-to-date during agent idle periods (timers, async I/O, spinners) without requiring a key press or tick. - "Framebuffer & Octant Charts" demo added to the gallery, showcasing both the
Framebuffer_widgetand Octant chart modes side-by-side.
Fixed
- Opam release metadata: removed the temporary
pin-dependsentry and local pin setup instructions forppx_forbid/ppx_enforce, now that both PPXs are available from opam as normal build-time dependencies. Input_parser: recognise PageUp / PageDown / Home / End CSI sequences:parse_keyandpeek_keynow handleESC[5~,ESC[6~,ESC[H/ESCOH/ESC[1~/ESC[7~, andESC[F/ESCOF/ESC[4~/ESC[8~instead of returningUnknown;key_to_stringandis_nav_keyare updated accordingly. This documents the already-merged #132 navigation-key fix for users relying on paging or home/end movement in widgets.- Viewer daemon race condition: the periodic viewer-refresh fiber previously called
idle_waiteach iteration, which allowed it to interleave with the command handler's ownidle_waitand concurrently mutate shared page-state (double-ticking clocks/timers). The daemon now reads the cached screen content directly viaHD.Screen.getwithout advancing any state. - Web driver Tab key:
ev.preventDefault()is now called for all recognized keys in the web client's keyboard handler. Previously, Tab (and other browser-reserved keys like F5) were forwarded to the server but also processed by the browser for focus navigation / page reload. Tab now reaches the Miaou application correctly.
[0.4.2] - Unreleased
Fixed
- Canvas ANSI row isolation:
Canvas.to_ansi_with_defaultsnow always emits an SGR sequence at column 0 of every row, making each row self-contained. Previously, style was carried across row boundaries as an optimisation; this causedapply_bg_fillto bleed the wrong background into the first character of rows where style happened to carry unchanged from the previous row. - Canvas widget fills full terminal height: Miaou Invaders (and any
Canvas_widgetpage) no longer shows black bars below the canvas on tall terminals. The 36-row cap on the canvas height has been removed so the game scales to the full terminal height. - Matrix driver scrub flicker:
force_renderis no longer called from the main loop (neither on modal transitions nor during periodic scrub). Both cases now only callmark_all_dirty, letting the render domain (the sole terminal writer) pick up the change within one frame. This eliminates the interleaved-write race that caused visible flicker. - Miaou Invaders background: All
draw_textcalls in the Invaders demo now carry an explicitbgmatching the current game or HUD background. Previously, entities drawn withbg=-1clobbered thefill_rect-painted background, producing black horizontal bars wherever sprites appeared. - Periodic scrub interval: Default
scrub_interval_framesreduced from 30 frames (0.5 s at 60 fps) to 300 frames (5 s), making the occasional full-refresh nearly imperceptible.
[0.4.1] - Unreleased
Fixed
- Table row selection highlighting: Full row background now displays correctly when
selection_mode = Row. Previously, only border characters (vertical separators) showed the selection color due to ANSI reset codes fromthemed_borderclearing the selection background. Now, border styling is skipped for selected rows, allowing the full row to inherit the selection background color.
[0.4.0] - Unreleased
Breaking Changes
- Box_widget border style: added
None_toBox_widget.border_stylefor borderless containers. Pattern matches onborder_stylemay need a new case.
Added
- Cascading style system (
miaou_style): semantic styles + CSS-like selectors with effect-based context (Style_context). - Theme JSON support with discovery/merge rules and optional validation for low-contrast fg/bg combinations.
Built-in themes (
Builtin_themes): 11 popular themes included directly in the library:- Dark: catppuccin-mocha, dracula, nord, gruvbox-dark, tokyonight, opencode, oled
- Light: catppuccin-latte, nord-light, gruvbox-light, tokyonight-day
opencodeandoledthemes use borderless style for a clean, minimal lookoledtheme features true black background (#000000) with soft pastel colors for OLED screens
- Theme registry API:
Builtin_themes.list_builtin(),get_builtin(id),is_builtin(id)for discovering and loading built-in themes. - Smart theme loading:
Theme_loader.load_any(name)checks built-in themes first, then user themes;list_all_themes()returns combined list. - Style system demo (
miaou.style_system-demo) with runtime theme switching and contextual styling.
Changed
- Widget theming: widgets now use semantic themed styles; containers fill contextual backgrounds across full line width.
Fixed
- Theme JSON parsing: tolerant parsing for partial style objects, multiple color formats, and string border styles.
[0.3.2] - Unreleased
Added
- Textarea widget (
miaou_widgets_input.Textarea_widget): multiline text input with cursor navigation, line joining, and scroll support. Use Alt+Enter to insert newlines. - Left-bordered box (
Widgets.render_left_border_box): display helper for context/quote blocks with colored left border and optional background. - Blocks spinner style (
Spinner_widget.Blocks): animated spinner with size+color gradient progression trail, configurable direction and block count. - Alt+Enter key parsing (
Input_parser.AltEnter): universally-supported newline insertion key for textarea widgets. - Mouse helper module (
Miaou_helpers.Mouse): utilities for parsing mouse events (clicks, drags, wheel) in widgets. Mouse support for widgets: wheel scrolling and click handling added to:
- Pager: wheel scroll, click to position cursor (in cursor mode)
- Select: wheel scroll, click to select item
- File Browser: wheel scroll, click to select entry
- Textbox: click to position cursor
- Textarea: wheel scroll, click to position cursor
- Tabs: click to select tab
- Breadcrumbs: click on crumb to navigate
- Button: click to activate
- Link: click to navigate
- Checkbox/Radio/Switch: click to toggle
- Signal handling control: optional SIGINT handling via
install_signals'andRunner_tui.runhandle_sigintoption. - Per-side border colors for
Box_widgetto style each edge independently.
Changed
- Input parser: added
AltEnterkey variant for Alt+Enter detection (ESC followed by newline).
Fixed
- Matrix driver scrub: avoid screen clear during periodic scrub to reduce flicker.
- Terminal raw mode: disable
c_isigand ignore SIGINT when not handling it. - Mouse interactions: consistent enable sequence via
/dev/tty, improved click handling, and double-click support. - Pager: add ANSI reset and wrap-aware scrolling.
[0.3.0] - Unreleased
Breaking Changes
- Navigation API hardening:
Navigation.pendingnow returnsNavigation.nav option(Goto of string | Back | Quit) instead ofstring option, replacing magic strings ("__BACK__","__QUIT__"). - Modal navigation callback API:
Modal_manager.set_pending_navigationnow takesNavigation.navinstead ofstring. - Page transition hooks: page transition handler records now expose an explicit
on_backcallback. - Matrix IO internals:
Matrix_io.tremoves legacy polling/drain fields (poll,drain_nav_keys,drain_esc_keys) in favor of a decoupled event queue reader model.
Added
- Clock capability (
miaou_interfaces.Clock) exposingdt,now, andelapsedthunks to pages/widgets. - Page-scoped timers (
miaou_interfaces.Timer) withset_interval,set_timeout,clear, and fired-event draining. - Animation module (
miaou_helpers.Animation) with easing, repeat modes, sequencing, delay, and lerp helpers. - Canvas abstraction (
miaou-core.canvas) with drawing primitives, border styles, composition, and ANSI rendering. - Canvas layers:
Canvas.composeandCanvas.compose_newfor ordered transparent/opaque overlay compositing. - Canvas widget (
miaou_widgets_layout.Canvas_widget) for embeddable mutable drawing surfaces in layout slots. - Runner CLI snapshot mode:
--cli-output(plus--cols,--rows,--ticks) for non-interactive stdout rendering. - Color documentation: new
docs/colors.mdplus widget interface docs clarifying ANSI payload formats and precedence rules.
Changed
- Matrix driver input architecture: dedicated Eio reader fiber + mutexed queue; tick loop drains full event batches.
- Default matrix tick rate increased to 60 TPS.
- Matrix artifact scrubbing is now configurable via
MIAOU_MATRIX_SCRUB_FRAMES(set0to disable). - Example gallery now includes the renamed Miaou Invaders demo, with richer gameplay systems and modularized demo code.
Fixed
- ESC parsing robustness: avoid out-of-bounds exceptions on unknown ESC-prefixed pairs while preserving Escape semantics.
- Demo overlay/collision consistency in Miaou Invaders: gameplay coordinate handling stays aligned with canvas size and reserved HUD rows.
[0.2.7] - 2026-02-07
Fixed
- Esc key repeat quitting app after modal close — Matrix driver now applies a 200ms cooldown after closing a modal with Esc, suppressing spurious Esc events from terminal key repeat that would otherwise reach the page and trigger app exit
- Footer hints not rendering in Matrix driver —
key_hintsfrom pages are now correctly rendered in the footer bar
[0.2.6] - 2026-02-06
Added
Unified key handling architecture with
Key_event.resulttype:- New
on_key/on_modal_keymethods returnHandled | Bubblefor composable key dispatch key_hintsfor display-only footer hints (replaces action-bearingkeymap)- All input widgets (
Button,Checkbox,Radio,Switch,Textbox,Select,ValidatedTextbox) exposeon_key Keys.of_stringnow accepts aliases:"S-Tab","BackTab"→ShiftTab;"Esc"→Escape
- New
Fixed
- Keymap dispatch bypassing
handle_key— Drivers now always route keys throughon_key, fixing Focus_ring Tab navigation when Tab was in page keymap - Lambda-term driver Enter key — Enter now goes through
on_keylike other keys
Changed
- BREAKING:
PAGE_SIGnow requireson_key,on_modal_key, andkey_hintsmethods Demo_page.MakeSimplefunctor for demos without explicitkey_hints- Legacy
handle_key,handle_modal_key,keymapdeprecated but still functional
[0.2.5] - 2026-02-05
Added
Focus Ring widget (
Miaou_internals.Focus_ring) for named-slot focus hierarchy:- Type-safe focus management with string-keyed slots
- Automatic wrap-around navigation (next/prev)
handle_keyreturnsHandled |Bubble` for composable key dispatch- Ideal for forms, toolbars, and multi-widget layouts
Focus Container widget (
Miaou_internals.Focus_container) for GADT-based focus management:- Type-safe heterogeneous widget containers using extensible GADTs
- No
Obj.magic- full type safety with witness pattern - Nested container support for complex UI hierarchies
- Generic focus traversal across different widget types
Box Widget (
Miaou_widgets_layout.Box_widget) for border-decorated containers:- Five border styles:
Single,Double,Rounded,Heavy,Ascii - Optional colored borders with 256-color support
- Configurable padding (top, bottom, left, right)
- Nested box support for complex layouts
- Automatic ASCII fallback via
MIAOU_TUI_UNICODE_BORDERS=false
- Five border styles:
Direct_page (
Miaou.Core.Direct_page) for simplified page development:- Only 3 required functions vs 13 in PAGE_SIG:
init,view,on_key - Navigation via OCaml 5 effects:
Direct_page.navigate,go_back,quit With_defaultsfunctor provides sensible defaults for optional functions- Reduces boilerplate significantly for simple pages
- Only 3 required functions vs 13 in PAGE_SIG:
Grid Layout (
Miaou_widgets_layout.Grid_layout) for CSS-grid-like layouts:- Row and column track definitions with
Fr,Px,Autosizing grid_areaplacement for precise cell positioning- Gap support (row_gap, column_gap)
- Span support for multi-cell items
- Automatic content fitting
- Row and column track definitions with
Fixed
- Flex layout column alignment - Short/empty lines in row layouts now properly padded to allocated width, preventing subsequent columns from bleeding into earlier column areas
[0.2.0] - 2026-02-05
Added
Web driver (
miaou-driver-web) for browser-based terminal rendering:- xterm.js terminal emulation over WebSocket
- Controller/viewer architecture for shared sessions
- Password authentication support
- 60 FPS configurable refresh rate
Path-based roles for web driver with explicit URL routing:
/ws— controller WebSocket (returns 409 if slot already taken)/ws/viewer— viewer WebSocket (returns 409 if no controller connected)/viewer— dedicated viewer HTML page- Separate
controller_passwordandviewer_passwordauthentication
Composable
MiaouTerminal(container, options)JS factory replacing the IIFE inclient.js:wsPathoption (/wsor/ws/viewer)onRole,onStatusChange,onAuthRequiredcallbackssessionStoragekeys scoped bywsPath- Returns
{ term, fitAddon, reconnect(pw), getRole() }
Custom HTML pages and extra assets for the web driver:
~controller_htmland~viewer_htmloptional parameters onWeb_driver.runextra_assettype for serving additional static files (e.g. logos)- Both parameters forwarded through
Runner_web.run
Branded gallery pages with Miaou logo header and role badges:
MIAOU_WEB_VIEWER_PASSWORDenvironment variable (falls back toMIAOU_WEB_PASSWORD)
Changed
- Web driver routing refactored:
/wsalways creates controller,/ws/vieweralways creates viewer (previously role was assigned by connection order on single/wsendpoint)
[0.1.4] - 2026-01-22
Fixed
Modal title rendering with multiline text
- Modal titles containing newlines no longer corrupt the layout
- First line is displayed in the colored title bar with blue background
- Additional lines are prepended to the modal content body
- Fixes misaligned borders and improper blue background spanning
[0.1.3] - 2026-01-16
Added
enable_mouseparameter forRunner_tui.runto programmatically control mouse tracking(* Disable mouse tracking from code *) Runner_tui.run ~enable_mouse:false my_page
Changed
- Version bump to 0.1.3
[0.1.2] - 2026-01-16
Added
Optional mouse tracking via environment variable
MIAOU_ENABLE_MOUSE- Set
MIAOU_ENABLE_MOUSE=0orMIAOU_ENABLE_MOUSE=noto disable mouse tracking - Allows traditional terminal copy/paste when mouse tracking interferes
- See
docs/MOUSE_CONTROL.mdfor details
- Set
Matrix_config.with_mouse_disabledhelper for programmatic mouse control
Changed
- Version bump to 0.1.2
[0.1.1] - 2026-01-16
Fixed
Matrix driver race condition in dirty flag handling that caused intermittent render artifacts
clear_dirtywas called outside the buffer lock, allowing new UI writes to be skipped- Now cleared atomically inside
compute_atomicwhile holding the lock
Lambda-term
split_lines_preserveincorrectly added an extra empty elementString.split_on_charalready handles trailing delimiters correctly
Changed
- File browser fixes for edit mode (Space key handling, selection highlight)
- Version bump to 0.1.1
[Unreleased]
API
- Rename
Vsection.renderparameter~footerto~content_footerto clarify it is for page content, not driver-generated keymap footers. - Clarify
PAGE_SIGdocs: keymap footers are auto-generated by drivers,?is reserved but may appear in keymaps for display, andhandled_keysis only for conflict detection. - Add
display_onlyflag to keymap bindings so reserved keys (e.g.,?) can be shown in footers/help without being dispatched; drivers and the key handler stack respect this. - Add
File_browser_modalhelper plusFile_browser_widget.key_hintsto avoid re-wrapping the widget for modals and to surface consistent key hints.
Breaking Changes (2026-01-08)
⚠️ PAGE_SIG Navigation API Rewrite
Impact: All page implementations must be updated. This is a significant API change.
What changed:
The next_page field and enter function have been removed from PAGE_SIG. Instead, pages now use the Navigation module for all navigation, and all handlers work with pstate (which wraps state in Navigation.t).
Old API (removed):
module type PAGE_SIG = sig
type state
type msg
val init : unit -> state
val next_page : state -> string option (* REMOVED *)
val enter : state -> state (* REMOVED *)
val update : state -> msg -> state
val view : state -> focus:bool -> size:LTerm_geom.size -> string
val move : state -> int -> state
val refresh : state -> state
val service_select : state -> int -> state
val service_cycle : state -> int -> state
val back : state -> state
val keymap : state -> (string * (state -> state) * string) list
val handled_keys : unit -> Keys.t list
val handle_modal_key : state -> string -> size:LTerm_geom.size -> state
val handle_key : state -> string -> size:LTerm_geom.size -> state
val has_modal : state -> bool
endNew API:
module type PAGE_SIG = sig
type state (* Your page's own state - no next_page field needed *)
type msg
type pstate = state Navigation.t (* Wrapped state with navigation *)
val init : unit -> pstate
val update : pstate -> msg -> pstate
val view : pstate -> focus:bool -> size:LTerm_geom.size -> string
val move : pstate -> int -> pstate
val refresh : pstate -> pstate
val service_select : pstate -> int -> pstate
val service_cycle : pstate -> int -> pstate
val back : pstate -> pstate
val keymap : pstate -> (string * (pstate -> pstate) * string) list
val handled_keys : unit -> Keys.t list
val handle_modal_key : pstate -> string -> size:LTerm_geom.size -> pstate
val handle_key : pstate -> string -> size:LTerm_geom.size -> pstate
val has_modal : pstate -> bool
endMigration guide:
- Remove
next_pagefrom your state type:
(* Before *)
type state = {
items : string list;
cursor : int;
next_page : string option; (* REMOVE THIS *)
}
(* After *)
type state = {
items : string list;
cursor : int;
}- Add the
pstatetype alias:
type pstate = state Navigation.t- Update
initto wrap state:
(* Before *)
let init () = { items = []; cursor = 0; next_page = None }
(* After *)
let init () = Navigation.make { items = []; cursor = 0 }- Remove
next_pageandenterfunctions (they no longer exist). - Update all handlers to use
pstateand Navigation functions:
(* Before *)
let handle_key s key ~size =
match key with
| "q" -> { s with next_page = Some "__QUIT__" }
| "Esc" -> { s with next_page = Some "__BACK__" }
| "Enter" -> { s with next_page = Some "details" }
| "j" -> { s with cursor = s.cursor + 1 }
| _ -> s
(* After *)
let handle_key ps key ~size =
match key with
| "q" -> Navigation.quit ps
| "Esc" -> Navigation.back ps
| "Enter" -> Navigation.goto "details" ps
| "j" -> Navigation.update (fun s -> { s with cursor = s.cursor + 1 }) ps
| _ -> ps- Update state transformations to use
Navigation.update:
(* Before *)
let refresh s = { s with items = load_items () }
(* After *)
let refresh ps = Navigation.update (fun s -> { s with items = load_items () }) ps- Update
viewto access inner state:
(* Before *)
let view s ~focus ~size = render_items s.items s.cursor
(* After *)
let view ps ~focus ~size =
let s = ps.s in (* Access inner state via .s field *)
render_items s.items s.cursorNavigation module reference:
Navigation.make : 'a -> 'a t- Wrap state with no pending navigationNavigation.goto : string -> 'a t -> 'a t- Navigate to a named pageNavigation.back : 'a t -> 'a t- Go back (equivalent togoto "__BACK__")Navigation.quit : 'a t -> 'a t- Quit application (equivalent togoto "__QUIT__")Navigation.update : ('a -> 'a) -> 'a t -> 'a t- Modify inner stateNavigation.pending : 'a t -> string option- Check pending navigation (used by framework)
Compiler errors you'll see:
Error: This expression has type state but an expression was expected of type
state Navigation.t
Error: Unbound value next_page
Error: Unbound value enterWhy this change?
- Eliminates the error-prone
next_pagefield that LLM agents frequently forgot to propagate - Clear, named navigation functions instead of magic strings in a field
- Pure functional style with no hidden side effects
- Framework handles navigation automatically - pages just call
Navigation.goto
Added (2026-01-08)
Modal Navigation Helpers
Modal on_close callbacks can now request navigation without using refs or checking state in service_cycle:
(* Before - error-prone pattern requiring manual ref and service_cycle check *)
let nav_ref = ref None in
Modal_manager.push
(module My_modal)
~init:(My_modal.init ())
~ui:{ title = "Choose"; ... }
~commit_on:["Enter"]
~cancel_on:["Esc"]
~on_close:(fun state outcome ->
match outcome with
| `Commit -> nav_ref := Some "next_page"
| `Cancel -> ()) ;
(* Then in service_cycle: *)
let service_cycle ps _ =
match !nav_ref with
| Some page ->
nav_ref := None ;
Navigation.goto page ps
| None -> ps
(* After - direct API call *)
Modal_manager.push
(module My_modal)
~init:(My_modal.init ())
~ui:{ title = "Choose"; ... }
~commit_on:["Enter"]
~cancel_on:["Esc"]
~on_close:(fun _state outcome ->
match outcome with
| `Commit -> Modal_manager.set_pending_navigation "next_page"
| `Cancel -> ())
(* No service_cycle code needed - framework handles it automatically *)New functions:
Modal_manager.set_pending_navigation : string -> unit- Request navigation from modal callbackModal_manager.take_pending_navigation : unit -> string option- Used by framework
Auto-Refresh Before Service Cycle
Drivers now automatically call Page.refresh before Page.service_cycle. This means:
- Pages no longer need to manually call
refreshinservice_cycle - Consistent behavior across all drivers (Matrix, Lambda-term, SDL)
- The pattern
Page.service_cycle (Page.refresh ps) 0is now handled by the framework
(* Before - manual refresh in service_cycle *)
let service_cycle ps _ =
let ps = refresh ps in (* Manual refresh call *)
(* ... check refs, etc. *)
ps
(* After - just handle service logic *)
let service_cycle ps _ =
(* refresh is already called by the driver *)
psPager Widget Enhancements
- Wrap toggle: Press
wto toggle word wrap on/off in the pager - Line truncation: Long lines are truncated with visual indicator when wrap is off
- Default behavior changed to wrap=on for better readability
Narrow Terminal Warning
Both Matrix and Lambda-term drivers now show consistent narrow terminal warnings:
- Warning banner displayed when terminal width < 80 columns
- One-time modal appears on first detection (auto-dismisses after 5 seconds)
- Any key dismisses the modal immediately
- Warning only shown once per session (not repeatedly on resize)
Changed (2026-01-08)
Driver Architecture Improvements
- Periodic partial refresh: Matrix driver performs full buffer refresh every ~2 seconds to catch rendering artifacts
- Region-based dirty marking: More efficient partial updates in Matrix driver
- Terminal cleanup reliability: Improved cleanup on exit to restore terminal state
- Screen content preservation: Exit screen content saved for debugging
Shared Driver Modules
Common functionality extracted into shared modules:
terminal_raw.ml- Raw terminal mode handlinginput_parser.ml- ANSI escape sequence parsing
This reduces code duplication between Matrix and Lambda-term drivers.
Fixed (2026-01-08)
- Modal close no longer causes double-navigation with Esc key
- Matrix driver now drains pending Esc keys after modal close
- Fiber scheduling improved in Matrix driver (uses
Eio.Time.sleep) - File pager uses proper Eio-based fiber scheduling
Added (2026-01-05)
High-Performance Matrix Terminal Driver
miaou-driver-matrixpackage with Ratatui-style diff renderingTwo-domain architecture using OCaml 5 Domains for true parallelism:
- Render Domain: 60 FPS, handles diff computation and terminal output
- Main Domain: 30 TPS, handles input and state updates
- Cell-based double buffering with O(1) pointer swap
- Diff-based rendering: only changed cells are written to terminal (no flicker)
- Thread-safe buffer with mutex synchronization and atomic dirty flag
- Pure ANSI output (no lambda-term dependency)
- Matrix is now the default driver (priority: Matrix > SDL > Lambda-term)
Configuration via environment variables:
MIAOU_DRIVER=matrix(default) ortermorsdlMIAOU_MATRIX_FPS=60- Render domain frame rate capMIAOU_MATRIX_TPS=30- Main domain tick rate
Debug Overlay for Performance Monitoring
MIAOU_OVERLAY=1environment variable enables real-time performance metricsDisplays in top-right corner with dim styling:
- L (Loop FPS): Render loop iteration rate (the cap)
- R (Render FPS): Actual frames rendered per second
- T (TPS): Ticks per second (main loop rate)
- Available in both Matrix and Lambda-term drivers
- Useful for diagnosing performance issues and verifying frame rates
Added (2025-12-19)
Debounced Validation for Validated Textbox Widget
debounce_msparameter forValidated_textbox_widget.create(default: 250ms)- Validation now defers during rapid typing, running after the debounce period elapses
- Text input remains immediate for responsive UX
New functions:
tick- Check and run pending validation (call inservice_cycle)flush_validation- Force immediate validation (useful before form submission)has_pending_validation- Check if validation is pending
- Set
debounce_ms=0to disable debouncing (legacy immediate behavior)
Global Render Notification System
Miaou_helpers.Render_notifymodule for widgets to request async UI updatesrequest_render()- Request a re-render from any widgetshould_render()- Check if a render was requested (called by driver)- Used by validated textbox to trigger validation after debounce period
Generic Debounce Module
Miaou_helpers.Debouncemodule for generic debounce timing- Thread-safe implementation using
Atomicoperations - Functions:
create,mark,is_ready,clear,has_pending,check_and_clear - Configurable debounce period in milliseconds (default: 250ms)
File Browser Performance Optimization
- Caching for directory listings -
list_entries_with_parentnow caches results - Caching for writable status -
is_writablechecks are cached per-directory - Cache automatically invalidates when navigating to a different directory
- Cache manually invalidated after directory creation (
mkdir_and_cd, inline mkdir) - New
invalidate_cache()function for manual cache clearing - Significantly reduces filesystem calls during rapid Up/Down navigation
- Note: Cache is shared globally across all file browser instances
File Browser Hidden Files Toggle
- Press
hto toggle visibility of hidden files/directories (starting with.) - New
show_hiddenparameter inopen_centered(default:false) - Tab completion always includes hidden files for convenience (allows completing
.config/etc.) - Header hint updates dynamically to show current state
Textbox Input Draining for Typing Responsiveness
Miaou_helpers.Input_drainmodule for draining buffered input characters- Textbox widgets now process all pending printable characters at once
- Prevents typing lag when entering text quickly
- Driver registers drain function, widgets call
drain_pending_chars()
Added (2025-12-17)
- Modal sizing supports dynamic width specs (
Fixed,Ratio,Clamped) resolved at render time, including fallback terminal size detection via/dev/ttyso modals resize with the terminal even whenSystemis mocked.
Changed (2025-12-17)
Opam package restructuring for optional SDL
Restructured opam packages to allow terminal-only builds without SDL2 dependency:
miaou-core: Standalone core package with all widgets, no SDL dependenciesmiaou-driver-term: Terminal driver, depends only onmiaou-coremiaou-driver-sdl: SDL driver with SDL2 dependencies (tsdl,tsdl-ttf,tsdl-image)miaou-widgets-display-sdl: SDL-specific widget implementationsmiaou-runner: Runner withmiaou-driver-sdlas optional dependencymiaou-tui: Meta-package for terminal-only installs (no SDL)miaou: Meta-package for full install (includes SDL)miaou-core.lib: The convenienceMiaoumodule (re-exporting Core, Widgets, etc.) is now part ofmiaou-core, available to terminal-only users
Terminal-only users can now: opam install miaou-tui
Breaking Changes (2025-12-17)
Library public names changed to use package prefixes:
miaou.lib→miaou-core.libmiaou.core→miaou-core.coremiaou.widgets.display→miaou-core.widgets.displaymiaou.driver.term→miaou-driver-term.drivermiaou.driver.sdl→miaou-driver-sdl.driver- And similar for other libraries
Miaou_widgets_display.Sparkline_widget_sdlmoved toMiaou_widgets_display_sdl.Sparkline_widget_sdl(and similar for other SDL widgets)Modal_manager.ui.max_width(and related helpers) now expectsmax_width_spec optioninstead ofint option; wrap existing fixed widths withFixed nor switch to ratio/clamped specs.
Changed (2025-12-15)
- File pager tail fibers are now scoped to per-page switches and auto-cancel on navigation; terminal, SDL, and headless drivers wrap pages in
Fiber_runtime.with_page_switchfor structured cleanup;Fiber_runtimeexposes page switch helpers. Adds regression coverage for pager cleanup. - Pager UX fixes: follow hint only shown when streaming, static pager test added, markdown renderer hides inline backticks and underlines H1 titles.
- Service lifecycle: removing instance files no longer requires a role value.
- File browser navigation: canonicalize paths so parent navigation works from relative paths, scroll the viewport earlier, and tighten Enter navigation checks; demo uses the real filesystem so Enter now changes directories.
- Modal sizing: modal pages now receive the actual modal content geometry (rows/cols) so list widgets don’t scroll into invisible items due to modal height clipping.
Added (2025-12-11)
Input Buffer Draining for Navigation Keys
- Navigation key coalescing to prevent scroll lag in list widgets
- When arrow keys are held down and released, consecutive identical navigation events are automatically drained from the input buffer
- Only the final navigation event is processed, making the UI feel more responsive
- Debug logging available with
MIAOU_DEBUG=1to track drain operations - Fixes issue where selection continues scrolling for ~0.5s after releasing arrow keys
Braille Rendering Mode
- Unicode Braille patterns for high-resolution chart rendering (2×4 dots per character cell)
- Braille mode support for
Line_chart_widget,Bar_chart_widget, andSparkline_widget Braille_canvasmodule for efficient braille dot manipulation- 8x higher resolution compared to ASCII mode with only 2x performance cost
- Colored braille output with ANSI styling support
- Performance: 9,259 renders/second for line charts in braille mode
Global Keys API
- Type-safe keyboard handling system with variant-based key definitions
- Extended
Keys.twith new key types:PageUp,PageDown,Home,End,Escape,Delete,Function of int - Global key reservations for application-wide actions:
Settings,Help,Menu,Quit - Registry validation to prevent key conflicts at page registration time
Registry.check_all_conflicts()for detecting inter-page key conflictsRegistry.conflict_report()for human-readable conflict summaries- Helper functions:
Keys.is_global_key,Keys.get_global_action,Keys.show_global_keys
Changed (2025-12-10)
Performance Optimizations
- Significant performance improvements across all widgets (8-24x faster in some cases)
- Replaced
String.concatwith buffer-based rendering throughout codebase - Introduced
Helpers.pad_to_widtheliminating O(n²) padding allocations - Optimized pager widget: 9.1s → 1.2s (8x faster)
- Optimized layout widget: 9.0s → 0.8s (12x faster)
- Optimized card_sidebar: 15.1s → 1.0s (15x faster)
- All other widgets show 20-40% performance improvements
Breaking Changes (2025-12-12)
⚠️ Runtime Migrated from Lwt to Eio
Impact: Applications must initialize the Eio runtime before using Miaou.
What changed:
- All async/concurrency now uses Eio instead of Lwt
cohttp-lwt-unixreplaced withcohttp-eio- Terminal driver uses
Eio_unix.await_readablefor input polling - Background tasks use
Eio.Fiberinstead ofThread.create
Migration guide:
- Wrap your main function in
Eio_main.runand initialize the runtime:
(* Before *)
let () =
let page = ... in
Miaou_runner_tui.Runner_tui.run page
(* After *)
let () =
Eio_main.run @@ fun env ->
Eio.Switch.run @@ fun sw ->
Miaou_helpers.Fiber_runtime.init ~env ~sw;
let page = ... in
Miaou_runner_tui.Runner_tui.run page- Update opam dependencies:
- lwt
- cohttp-lwt-unix
+ eio
+ eio_main
+ cohttp-eioNew modules:
Miaou_helpers.Fiber_runtime— shared Eio runtime managementMiaou_widgets_display.File_pager— Eio-based file tailing pager
⚠️ Pager Widget: Notification Callback Now Per-Instance
Impact: Code using Pager_widget.set_notify_render must be updated.
What changed:
(* OLD - Global callback (removed) *)
Pager_widget.set_notify_render (Some callback);
let pager = Pager_widget.open_lines ~title:"Log" lines in
(* NEW - Per-instance callback *)
let pager = Pager_widget.open_lines ~title:"Log" ~notify_render:callback lines inMigration guide:
- Remove calls to
set_notify_render - Pass
~notify_renderparameter toopen_lines/open_text
Before:
let pager = Pager_widget.open_lines ~title:"My Pager" [] in
Pager_widget.set_notify_render (Some render_callback);After:
let pager = Pager_widget.open_lines ~title:"My Pager"
~notify_render:render_callback [] inWhy this change?
- Eliminates global mutable state
- Enables multiple independent pagers with different callbacks
- Makes callback lifetime explicit (tied to pager instance)
- Better composability and testability
Type signature changes:
open_lines : ?title:string -> ?notify_render:(unit -> unit) -> string list -> topen_text : ?title:string -> ?notify_render:(unit -> unit) -> string -> tset_notify_renderfunction removed
Compiler error you'll see:
Error: Unbound value Pager_widget.set_notify_renderBreaking Changes (2025-12-11)
⚠️ PAGE_SIG Requires handled_keys Function
Impact: All page implementations must be updated.
What changed:
module type PAGE_SIG = sig
(* ... existing fields ... *)
(* NEW - REQUIRED *)
val handled_keys : unit -> Keys.t list
endMigration guide:
For minimal migration, add this to every page:
let handled_keys () = []For proper key declaration (recommended):
let handled_keys () = [
Keys.Char "a"; (* Declare all keys your page handles *)
Keys.Enter;
Keys.Up;
Keys.Down;
(* ... *)
]Why this change?
- Enables compile-time key conflict detection
- Self-documents key bindings
- Foundation for auto-generated help system
- Enables future page registry and navigation features
Compiler error you'll see:
Error: Signature mismatch:
The value `handled_keys' is required but not provided
File "src/miaou_core/tui_page.mli", line 38, characters 2-40:
Expected declarationBenefits:
- ✅ Type-safe key handling (variants, not strings)
- ✅ Prevents global key conflicts automatically
- ✅ Runtime validation catches inter-page conflicts
- ✅ Clear error messages when conflicts occur
[Previous Releases]
- [Unreleased]
- [0.5.2] - 2026-05-06
- [0.4.2] - Unreleased
- [0.4.1] - Unreleased
- [0.4.0] - Unreleased
- [0.3.2] - Unreleased
- [0.3.0] - Unreleased
- [0.2.7] - 2026-02-07
- [0.2.6] - 2026-02-06
- [0.2.5] - 2026-02-05
- [0.2.0] - 2026-02-05
- [0.1.4] - 2026-01-22
- [0.1.3] - 2026-01-16
- [0.1.2] - 2026-01-16
- [0.1.1] - 2026-01-16
-
[Unreleased]
- API
- Breaking Changes (2026-01-08)
- Added (2026-01-08)
- Changed (2026-01-08)
- Fixed (2026-01-08)
- Added (2026-01-05)
- Added (2025-12-19)
- Added (2025-12-17)
- Changed (2025-12-17)
- Breaking Changes (2025-12-17)
- Changed (2025-12-15)
- Added (2025-12-11)
- Changed (2025-12-10)
- Breaking Changes (2025-12-12)
- Breaking Changes (2025-12-11)
- [Previous Releases]