iocraft

repository·main·Indexed 23 days ago

https://github.com/ccbrown/iocraft

A Rust library for creating declarative text-based user interfaces (TUIs) and command-line interfaces (CLIs). It implements React-like concepts including components, hooks, and declarative layouts using the element! macro and the taffy flexbox layout engine. Key features include support for fullscreen terminal applications, asynchronous data handling via use_future, state management with use_state, and a ContextProvider for sharing state across component trees.

Tokens
10.2K
Snippets
25
Records
39
Agent score
79%

What's inside iocraft

  1. Core iocraft features and capabilities

    main

    iocraft is designed for building beautiful CLIs, TUIs, and text-based IO. Key capabilities include:

    • Declarative UI: Define interfaces using a clean, React-like syntax via the element! macro.
    • Flexbox Layouts: Organize UI elements using the taffy layout engine.
    • Styling: Output colored and styled text to terminals or ASCII output.
    • Interactivity: Create animated or interactive elements using hooks and event handling.
    • Fullscreen Apps: Easily build full-screen terminal applications.
    • Performance: Pass props and context by reference to minimize cloning.
    • Cross-platform: Broad support for both Unix and Windows terminals.
  2. How to create custom components with #[component]

    main

    You can define your own reusable UI components using the #[component] macro. A component function takes hooks: Hooks as an argument and returns an object that implements Into<AnyElement<'static>>.

    Inside a component, you can use hooks like use_state for managing local state and use_future for running asynchronous tasks (e.g., timers or network requests). To run an interactive component in a fullscreen terminal application, use element!(ComponentName).render_loop() inside an async executor like smol.

    use iocraft::prelude::*;
    use std::time::Duration;
    
    #[component]
    fn Counter(mut hooks: Hooks) -> impl Into<AnyElement<'static>> {
        let mut count = hooks.use_state(|| 0);
    
        hooks.use_future(async move {
            loop {
                smol::Timer::after(Duration::from_millis(100)).await;
                count += 1;
            }
        });
    
        element! {
            Text(color: Color::Blue, content: format!("counter: {}", count))
        }
    }
    
    fn main() {
        smol::block_on(element!(Counter).render_loop()).unwrap();
    }
  3. Quickstart with iocraft

    main

    To create a simple text-based UI, use the element! macro to declare components like View and Text. For a one-off print to the terminal, you can call .print() on the result of the element! macro.

    iocraft uses a declarative API inspired by React, allowing you to build complex layouts using flexbox (powered by taffy) and styled text output.

    use iocraft::prelude::*;
    
    fn main() {
        element! {
            View(
                border_style: BorderStyle::Round,
                border_color: Color::Blue,
            ) {
                Text(content: "Hello, world!")
            }
        }
        .print();
    }
  4. Explore iocraft feature examples

    main

    The iocraft-examples package contains several demonstrations of core features and patterns. Key examples include:

    • Borders: Showcases various border styles (borders.rs).
    • Calculator: Demonstrates clickable buttons and light/dark mode themes (calculator.rs).
    • Context: Demonstrates custom context management using ContextProvider and use_context (context.rs).
    • Dynamic Components: Renders components that spawn futures to update state (e.g., a counter incrementing every 100ms) (counter.rs).
    • Forms: Uses mutable reference props to surface user input to a caller upon submission (form.rs).
    • Fullscreen: Renders to an alternate terminal buffer to prevent user scrolling (fullscreen.rs).
    • Layout & Positioning: Demonstrates absolute positioning for overlapping elements (overlap.rs) and scrollable text using ScrollView (scrolling.rs).
    • Input & Output: Demonstrates keyboard input handling (use_input.rs) and continuous text logging above rendered components (use_output.rs).
    • Asynchronous Data: Demonstrates loading data from remote APIs in response to user input (weather.rs).
  5. How auto-scroll works in ScrollView

    main

    The auto_scroll feature in ScrollView allows the view to stay pinned to the bottom as content grows (e.g., for a log viewer).

    Behavior:

    • When auto_scroll is enabled, the view stays at the bottom of the content.
    • If a user manually scrolls up, auto-scroll is disengaged.
    • If the user scrolls back down to the bottom of the content, auto-scroll is re-engaged.
    • This behavior is controlled by the user_scrolled_up state within the component.
  6. Avoid invariant fields in `Props` structs

    main

    When defining properties, ensure that no field makes the struct invariant. Invariant fields (such as a mutable reference &'a mut T) will prevent the struct from being used as Props and will cause a compilation error. This is because Props requires the type to be covariant to ensure memory safety during component updates and re-renders.

    // This will fail to compile
    #[derive(Default, Props)]
    struct MyProps<'a> {
       foo: &'a mut MyType<'a>,
    }
  7. Use ContextProvider to share state across the component tree

    main

    The ContextProvider component allows you to provide a piece of data (a Context) to all its descendant components. Descendant components can then access this data using the use_context::<T>() hook.

    ContextProvider is a transparent component, meaning it does not affect the layout of its children. You can provide context using different ownership models via Context::owned, Context::from_ref, or Context::from_mut.

    # use iocraft::prelude::*;
    struct NumberOfTheDay(i32);
    
    #[component]
    fn MyContextConsumer(hooks: Hooks) -> impl Into<AnyElement<'static>> {
        // Access the provided context using the type as the key
        let number = hooks.use_context::<NumberOfTheDay>();
    
        element! {
            View(border_style: BorderStyle::Round, border_color: Color::Cyan) {
                Text(content: "The number of the day is... ")
                Text(color: Color::Green, weight: Weight::Bold, content: number.0.to_string())
                Text(content: "!")
            }
        }
    }
    
    fn main() {
        element! {
            // Provide the context to the tree
            ContextProvider(value: Context::owned(NumberOfTheDay(42))) {
                MyContextConsumer
            }
        }
        .print();
    }
  8. How `Handler` and `HandlerMut` work together

    main

    In iocraft, Handler (immutable/clonable) and HandlerMut (mutable/non-clonable) are designed to be interoperable.

    • Upgrading: You can convert a Handler<T> into a HandlerMut<'static, T> using From. This is useful when a component expects a mutable handler but you only have an immutable one.
    • Usage Pattern: For component properties, prefer HandlerMut. If you need to share a single logic block across multiple UI elements (like two different buttons triggering the same action with different arguments), use Handler and the .bind() method.
    // Example: Sharing a handler across multiple buttons using .bind()
    // inside an element! macro context
    
    let counter_handler: Handler<_> = hooks.use_async_handler(move |n| async move {
        counter += n;
    });
    
    element! {
        Fragment {
            Button(handler: counter_handler.bind(1), has_focus: true) {
                Text(content: "[ +1 ]")
            }
            Button(handler: counter_handler.bind(-1)) {
                Text(content: "[ -1 ]")
            }
        }
    }
  9. Get started with iocraft

    main

    To build a basic UI in iocraft, use the element! macro to declare your components in a declarative, React-like syntax. You can then call .print() on the resulting element to output it to the terminal.

    Built-in components include View, Text, and TextInput. Layouts are powered by taffy (flexbox).

    use iocraft::prelude::*;
    
    fn main() {
        element! {
            View(
                border_style: BorderStyle::Round,
                border_color: Color::Blue,
            ) {
                Text(content: "Hello, world!")
            }
        }
        .print();
    }
  10. Use the `use_ref` hook to store mutable values without re-renders

    main

    The use_ref hook allows you to store a value that can be modified but does not trigger a component re-render when changed. This is useful for imperative control, such as managing component handles or storing values that don't affect the UI directly.

    Ref<T> is a copyable wrapper for the stored value. It is owned by the component and will panic if you attempt to access it after the component has been dropped (use try_read or try_write to avoid panics).

    Warning on Deadlocks: Using .read() or .write() can cause deadlocks if you hold multiple references to the same Ref. Writes to a Ref will be blocked as long as any reference returned by .read() exists.

    # use iocraft::prelude::*;
    # #[component]
    # fn FormField(mut hooks: Hooks) -> impl Into<AnyElement<'static>> {
    # let mut value = hooks.use_state(|| "".to_string());
    # let initial_cursor_position = 0;
    let mut handle = hooks.use_ref_default::<TextInputHandle>();
    
    hooks.use_effect(
        move || handle.write().set_cursor_offset(initial_cursor_position),
        (),
    );
    
    element! {
        View(
            background_color: Color::DarkGrey,
            width: 30,
        ) {
            TextInput(
                has_focus: true,
                value: value.to_string(),
                on_change: move |new_value| value.set(new_value),
                handle,
            )
        }
    }
    # }