package matcha
sectionYPositions = computeSectionYPositions($el), 10)"
x-init="setTimeout(() => sectionYPositions = computeSectionYPositions($el), 10)"
>
On This Page
A React-like terminal UI library for ReasonML
Install
dune-project
Dependency
Authors
Maintainers
Sources
v0.1.0.tar.gz
md5=ddf4dd3bce67dde4821d4ddfd86580f3
sha512=aa1a2e3048d27817c77bc3cd7926da6e1ef7555221c7a48f64948a7515368bf13098f248f87acc2deee829f103ae44ae4c0bdcfe85dbb6cd93149de740a44d4b
Description
Build interactive command-line applications with a familiar component-based architecture, hooks, and JSX syntax. Features include useState, useEffect, Context API, and JSX support via PPX.
Added to opam-repository:
README
Matcha
A React-like terminal UI library for ReasonML/OCaml. Build interactive command-line applications with a familiar component-based architecture, hooks, and JSX syntax.
Features
- React-like API: Components, hooks (
useState,useEffect), and JSX syntax - Declarative UI: Describe your UI as a tree of elements
- Built-in hooks:
useState,useEffect,useKeyDown,useQuit - Context API: Share state across components without prop drilling
- Layout primitives:
Column,Row,Box,SplitView - Styling: Bold, dim, italic, underline, inverted text
- PPX support:
[@component]decorator for cleaner component definitions
Installation
Add to your dune-project:
(package
(name your-app)
(depends
(matcha (>= 0.1.0))))Add to your dune file:
(executable
(name main)
(libraries matcha)
(preprocess (pps ppx_component)))Quick Start
open Matcha;
[@component]
let make = () => {
let quit = Event.useQuit();
let (count, setCount) = Component.useState(0);
Event.useKeyDown((key, _) => {
switch (key) {
| Key.Arrow_up => setCount(count + 1)
| Key.Arrow_down => setCount(max(0, count - 1))
| Key.Char('q') => quit()
| _ => ()
}
});
<Column>
<Bold> <Text> "Counter" </Text> </Bold>
<Text> {"\nCount: " ++ string_of_int(count)} </Text>
<Dim> <Text> "\n\nPress ↑/↓ to change, Q to quit" </Text> </Dim>
</Column>;
};
module App = { let make = make; };
let () = Runtime.start((module App));Core Concepts
Components
Components are defined using the [@component] decorator:
[@component]
let make = (~name: string, ~age: int) => {
<Text> {name ++ " is " ++ string_of_int(age) ++ " years old"} </Text>
};Elements
Built-in JSX elements:
Element | Description |
|---|---|
| Render text |
| Bold text |
| Dimmed text |
| Italic text |
| Underlined text |
| Inverted colors |
| Vertical layout |
| Horizontal layout |
| Constrained box with width/height |
Hooks
useState
let (value, setValue) = Component.useState(initialValue);useEffect
// Run when dependencies change
Hooks.useEffect(() => {
// Effect code
Some(() => {
// Cleanup (optional)
})
}, [|dependency1, dependency2|]);
// Run every render
Hooks.useEffectAlways(() => {
// Effect code
None
});useKeyDown
Event.useKeyDown((key, modifiers) => {
switch (key, modifiers) {
| (Key.Char('q'), _) => quit()
| (Key.Char('c'), {Key.ctrl: true, _}) => quit()
| (Key.Arrow_up, _) => moveUp()
| _ => ()
}
});useQuit
let quit = Event.useQuit();
// Call quit() to exit the applicationContext
Create shared state accessible by any component:
// Define context
module ThemeContext = {
include Context.Make({
type t = string;
let default = "dark";
});
};
// Provide value
ThemeContext.provide("light", children);
// Consume value
let theme = ThemeContext.use();Key Types
type Key.t =
| Arrow_up | Arrow_down | Arrow_left | Arrow_right
| Char(char)
| Enter | Escape | Backspace
| Unknown;
type Key.modifiers = {
ctrl: bool,
alt: bool,
shift: bool,
};Examples
Hello World
open Matcha;
[@component]
let make = () => {
let quit = Event.useQuit();
Event.useKeyDown((key, _) => {
switch (key) {
| Key.Char('q') => quit()
| _ => ()
}
});
<Column>
<Bold> <Text> "Hello, World!" </Text> </Bold>
<Dim> <Text> "\nPress Q to quit" </Text> </Dim>
</Column>;
};Counter
open Matcha;
[@component]
let make = () => {
let quit = Event.useQuit();
let (count, setCount) = Component.useState(0);
Event.useKeyDown((key, _) => {
switch (key) {
| Key.Arrow_up => setCount(count + 1)
| Key.Arrow_down => setCount(max(0, count - 1))
| Key.Char('r') => setCount(0)
| Key.Char('q') => quit()
| _ => ()
}
});
<Column>
<Text> {"Count: " ++ string_of_int(count)} </Text>
<Dim> <Text> "\n↑: Inc ↓: Dec r: Reset q: Quit" </Text> </Dim>
</Column>;
};Async Data Fetching with useEffect
[@component]
let make = (~userId: string) => {
let (data, setData) = Component.useState(None);
let (loading, setLoading) = Component.useState(false);
Hooks.useEffect(() => {
setLoading(true);
let cancelled = ref(false);
let _ = Thread.create(() => {
Thread.delay(1.0); // Simulate network delay
if (!cancelled^) {
setData(Some("User data for " ++ userId));
setLoading(false);
}
}, ());
Some(() => { cancelled := true }); // Cleanup cancels fetch
}, [|userId|]);
if (loading) {
<Dim> <Text> "Loading..." </Text> </Dim>;
} else {
switch (data) {
| None => <Text> "No data" </Text>
| Some(d) => <Text> d </Text>
};
};
};Running Examples
# Build all examples
dune build
# Run examples
dune exec matcha-example-hello-world
dune exec matcha-example-counter
dune exec matcha-example-people-listLicense
MIT
sectionYPositions = computeSectionYPositions($el), 10)"
x-init="setTimeout(() => sectionYPositions = computeSectionYPositions($el), 10)"
>
On This Page