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 gallery Games group, 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 via Arcade_kit.Score_store (miaou_links and miaou_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_MODE override). No public library API is removed or changed. - MIAOU Crypt demo (
example/demos/miaou_crypt/): a pseudo-3-D dungeon crawler registered in the gallery Games group, with seven hand-authored floors, raycast walls, billboarded enemies, boss encounters, pickups, minimap support, and deterministic debug stepping via MIAOU_CRYPT_DEBUG=1. It demonstrates how to build a first-person game on top of Framebuffer_widget, Arcade_kit.Particles, and Arcade_kit.Score_store while bounding framebuffer size and raycast work for terminal performance (MIAOU_CRYPT_PIXEL_MODE override). 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.Particles is a pre-allocated ring-buffer particle pool with spawn / spawn_burst / tick ~dt ~ax ~ay / iter and zero per-frame allocation in the hot path. Arcade_kit.Hue ships 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_fx exposes flash and shake overlays decaying over a duration. Arcade_kit.Score_store reads/writes per-demo high scores under $XDG_STATE_HOME/miaou/<demo>.score (best-effort, silent on IO error). Arcade_kit.Pixel_mode.resolve returns Caps.Octant by 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's Games group. It demonstrates framebuffer-based arcade rendering, persistent high scores through Arcade_kit.Score_store, deterministic turn-based stepping via MIAOU_FORCE_TURN_BASED=1, and a dockable Force pod (d detach/recall, f flip 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_MODE override). 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 with Tab) shows simulated time, the speed multiplier (1–5 set ×1 / ×10 / ×100 / ×1000 / ×10 000 days per real second), each planet's current orbital phase in degrees, and inline help. Space/p pauses, o toggles orbit rings, l toggles labels, r resets time. The pixel mode is auto-detected (Sixel on capable terminals, Octant otherwise) and overridable via MIAOU_SOLAR_PIXEL_MODE. Registered under the gallery's Showcases group. - Geo Quiz demo (
example/demos/geo_quiz/): a city-locator showcase game registered in the gallery's Games group. 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 embedded coastline.bin (Natural Earth ne_50m, 60 K points), landmask.bin (rasterised ne_50m_land polygons at 720×360, 32 KB) and cities.bin (filtered GeoNames cities15000, 33 K cities) blobs. Round mode draws a colour, aspect-corrected equirectangular world map (Octant sub-pixels via Framebuffer_widget); the player moves a crosshair with arrow keys, Shift+arrow for big jumps, or a mouse click, and presses Enter to lock in the guess. A 30 s Timer.set_timeout auto-locks a zero-score guess on expiry. Scoring combines a haversine distance score (max 0 (1000·(1 − d/max_d)), with max_d = 5000 km for tiers 1–3 and 2500 km for 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 an Octant_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 exposes latlon_to_xyz and haversine_km helpers for callers that need the same sphere math. Self-registers via Miaou_registry. - Reusable
Prompt helpers (Miaou_core.Prompt): thin wrappers over Modal_manager.confirm_with_extract exposing Prompt.confirm, Prompt.input, and Prompt.select so application code no longer has to assemble the modal-page boilerplate by hand. Each helper takes an on_result callback receiving the user's choice (or None/false for 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 under example/demos/ exercising recently-added building blocks. responsive/ swaps between 4-column / 2×2-grid / stacked layouts as the terminal narrows. inline_select/ puts a Select_widget inline on a page rather than as a centred dialog. inline_cli/ is a tiny "list current directory" page that, when launched via its run.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 own run.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 form page=<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 resolves page=* 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_widget now 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. Setting MIAOU_NERD_FONT=1 switches to a Nerd Font glyph set for terminals using a Nerd-patched font. The icon table lives in the new Miaou_widgets_layout.File_icons module and is reusable from any custom widget that lists files. Responsive: a tiny utility for picking among layouts based on terminal width. Responsive.pick walks an ascending list of {max_width; layout} breakpoints and returns the first match (mobile-first ordering). Layouts can be Flex_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 new Matrix_config.inline_mode field and Matrix_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 own render, validate, and on_key; the wizard owns navigation (Enter advances when validation passes, Shift+Tab returns, Esc cancels), breadcrumb chrome, and finished/cancelled state. Adds example/demos/wizard/ with a 3-step "pick backend → name it → review" flow.Textarea_widget undo / redo: the multi-line editor now supports Ctrl+Z (undo) and Ctrl+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_widget keyboard navigation: the tree widget now responds to Up/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 helpers Tree_widget.expand_all, collapse_all, is_expanded, and flatten_visible are 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_frame callback in headless runner: Headless_json_runner.run and Runner_tui.run accept 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 (like Web_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 on Sparkline_widget, Line_chart_widget, and Bar_chart_widget via a new ~mode:Octant parameter. - 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_frame callback is registered (e.g. by Web_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_widget and Octant chart modes side-by-side.
Fixed
- Opam release metadata: removed the temporary
pin-depends entry and local pin setup instructions for ppx_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_key and peek_key now handle ESC[5~, ESC[6~, ESC[H / ESCOH / ESC[1~ / ESC[7~, and ESC[F / ESCOF / ESC[4~ / ESC[8~ instead of returning Unknown; key_to_string and is_nav_key are 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_wait each iteration, which allowed it to interleave with the command handler's own idle_wait and concurrently mutate shared page-state (double-ticking clocks/timers). The daemon now reads the cached screen content directly via HD.Screen.get without 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_defaults now 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 caused apply_bg_fill to 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_widget page) 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_render is no longer called from the main loop (neither on modal transitions nor during periodic scrub). Both cases now only call mark_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_text calls in the Invaders demo now carry an explicit bg matching the current game or HUD background. Previously, entities drawn with bg=-1 clobbered the fill_rect-painted background, producing black horizontal bars wherever sprites appeared. - Periodic scrub interval: Default
scrub_interval_frames reduced 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 from themed_border clearing 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_ to Box_widget.border_style for borderless containers. Pattern matches on border_style may need a new case.
Added
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
Changed
- Input parser: added
AltEnter key 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_isig and 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.pending now returns Navigation.nav option (Goto of string | Back | Quit) instead of string option, replacing magic strings ("__BACK__", "__QUIT__"). - Modal navigation callback API:
Modal_manager.set_pending_navigation now takes Navigation.nav instead of string. - Page transition hooks: page transition handler records now expose an explicit
on_back callback. - Matrix IO internals:
Matrix_io.t removes 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) exposing dt, now, and elapsed thunks to pages/widgets. - Page-scoped timers (
miaou_interfaces.Timer) with set_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.compose and Canvas.compose_new for 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.md plus 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 (set 0 to 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_hints from pages are now correctly rendered in the footer bar
[0.2.6] - 2026-02-06
Added
Fixed
- Keymap dispatch bypassing
handle_key — Drivers now always route keys through on_key, fixing Focus_ring Tab navigation when Tab was in page keymap - Lambda-term driver Enter key — Enter now goes through
on_key like other keys
Changed
- BREAKING:
PAGE_SIG now requires on_key, on_modal_key, and key_hints methods Demo_page.MakeSimple functor for demos without explicit key_hints- Legacy
handle_key, handle_modal_key, keymap deprecated 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_key returns Handled | 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
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_defaults functor provides sensible defaults for optional functions- Reduces boilerplate significantly for simple pages
Grid Layout (Miaou_widgets_layout.Grid_layout) for CSS-grid-like layouts:
- Row and column track definitions with
Fr, Px, Auto sizing grid_area placement for precise cell positioning- Gap support (row_gap, column_gap)
- Span support for multi-cell items
- Automatic content fitting
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_password and viewer_password authentication
Composable MiaouTerminal(container, options) JS factory replacing the IIFE in client.js:
wsPath option (/ws or /ws/viewer)onRole, onStatusChange, onAuthRequired callbackssessionStorage keys scoped by wsPath- Returns
{ term, fitAddon, reconnect(pw), getRole() }
Custom HTML pages and extra assets for the web driver:
~controller_html and ~viewer_html optional parameters on Web_driver.runextra_asset type 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_PASSWORD environment variable (falls back to MIAOU_WEB_PASSWORD)
Changed
- Web driver routing refactored:
/ws always creates controller, /ws/viewer always creates viewer (previously role was assigned by connection order on single /ws endpoint)
[0.1.4] - 2026-01-22
Fixed
[0.1.3] - 2026-01-16
Added
Changed
[0.1.2] - 2026-01-16
Added
Changed
[0.1.1] - 2026-01-16
Fixed
Changed
- File browser fixes for edit mode (Space key handling, selection highlight)
- Version bump to 0.1.1
[Unreleased]
API
- Rename
Vsection.render parameter ~footer to ~content_footer to clarify it is for page content, not driver-generated keymap footers. - Clarify
PAGE_SIG docs: keymap footers are auto-generated by drivers, ? is reserved but may appear in keymaps for display, and handled_keys is only for conflict detection. - Add
display_only flag 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_modal helper plus File_browser_widget.key_hints to 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
end
New 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
end
Migration guide:
- Remove
next_page from 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
pstate type alias:
type pstate = state Navigation.t
- Update
init to wrap state:
(* Before *)
let init () = { items = []; cursor = 0; next_page = None }
(* After *)
let init () = Navigation.make { items = []; cursor = 0 }
- Remove
next_page and enter functions (they no longer exist). - Update all handlers to use
pstate and 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
view to 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.cursor
Navigation 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 to goto "__BACK__")Navigation.quit : 'a t -> 'a t - Quit application (equivalent to goto "__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 enter
Why this change?
- Eliminates the error-prone
next_page field 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
refresh in service_cycle - Consistent behavior across all drivers (Matrix, Lambda-term, SDL)
- The pattern
Page.service_cycle (Page.refresh ps) 0 is 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 *)
ps
- Wrap toggle: Press
w to 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)
Added (2025-12-19)
Debounced Validation for Validated Textbox Widget
debounce_ms parameter for Validated_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 in service_cycle)flush_validation - Force immediate validation (useful before form submission)has_pending_validation - Check if validation is pending
- Set
debounce_ms=0 to disable debouncing (legacy immediate behavior)
Global Render Notification System
Miaou_helpers.Render_notify module 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.Debounce module for generic debounce timing- Thread-safe implementation using
Atomic operations - Functions:
create, mark, is_ready, clear, has_pending, check_and_clear - Configurable debounce period in milliseconds (default: 250ms)
- Caching for directory listings -
list_entries_with_parent now caches results - Caching for writable status -
is_writable checks 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
h to toggle visibility of hidden files/directories (starting with .) - New
show_hidden parameter in open_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_drain module 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/tty so modals resize with the terminal even when System is 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 on miaou-coremiaou-driver-sdl: SDL driver with SDL2 dependencies (tsdl, tsdl-ttf, tsdl-image)miaou-widgets-display-sdl: SDL-specific widget implementationsmiaou-runner: Runner with miaou-driver-sdl as optional dependencymiaou-tui: Meta-package for terminal-only installs (no SDL)miaou: Meta-package for full install (includes SDL)miaou-core.lib: The convenience Miaou module (re-exporting Core, Widgets, etc.) is now part of miaou-core, available to terminal-only users
Terminal-only users can now: opam install miaou-tui
Breaking Changes (2025-12-17)
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_switch for structured cleanup; Fiber_runtime exposes 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)
- 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=1 to 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, and Sparkline_widget Braille_canvas module 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.t with 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)
- Significant performance improvements across all widgets (8-24x faster in some cases)
- Replaced
String.concat with buffer-based rendering throughout codebase - Introduced
Helpers.pad_to_width eliminating 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-unix replaced with cohttp-eio- Terminal driver uses
Eio_unix.await_readable for input polling - Background tasks use
Eio.Fiber instead of Thread.create
Migration guide:
- Wrap your main function in
Eio_main.run and 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-eio
New modules:
Miaou_helpers.Fiber_runtime — shared Eio runtime managementMiaou_widgets_display.File_pager — Eio-based file tailing pager
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 in
Migration guide:
- Remove calls to
set_notify_render - Pass
~notify_render parameter to open_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 [] in
Why 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_render function removed
Compiler error you'll see:
Error: Unbound value Pager_widget.set_notify_render
Breaking 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
end
Migration 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 declaration
Benefits:
- ✅ 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]