Skip to content

Add clipboard demo with modal workflow#128

Open
vch9 wants to merge 15 commits intomainfrom
feat/clipboard-demo
Open

Add clipboard demo with modal workflow#128
vch9 wants to merge 15 commits intomainfrom
feat/clipboard-demo

Conversation

@vch9
Copy link
Collaborator

@vch9 vch9 commented Mar 4, 2026

Summary

Adds a new clipboard demo that demonstrates copying text to the system clipboard using a modal-based workflow.

Features

  • Modal workflow: Press Space/Enter to open a modal, type text, press Enter to copy
  • Quick copy samples: Press 1-5 for instant copy of predefined snippets
  • Toast notifications: Visual feedback when text is copied
  • Copy tracking: Shows copy counter and last copied text

Demo Location

  • Gallery: Core → Clipboard
  • Standalone: dune exec miaou.clipboard-demo

Requirements

Clipboard works with:

  • Native tools: wl-clipboard (Wayland), xclip (X11)
  • Or terminals supporting OSC 52 (kitty, alacritty, wezterm)

mathiasbourgoin and others added 13 commits March 3, 2026 14:13
…less

Any Miaou TUI app can now be driven headlessly by an AI agent or CI
script without a separate binary or app changes.  Set
MIAOU_DRIVER=headless and the app reads newline-delimited JSON commands
from stdin and writes JSON frames to stdout.

Protocol (subset):
  {"cmd":"render"}              → {"type":"frame","text":"...","rows":R,"cols":C}
  {"cmd":"key","key":"Tab"}     → frame (after 3 idle ticks)
  {"cmd":"tick","n":N}          → frame
  {"cmd":"resize","rows":R,"cols":C} → frame
  {"cmd":"quit"}                → {"type":"nav","action":"quit"}

Implementation:
- New src/miaou_runner/headless_json_runner.ml — JSON loop using
  Lib_miaou_internal.Headless_driver.Stateful; inline ANSI strip.
- runner_tui.ml — env-var check at top of run dispatches to headless
  runner before any terminal backend initialisation.
- dune — adds headless_json_runner module, miaou-core.core,
  miaou-core.lib_miaou_internal, yojson, unix deps.
- New standalone `miaou-registry` opam package (zero deps beyond stdlib):
  `Miaou_registry.{register,list,find,search}` with Mutex-safe global table.

- 23 widgets self-register at library load time via `[%blob "...mli"]`:
  8 display (pager, list, description_list, table, tree, sparkline,
  line_chart, bar_chart), 8 input (textbox, textarea, validated_textbox,
  select, checkbox, radio, button, switch), 7 layout (box, progress,
  spinner, file_browser, card, toast, canvas).

- Non-widget utility modules marked `[@@@enforce_exempt]`.

- `ppx_enforce` (new companion to ppx_forbid, same repo) enforces the
  presence of `Miaou_registry.register` in every widget `.ml` — new
  widgets without a registration call fail to compile.
  Config: `.ppx_enforce` at project root.

- Explicit `(preprocessor_deps (file ...mli))` per widget to make blobs
  work without glob-induced dependency cycles.
…le_key

Pages that implement the new on_key API (like the miaou-composer designer)
had their key events silently dropped because the headless driver was
calling the deprecated handle_key stub (which returns pstate unchanged).
Switch both the batch run() loop and Stateful.install_page to call
P.on_key with a proper Keys.t value converted from the string key name.
The row_gap code emitted \n BEFORE each gap blank line but not AFTER
the last one, so the next row's first content line was concatenated
onto the gap blank. When Flex truncates to width, the actual content
was lost (only whitespace survived).

Fix: emit \n AFTER each gap blank line instead of before. This ensures
each gap blank and each row's first line start on their own buffer line.

Add row_gap and row_gap_multi_line test cases to verify.
The headless driver's idle_wait loop was purely synchronous,
never yielding to the eio scheduler. Fibers spawned via
Fiber.fork (e.g. async LLM calls in crucible) were never
scheduled, causing the app to spin indefinitely with a
spinner but no actual work happening.

Add Eio.Fiber.yield() on each iteration so forked fibers
can make progress between ticks.
The headless JSON runner's main loop used `input_line stdin` which is a
blocking POSIX syscall.  This froze the entire eio event loop between
commands, preventing background fibers (e.g. LLM subprocess I/O) from
making progress.

Replace with Eio_unix.run_in_systhread to run the blocking read in a
system thread, keeping the eio event loop active so background fibers
can process I/O events (subprocess stdout, network calls, etc.).
Add optional ?on_frame:(string -> unit) parameter to
Headless_json_runner.run and Runner_tui.run. When provided, the
callback is invoked with the raw ANSI frame content (before
stripping) on every frame emit.

Add Web_viewer module to miaou-driver-web: a standalone viewer-only
HTTP+WebSocket server that serves the xterm.js viewer page and
broadcasts raw ANSI frames to all connected viewers. Designed to
run alongside the headless driver so a human can observe an AI
agent's TUI session in a browser.
xterm.js with convertEol:false treats bare LF as move-down without
carriage return. The headless driver produces newline-separated text
with bare LF. Convert \n to \r\n in broadcast and prepend clear-screen
so each frame redraws cleanly in the viewer terminal.
- on_frame callback now passes ~rows ~cols so viewers know the terminal size
- Web_viewer.broadcast accepts ~rows ~cols, sends {"type":"dimensions",...}
  JSON to viewers when dimensions change
- New viewers receive current dimensions + last frame on connect (no blank screen)
- client.js: viewers handle dimensions message to resize xterm.js, FitAddon
  auto-fit disabled for viewers (size controlled by server)
- client.js: auto-reconnect with 2s retry when viewer WebSocket disconnects
Document the new Web_viewer module, on_frame callback, viewer
auto-reconnect, and dimension sync in the unreleased section.
When a web viewer is attached (on_frame callback set), fork an Eio
daemon fiber that re-renders the screen every 200ms and broadcasts
changes.  This keeps the viewer up-to-date even when the agent is
idle, catching background activity (timers, async I/O, spinners).
Add a new clipboard demo that demonstrates copying text to the system
clipboard using a modal-based workflow:

- Open modal with Space/Enter to type custom text
- Press Enter in modal to copy text to clipboard
- Quick copy samples (1-5 keys) for instant copying
- Toast notifications for user feedback
- Copy counter and last copied text display

The demo works with both native clipboard tools (wl-copy, xclip, xsel,
pbcopy) and falls back to OSC 52 escape sequences when native tools
are unavailable.

Co-Authored-By: Claude <noreply@anthropic.com>
vch9 and others added 2 commits March 4, 2026 12:32
When copying from the modal, the demo now shows toast notifications
indicating success or failure, just like the quick copy samples (1-5).

The fix uses a global ref to store pending copies from the modal's
on_close callback, which is then processed in the refresh cycle to
update the state and show appropriate toast messages.

This ensures users get immediate feedback when they press Enter to
copy text from the modal.
- Change toast from Success to Info level
- Change 'Copied' to 'Sent to clipboard' to reflect uncertainty
- Add instructions about native clipboard tools requirement
- Explain OSC 52 fallback limitations

The clipboard API is fire-and-forget and cannot verify success,
so we should be honest with users about reliability.

Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants