superconsole

repository·main·Indexed 19 days ago

https://github.com/facebookincubator/superconsole

A component-based framework for building Rust Text-based User Interfaces (TUIs) that prioritizes testability, ease of composition, and flexibility. It provides a line-based abstraction for terminal rendering, separates state from rendering logic, and uses crossterm for cross-platform compatibility across Windows, Unix, and MacOS. The framework includes built-in components such as Aligned, Bordered, Spinner, and layout tools like DrawHorizontal and DrawVertical.

Tokens
10.8K
Snippets
43
Records
49
Agent score
68%

What's inside superconsole

  1. Overview of the superconsole framework

    main

    superconsole is a component-based framework for building Rust Text-based User Interfaces (TUIs). It provides a line-based abstraction for terminal rendering and includes a set of 'batteries' components for rapid development.

    Key features include:

    • Stylization: Supports italics, underlining, bolding, and coloring.
    • Cross-platform: Uses crossterm to ensure compatibility with Windows, Unix, and MacOS.
    • State/Rendering Separation: The framework delineates between rendering logic and program state. Each render call accepts an immutable reference to state, allowing components to inject state into their rendering logic without mutating it.
  2. Implement the Component trait

    main

    To create custom UI elements, you must implement the Component trait for your type. The trait requires defining an Error type and implementing the draw_unchecked method.

    draw_unchecked receives Dimensions and DrawMode and must return a Result<Lines, Self::Error>. The Lines type represents the lines of text to be rendered.

    use std::convert::Infallible;
    use superconsole::{Component, Dimensions, DrawMode, Lines};
    
    #[derive(Debug)]
    struct MyComponent;
    
    impl Component for MyComponent {
        type Error = Infallible;
    
        fn draw_unchecked(&self, _dimensions: Dimensions, _mode: DrawMode) -> Result<Lines, Infallible> {
            // Return a vector of lines
            Ok(Lines(vec![
                vec!["My content".to_owned()],
            ]))
        }
    }
  3. Render a component with superconsole

    main

    To display a component in the terminal, follow these steps:

    1. Initialize SuperConsole using SuperConsole::new(). This will fail if the output is not a TTY.
    2. Wrap your component (or use a built-in decorator like Bordered) to create the final component instance.
    3. Call superconsole.render(&component)? to perform the initial draw.
    4. Call superconsole.finalize(&component)? to complete the rendering process.
    use std::convert::Infallible;
    use superconsole::components::bordering::{Bordered, BorderedSpec};
    use superconsole::{Component, Dimensions, DrawMode, Lines, SuperConsole};
    
    #[derive(Debug)]
    struct HelloWorld;
    
    impl Component for HelloWorld {
        type Error = Infallible;
    
        fn draw_unchecked(&self, _dimensions: Dimensions, _mode: DrawMode) -> Result<Lines, Infallible> {
            Ok(Lines(vec![
                vec!["Hello world!".to_owned()],
            ]))
        }
    }
    
    pub fn main() -> anyhow::Result<()> {
        let bordering = BorderedSpec::default();
        let mut superconsole = SuperConsole::new().ok_or_else(|| anyhow::anyhow!("Not a TTY"))?;
        let component = Bordered::new(HelloWorld, bordering);
        
        superconsole.render(&component)?;
        superconsole.finalize(&component)?;
        
        Ok(())
    }
  4. How SuperConsole rendering works

    main

    SuperConsole is a TUI framework built on top of crossterm that renders to stdout. The rendering model is split into two distinct areas:

    1. The scratch area: This is where components live. The previous content in this area is overwritten at each render cycle.
    2. The emitted area: This area contains lines that scroll away above the scratch area, typically used for diagnostic output.

    Important Usage Notes:

    • Manual Re-rendering: The caller is responsible for triggering re-renders whenever the UI needs to update.
    • Output Interference: User input can cause aberrations in output. Additionally, you should avoid producing output from other sources (like direct println! calls) while SuperConsole is active to prevent UI corruption.
  5. Use OutputTarget to route output

    main

    SuperConsole supports multiple output streams via the OutputTarget enum. This allows you to separate the main TUI interface from auxiliary data or logs.

    • OutputTarget::Main: The primary output stream (typically stderr by default).
    • OutputTarget::Aux: An auxiliary output stream (typically stdout by default).

    You can use output_to to specify which stream a buffer should be written to.

    // Example of routing to different targets
    output.output_to(buffer, OutputTarget::Main)?;
    output.output_to(buffer, OutputTarget::Aux)?;
  6. Understand DrawMode

    main

    The DrawMode enum is used during the rendering process to signal the lifecycle state of a draw operation to the component.

    • DrawMode::Normal: The component is being drawn as part of a standard update cycle.
    • DrawMode::Final: This is the final time the component will be drawn for the current state.
    pub enum DrawMode {
        Normal,
        Final,
    }
  7. Initialize a new SuperConsole

    main

    To start using SuperConsole, you can use SuperConsole::new() which automatically checks for terminal compatibility (TTY support and ANSI support). If compatible, it initializes with a BlockingSuperConsoleOutput using stderr for the main TUI and stdout for auxiliary output.

    If you are in a testing environment or a non-standard terminal, use forced_new(fallback_size) to bypass compatibility checks by providing a specific Dimensions object.

    // Standard initialization
    let mut console = SuperConsole::new().expect("Failed to create console");
    
    // Forced initialization for testing or non-TTY environments
    let fallback = Dimensions::new(80, 24);
    let mut console = SuperConsole::forced_new(fallback);
  8. Configure borders with BorderedSpec

    main

    BorderedSpec defines the content for the left, right, top, and bottom boundaries of a Bordered component. Each field is an Option<Span>.

    Default Values

    If you use BorderedSpec::default(), the following defaults are applied:

    • left and right: '|'
    • top and bottom: '-'

    Customizing Borders

    You can specify custom Span objects for any side. If a side is set to None, that specific border will not be rendered.

    To use specific sides while keeping others at their defaults, use the struct update syntax.

    ```rust
    // Example: Custom top and bottom, no left/right borders
    let spec = BorderedSpec {
        top: Some("@@@".try_into().unwrap()),
        left: None,
        bottom: Some("@".try_into().unwrap()),
        ..Default::default()
    };
    
    let component = Bordered::new(child, spec);
    ```埋
  9. Handle invalid whitespace in Spans

    main

    Because superconsole expects mono-spaced content, Span validation fails if the input contains characters like \n or \t.

    If you encounter SpanError::InvalidWhitespace, you have three options:

    1. Validate manually: Use Span::valid(string) to check if a string is safe.
    2. Use Lossy methods: Use Span::new_unstyled_lossy(text) or Span::new_styled_lossy(content) to automatically strip invalid whitespace.
    3. Use Raw methods: Use Span::new_styled_raw(content) if you are using the span for special scenarios (like emit/emit_aux) where precise character alignment is not required and you need to bypass validation.
  10. Render a component using SuperConsole

    main

    To display a component in a terminal, follow these steps:

    1. Initialize a SuperConsole instance using SuperConsole::new(). Note that this will fail if the output is not a TTY.
    2. Wrap your component with decorators (like Bordered) if desired.
    3. Call superconsole.render(&component)? to perform the initial draw.
    4. Call superconsole.finalize(&component)? to clean up or finish the rendering cycle.
    use superconsole::{SuperConsole, Component};
    use superconsole::components::bordering::{Bordered, BorderedSpec};
    
    // ... component implementation ...
    
    pub fn main() -> anyhow::Result<()> {
        let bordering = BorderedSpec::default();
        let mut superconsole = SuperConsole::new().ok_or_else(|| anyhow::anyhow!("Not a TTY"))?;
        let component = Bordered::new(HelloWorld, bordering);
        
        superconsole.render(&component)?;
        superconsole.finalize(&component)?;
        
        Ok(())
    }
  11. Trim ends of a Line

    main

    The trim_ends(start: usize, width: usize) method allows you to slice a sub-section of a Line. It removes the first start characters and keeps up to width characters following that point. This operation respects grapheme boundaries.

    let mut line = Line::unstyled("hello cat world")?;
    
    // Skip 6 chars ('hello ') and take 3 chars ('cat')
    line.trim_ends(6, 3);
    
    assert_eq!(line.to_unstyled(), "cat");
  12. Manipulate Line width and padding

    main

    You can adjust the width and alignment of a Line using these methods:

    • pad_right(amount: usize): Adds the specified number of spaces to the end of the line.
    • pad_left(amount: usize): Adds the specified number of spaces to the beginning of the line.
    • truncate_line(max_width: usize): Truncates the line from the right until it is no longer than max_width. This may delete entire words or partially cut a word if it cannot fit.
    • to_exact_width(exact_width: usize): Ensures the line is exactly exact_width characters long by either padding the right side or truncating the right side.
    let mut line = Line::unstyled("hello")?;
    
    // Pad to 10 characters: "hello     "
    line.pad_right(5);
    
    // Ensure exact width
    line.to_exact_width(10);