package miaou-core
sectionYPositions = computeSectionYPositions($el), 10)"
x-init="setTimeout(() => sectionYPositions = computeSectionYPositions($el), 10)"
>
Miaou core/widgets (no drivers, no SDL)
Install
dune-project
Dependency
Authors
Maintainers
Sources
v0.5.2.tar.gz
md5=60a3b9f181f24572a06a9492532bfdda
sha512=fcc35a275066be2900e6201782faf47503076fa4640f08cf78067835a6f447b74613009e55b2ac799adb7ca46f1bffa261fc5971753f2cc3c6bef327511c7ef6
doc/src/miaou-core.lib_miaou_internal/headless_driver.ml.html
Source file headless_driver.ml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363(*****************************************************************************) (* *) (* SPDX-License-Identifier: MIT *) (* Copyright (c) 2025 Nomadic Labs <contact@nomadic-labs.com> *) (* Copyright (c) 2026 Mathias Bourgoin <mathias.bourgoin@atacama.tech> *) (* *) (*****************************************************************************) (* Minimal headless TUI driver used for testing Miaou pages without a terminal. *) [@@@warning "-32-34-37-69"] module Tui_page = Miaou_core.Tui_page module Modal_manager = Miaou_core.Modal_manager module Capture = Miaou_core.Tui_capture module Fibers = Miaou_helpers.Fiber_runtime module Clock = Miaou_interfaces.Clock module Timer = Miaou_interfaces.Timer module Clipboard = Miaou_interfaces.Clipboard module Style_context = Miaou_style.Style_context module Theme_loader = Miaou_style.Theme_loader module Keys = Miaou_core.Keys open LTerm_geom (** Convert a string key name to a [Keys.t], falling back to [Char s]. *) let key_of_string s = match Keys.of_string s with Some k -> k | None -> Keys.Char s (* Helper: apply any pending navigation from modal callbacks to pstate *) let ps = match Modal_manager.take_pending_navigation () with | Some (Navigation.Goto page) -> Navigation.goto page ps | Some Navigation.Back -> Navigation.back ps | Some Navigation.Quit -> Navigation.quit ps | None -> ps module Key_queue = struct let q : string Queue.t = Queue.create () let feed_keys ks = List.iter (fun k -> Queue.push k q) ks let take () = if Queue.is_empty q then None else Some (Queue.pop q) let clear () = Queue.clear q end module Screen = struct let buf = Buffer.create 4096 let clear () = Buffer.clear buf let append s = Buffer.add_string buf s let get () = Buffer.contents buf end let size_ref : LTerm_geom.size ref = ref {rows = 24; cols = 80} let set_size rows cols = size_ref := {rows; cols} let get_size () = !size_ref let current_page : (module Tui_page.PAGE_SIG) option ref = ref None let set_page p = current_page := Some p let feed_keys = Key_queue.feed_keys let get_screen_content () = Screen.get () let render_page_with (type s) (module P : Tui_page.PAGE_SIG with type state = s) (ps : s Navigation.t) = let size = get_size () in let base = P.view ps ~focus:true ~size in let base = match Miaou_internals.Modal_renderer.render_overlay ~cols:(Some size.cols) ~base ~rows:size.rows () with | Some overlay -> overlay | None -> base in let content = let flashes = Flash_bus.snapshot () in match flashes with | [] -> base | lst -> let toast_block = Flash_toast_renderer.render_snapshot ~position:`Bottom_right ~cols:size.cols lst in base ^ "\n" ^ toast_block in Screen.clear () ; Screen.append content ; Capture.record_frame ~rows:size.rows ~cols:size.cols content let render_only (type s) (module P : Tui_page.PAGE_SIG with type state = s) : unit = let ps = P.init () in render_page_with (module P) ps let max_iterations_ref = ref 20_000 let max_seconds_ref = ref 10.0 let set_limits ?iterations ?seconds () = Option.iter (fun i -> max_iterations_ref := i) iterations ; Option.iter (fun s -> max_seconds_ref := s) seconds let with_page_scope f = Fibers.with_page_scope f let run (initial_page : (module Tui_page.PAGE_SIG)) : [`Quit | `Back | `SwitchTo of string] = with_page_scope (fun () -> let module P : Tui_page.PAGE_SIG = (val initial_page) in let start_time = Unix.gettimeofday () in (* Clock capability — provides dt/now/elapsed to pages and widgets *) let clock_state = Clock.create_state () in Clock.register clock_state ; (* Timer capability — page-scoped periodic/one-shot callbacks *) let timer_state = Timer.create_state () in Timer.register timer_state ; (* Clipboard capability — no-op in headless mode *) Clipboard.register ~write:(fun _ -> ()) () ; let exceed_guard iteration = let elapsed = Unix.gettimeofday () -. start_time in if iteration >= !max_iterations_ref || elapsed >= !max_seconds_ref then failwith (Printf.sprintf "Headless_driver timeout: iteration=%d elapsed=%.3fs (limits: \ %d iter / %.1fs)" iteration elapsed !max_iterations_ref !max_seconds_ref) in let = function | Navigation.Quit -> `Quit | Navigation.Back -> `Back | Navigation.Goto name -> `SwitchTo name in let rec loop iteration (ps : P.pstate) : [`Quit | `Back | `SwitchTo of string] = exceed_guard iteration ; Clock.tick clock_state ; Timer.tick timer_state ; render_page_with (module P) ps ; match Key_queue.take () with | None -> ( (try Printf.eprintf "[driver][debug] No key in queue, refreshing page\n%!" with _ -> ()) ; let ps' = P.refresh ps |> apply_pending_modal_nav in match Navigation.pending ps' with | Some -> nav_to_outcome nav | None -> loop (iteration + 1) ps') | Some k -> ( (try Printf.eprintf "[driver][debug] Key event: '%s' modal_active=%b\n%!" k (Miaou_core.Modal_manager.has_active ()) with _ -> ()) ; let forced_switch = String.length k > 11 && String.sub k 0 11 = "__SWITCH__:" && Sys.getenv_opt "MIAOU_TEST_ALLOW_FORCED_SWITCH" = Some "1" in if not forced_switch then Capture.record_keystroke k ; if forced_switch then `SwitchTo (String.sub k 11 (String.length k - 11)) else let ps' = if Miaou_core.Modal_manager.has_active () then ( (try Printf.eprintf "[driver][debug] Modal manager handling key: '%s'\n%!" k with _ -> ()) ; Miaou_core.Modal_manager.handle_key k ; ps) else let key = key_of_string k in let new_ps, _result = P.on_key ps key ~size:(get_size ()) in new_ps in let ps' = apply_pending_modal_nav ps' in render_page_with (module P) ps' ; match Navigation.pending ps' with | Some -> nav_to_outcome nav | None -> loop (iteration + 1) ps') in set_page initial_page ; (* Load theme and wrap entire loop in mutable theme context. This ensures both page views AND modal rendering inherit the theme, and allows runtime theme updates via Style_context.set_theme. *) let theme = Theme_loader.load () in Style_context.with_mutable_theme theme (fun () -> loop 0 (P.init ()))) module Stateful = struct let initialized = ref false (* Store theme for use in render callbacks. The Stateful module uses closures that are called outside any effect handler, so we need a mutable ref here. This ref is read by render callbacks to get the current theme. *) let theme_ref : Miaou_style.Theme.t ref = ref Miaou_style.Theme.default let send_key_impl : (string -> unit) ref = ref (fun _ -> ()) let refresh_impl : (unit -> unit) ref = ref (fun () -> ()) let next_page_impl : (unit -> Navigation.nav option) ref = ref (fun () -> None) let current_page_name : string option ref = ref None (* Tracks the most recent automatic page switch so that callers can detect transitions even though [classify_next] returns [`Continue] after a successful switch. Reset by [consume_last_switch]. *) let last_switch : string option ref = ref None (* Allow tests to reload/change the theme *) let set_theme theme = theme_ref := theme let reload_theme () = let theme = Theme_loader.load () in theme_ref := theme ; theme (* Install closures for a given page module. This is the shared initialisation logic used by both [init] and [switch_to_page]. Accepts the packed existential [(module PAGE_SIG)] so it works with both a concrete module from [init] and a registry lookup. *) let install_page (page : (module Tui_page.PAGE_SIG)) : unit = let module P = (val page) in let ps = ref (P.init ()) in let clock_state = Clock.create_state () in Clock.register clock_state ; let timer_state = Timer.create_state () in Timer.register timer_state ; Clipboard.register ~write:(fun _ -> ()) () ; (* Read theme from ref each render, allowing runtime updates *) let render () = Style_context.with_theme !theme_ref (fun () -> render_page_with (module P) !ps) in let handle_modal_key k = if Miaou_core.Modal_manager.has_active () then Miaou_core.Modal_manager.handle_key k in let handle_key (k : string) = Clock.tick clock_state ; Timer.tick timer_state ; if Miaou_core.Modal_manager.has_active () then ( handle_modal_key k ; render ()) else let key = key_of_string k in let new_ps, _result = P.on_key !ps key ~size:(get_size ()) in ps := new_ps ; render () in send_key_impl := handle_key ; (refresh_impl := fun () -> Clock.tick clock_state ; Timer.tick timer_state ; ps := P.refresh !ps ; render ()) ; (next_page_impl := fun () -> Navigation.pending !ps) ; render () let init (type s) (module P : Tui_page.PAGE_SIG with type state = s) : unit = with_page_scope (fun () -> install_page (module P : Tui_page.PAGE_SIG) ; current_page_name := None ; last_switch := None ; initialized := true) (** Switch the stateful driver to a different page looked up from the {!Miaou_core.Registry}. Returns [true] if the page was found and the switch succeeded. *) let switch_to_page name = match Miaou_core.Registry.find name with | None -> false | Some (module P) -> install_page (module P : Tui_page.PAGE_SIG) ; current_page_name := Some name ; last_switch := Some name ; true (** Return and consume the last automatic page switch, if any. This allows callers to detect that a switch occurred even though [classify_next] now returns [`Continue] after a successful switch. *) let consume_last_switch () = let s = !last_switch in last_switch := None ; s let ensure () = if not !initialized then invalid_arg "Stateful driver not initialised" let classify_next () = match !next_page_impl () with | Some Navigation.Quit -> `Quit | Some Navigation.Back -> `Back | Some (Navigation.Goto name) -> `SwitchTo name | None -> `Continue (* When the current page signals a navigation, try to switch automatically. Returns the final classification after any switch. *) let maybe_auto_switch () = match classify_next () with | `SwitchTo name as r -> if switch_to_page name then `Continue else r | other -> other let send_key k = ensure () ; let forced_switch = String.length k > 11 && String.sub k 0 11 = "__SWITCH__:" && Sys.getenv_opt "MIAOU_TEST_ALLOW_FORCED_SWITCH" = Some "1" in if forced_switch then let target = String.sub k 11 (String.length k - 11) in if switch_to_page target then `Continue else `SwitchTo target else ( Capture.record_keystroke k ; !send_key_impl k ; maybe_auto_switch ()) let idle_wait ?(iterations = 1) ?(sleep = 0.0) () = ensure () ; let rec loop i = if i <= 0 then `Continue else match maybe_auto_switch () with | (`Quit | `Back | `SwitchTo _) as r -> r | `Continue -> !refresh_impl () ; (* Yield to the eio scheduler so that fibers spawned via Fiber.fork (e.g. async LLM calls) can make progress. *) (match Fibers.switch_opt () with | Some _ -> Eio.Fiber.yield () | None -> ()) ; (if sleep > 0.0 then try (Unix.sleepf [@allow_forbidden "headless driver uses blocking sleep"]) sleep with _ -> ()) ; loop (pred i) in loop iterations end
sectionYPositions = computeSectionYPositions($el), 10)"
x-init="setTimeout(() => sectionYPositions = computeSectionYPositions($el), 10)"
>