Shopify CLI UI

repository·main·Indexed 22 days ago

https://github.com/shopify/cli-ui

A lightweight Ruby framework for building polished, interactive command-line user interfaces. It provides components for nested frames, spinners, progress bars, and interactive prompts, along with a custom formatting system for ANSI colors and glyphs. Features include multi-threaded process management via Spinner Groups, terminal link creation, and Sorbet compatibility.

Tokens
15.3K
Snippets
73
Records
86
Agent score
78%

What's inside cli-ui

  1. How nested framing works

    main

    CLI UI supports nested framing to manage content flow and visual hierarchy. By opening frames within other frames, you can create structured, indented UI elements.

    To use this feature, call CLI::UI::Frame.open inside an existing frame block. Ensure CLI::UI::StdoutRouter.enable is called before starting frames.

    CLI::UI::StdoutRouter.enable
    CLI::UI::Frame.open('Frame 1') do
      CLI::UI::Frame.open('Frame 2') { puts "inside frame 2" }
      puts "inside frame 1"
    end
  2. Sorbet compatibility and loading order

    main
    CLI UI uses Sorbet. If your project uses Sorbet, you must load Sorbet before loading cli-ui to ensure the provided stubs work correctly. If Sorbet is not present, the gem activates its own stubs automatically.
  3. Install CLI UI

    main

    You can install CLI UI via the command line or by adding it to your Gemfile.

    Via Command Line:

    gem install cli-ui

    Via Gemfile:

    gem 'cli-ui'

    After installation, include it in your code using require 'cli/ui'. Note that most features require CLI::UI::StdoutRouter.enable to be called first.

  4. Manage terminal progress bars with ProgressReporter

    main
    The CLI::UI::ProgressReporter module provides a way to manage terminal progress bars using ConEmu OSC 9;4 escape sequences. It supports numerical progress (0-100%), indeterminate/pulsing states, error states, and paused states. It is designed to handle nested progress reporting, where child reporters are registered with a parent.
  5. How nested framing works in CLI::UI::Frame

    main

    The CLI::UI::Frame module allows you to create visual boundaries in the terminal to organize content. Frames can be nested, creating a hierarchical visual structure where each nested frame inherits or overrides the styling of its parent.

    There are two ways to use frames:

    1. Block Form (Recommended): You pass a block to CLI::UI::Frame.open. The frame automatically closes when the block finishes. The return value of the block determines if the frame is marked as a success or failure. If the block raises an error, the frame is automatically closed with a failure state.
    2. Blockless Form: You call CLI::UI::Frame.open without a block. In this mode, you must manually call CLI::UI::Frame.close to terminate the frame. This mode is strongly discouraged.

    When nesting frames, CLI::UI::Frame.divider can be used to add horizontal separators within a frame, and it correctly respects the nesting level to maintain visual alignment.

    # Block form with automatic success/failure handling
    CLI::UI::Frame.open('Task Name', success_text: 'Done!', failure_text: 'Failed') do
      # Perform work here
      puts 'Working...'
    end
    
    # Nested framing
    CLI::UI::Frame.open('Parent') do
      puts 'Parent content'
      CLI::UI::Frame.open('Child') do
        puts 'Child content'
        CLI::UI::Frame.divider('Separator')
      end
    end
  6. Implement a custom UI widget by subclassing Base

    main

    To create a custom UI widget in cli-ui, you must subclass CLI::UI::Widgets::Base. Your implementation must define two specific methods:

    1. self.argparse_pattern: Returns a Regexp used to parse the string argument passed to the widget. This pattern is used during initialization to extract named capture groups into instance variables.
    2. render: Returns a String representing the visual output of the widget.

    When you call WidgetClass.call("argument"), the base class instantiates your widget, parses the argument using your pattern, and calls render. If the argument does not match your pattern, it raises a CLI::UI::Widgets::InvalidWidgetArguments error.

    module CLI
      module UI
        module Widgets
          class MyCustomWidget < Base
            def self.argparse_pattern
              /my_pattern/(?<name>\w+)/
            end
    
            def render
              "Rendered: #{@name}"
            end
          end
        end
      end
    end
    
    # Usage:
    puts CLI::UI::Widgets::MyCustomWidget.call("some_value")
  7. Suppress output using an output hook

    main
    The StdoutRouter mechanism supports a global output hook via Thread.current[:cliui_output_hook]. If this hook returns false, the output is suppressed. This is an internal mechanism used by Capture to prevent output from hitting the terminal while it is being recorded.
  8. Manage multiple concurrent spinners with SpinGroup

    main

    For complex CLI interactions involving multiple background tasks, use CLI::UI::Spinner::SpinGroup. A SpinGroup allows you to manage a collection of tasks that run concurrently, providing a single coordinated interface for rendering spinners and waiting for all tasks to finish.

    Note that SpinGroup manages its own global state during the #wait lifecycle. It is recommended to avoid running multiple concurrent SpinGroup instances to prevent rendering conflicts.

  9. Implement a custom FrameStyle

    main

    To create a custom frame style, you must implement the CLI::UI::Frame::FrameStyle interface. A valid implementation must provide the following methods:

    • style_name: Returns a Symbol representing the name of the style.
    • prefix: Returns a String containing the character(s) to be printed at the beginning of every line within the frame.
    • start(text, color:): Returns a String representing the 'Open' line of the frame. Requires a text string and a color (of type CLI::UI::Color).
    • close(text, color:, right_text: nil): Returns a String representing the 'Close' line. Requires text and color. Optionally accepts right_text (a String) to be printed at the right side of the line.
    • divider(text, color:): Returns a String representing a divider line within the frame. Requires text and color.
  10. Configure Frame Styles

    main

    You can modify the appearance of frames globally or on an individual basis. Supported styles include :box (default) and :bracket.

    Set global default style:

    CLI::UI.frame_style = :box

    Set style for an individual frame:

    CLI::UI.frame('New Style!', frame_style: :bracket) { puts "It's pretty cool!" }
  11. How InteractiveOptions works

    main

    The InteractiveOptions prompt provides a highly interactive terminal interface for selecting one or more items from a list.

    Interaction Modes

    • Root Mode: The default state where users navigate the list and select an item.
    • Filter Mode: Triggered by f or /. Users type to filter the visible list. Pressing Ctrl-D or Backspace on an empty filter returns to Root mode.
    • Line Select Mode: Triggered by e, :, or G (for lists > 9 items). Allows users to type specific line numbers to select items quickly.
    • Esc Mode: Handles escape sequences for advanced navigation (e.g., using Page Up/Down via arrow key sequences).

    Selection Logic

    • Single Selection: The call method returns the string value of the selected option.
    • Multiple Selection: When multiple: true is enabled, the prompt displays checkboxes (/). Users toggle items and must select the Done option to return the array of selected strings.
  12. Extend CLI::UI with custom widgets

    main

    You can extend CLI::UI by creating a new widget class that inherits from CLI::UI::Widgets::Base and registering it with the CLI::UI::Widgets.register method. Once registered, your widget can be invoked using the CLI::UI.fmt method with the syntax {{@widget/your-widget-name:args}}.

    require('cli/ui')
    
    class MyWidget < CLI::UI::Widgets::Base
      # Implement widget logic here
    end
    
    CLI::UI::Widgets.register('my-widget') { MyWidget }
    
    # Usage via formatting engine
    puts(CLI::UI.fmt("{{@widget/my-widget:args}}"))