cli-ui: the small Ruby framework for the parts of a terminal program that are awkward
CLI tooling framework with simple interactive widgets
At a glance
- What is it?
- Shopify's cli-ui is a small, dependency-free Ruby library for the parts of a command line interface that are tedious to get right, nested frames, interactive prompts, spinners over multiple threads, progress bars and coloured text. Its release history stops in 2018 while the repository keeps receiving commits, and that gap is the one thing to check before depending on it.
- Who is it for?
- cli-ui fits a Ruby team writing a command line tool that needs to ask questions, show progress over several processes, or print structured output without hand-writing escape sequences. It does not fit a tool that needs a rich terminal UI with continuous redraw, and it does not fit a team that wants its dependency surface governed by a lockfile policy, since the gem ships no runtime dependencies at all.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 37 days ago.
- What is it written in?
- Mainly Ruby, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A framework for the parts of a CLI that nobody enjoys writing
CLI UI is described as a small framework for generating nice command-line user interfaces, and the repository description calls it a CLI tooling framework with simple interactive widgets.
The problem it solves is specific. A terminal program that wants to ask a question, show a spinner while three things run in parallel, print a progress bar, and indent a block of output has to deal with cursor movement, colour codes, and the fact that a spinner is writing to the same stream as the output it is spinning over. Most projects solve that with a mixture of puts, escape sequences, and a library that somebody chose badly.
The audience is therefore a Ruby developer building a developer tool, a generator, a setup script or a release command, where the output is part of the product and a stack trace of raw output is a bad first impression.
The library is deliberately small in scope. The README says the feature list may not be exhaustive and points to the documentation, which is published as Rubydoc for both the master branch and the released gem version. The features that are documented are the ones a CLI needs constantly: framing, prompts, spinners, progress, and formatted text.
The installation is a gem, and the first line of code is a router enable. Most options assume that CLI::UI::StdoutRouter.enable has been called, and in your code you simply add a require 'cli/ui'.
Frames nest, and a router owns the output stream
The core mechanism is a frame, and frames nest.
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"
endEach frame draws a border and indents the content inside it, and the nesting is what makes the output readable, because the indentation is derived from the enclosing frames rather than from the string you happened to write. The README calls this nested framing, and describes the purpose as handling content flow.
The router is the part that is easy to miss and hard to live without. StdoutRouter.enable takes over the output stream, and that is what lets the library know where the cursor is and what to redraw. A spinner and a frame are both stateful views of a terminal, and a library that writes directly to stdout with puts cannot coordinate with them.
The other half of the mechanism is the spinner group, which is where the multi-threading shows up. A SpinGroup handles many multi-threaded processes while suppressing output unless there is an issue, and it can update the title to show state:
CLI::UI::SpinGroup.new do |spin_group|
spin_group.add('Title') { |spinner| sleep 3.0 }
spin_group.add('Title 2') { |spinner| sleep 3.0; spinner.update_title('New Title'); sleep 3.0 }
endEach task gets its own spinner, the group's title is the shared line, and a task that finishes without error disappears from the display rather than printing a line into the middle of the others.
Prompts that accept a keyboard, vim keys or a number
The interactive prompt is where a CLI earns its keep, and cli-ui's version handles several input styles at once.
The basic call asks a question with a list of options, and the user can answer using arrow keys, vim bindings with j and k, or numbers, and a yes or no question is answered with y and n. The example in the README is a single line:
CLI::UI.ask('What language/framework do you use?', options: %w(rails go ruby python))For long option lists the README describes two extra modes. Using e, : or G toggles a line select mode, which allows numbers greater than 9 to be typed, and f or / lets the user filter options using a free-form text input. Those two modes are what stop a prompt with thirty options from being unusable.
There is a second form of the same prompt, where each option carries its own callback, and the README notes that the two examples provided are identical in functionality.
CLI::UI::Prompt.ask('What language/framework do you use?') do |handler|
handler.option('rails') { |selection| selection }
handler.option('go') { |selection| selection }
handler.option('ruby') { |selection| selection }
handler.option('python') { |selection| selection }
endFree form text is the other half of the prompting story, and it takes a default value:
CLI::UI.ask('Is CLI UI Awesome?', default: 'It is great!')The instruction text colour is configurable through CLI::UI::Prompt.instructions_color, so a prompt can be tuned to match the rest of a tool's output rather than fighting it.
A tiny markup language for colour, glyphs and status
Formatted output uses a compact markup that is worth understanding, because it appears in every string the library prints.
Colour is written as a name and a value in braces, so {{red:Red}} and {{green:Green}} are the examples in the README, and the formatted result is produced by CLI::UI.fmt. Symbols and glyphs use the same braces, and {{*}} becomes a yellow star, with {{v}}, {{?}} and {{x}} given as the other examples.
puts CLI::UI.fmt "{{red:Red}} {{green:Green}}"
puts CLI::UI.fmt "{{*}} {{v}} {{?}} {{x}}"The status widget is a third form, a {{@widget/status:1:2:3:4}} placeholder that is replaced by a live status display as the work behind it progresses. The example shows it inside a spinner:
CLI::UI::Spinner.spin("building packages: {{@widget/status:1:2:3:4}}") do |spinner|
# spinner.update_title(...)
sleep(3)
endThe progress bar is the plainest of the set, a block that yields a bar you tick:
CLI::UI::Progress.progress do |bar|
100.times do
bar.tick
end
endThe frames themselves are styleable, globally with CLI::UI.frame_style = :box, which is the default, or per frame with the frame_style option, where the other documented style is :bracket. The README shows the bracket style rendering as a lighter frame around the same content.
The reason this all works together is the router from the previous section. Colour, glyphs, frames, spinners and progress all assume they own the terminal.
No runtime dependencies, and Sorbet that stays out of the way
The development section of the README has one rule that defines the library's character: no code, outside of development and tests needs, should use dependencies. This is a self contained library.
That constraint has consequences. A terminal library that pulls in a colour gem, a prompt library and a progress bar library inherits three sets of opinions about the terminal, and they rarely agree. cli-ui decides to own that layer itself, which is why the repository can promise that the only thing your application needs from the gem is cli-ui itself.
Type checking is handled with Sorbet, and the arrangement here is unusually careful. The project makes use of Sorbet in cli-ui and provides stubs for Sorbet so that you can use the gem even if you are not using Sorbet. Those stubs are activated if T is undefined when the gem is loaded, and the README draws the consequence: if your project does use Sorbet, ensure you load Sorbet before loading cli-ui.
That ordering requirement is the one piece of friction a consumer has to manage, and it is a direct consequence of the stubbing approach rather than a separate design decision.
The repository reflects the same shape. There is a lib directory, a sorbet directory with the type definitions, a test directory, a vendor directory, a bin directory, the cli-ui.gemspec, a Gemfile and Rakefile, a .ruby-version file, and two rubocop configurations, one for the general code and one for the Sorbet parts. Tests are run with bundle exec rake test, and the README states that all code should be tested.
Tagged releases stop in 2018, the branch does not
The release history is the thing to look at before you adopt this, and it needs a sentence of explanation.
The listed releases are v1.1.2 and v1.1.3, both from 2018-05-15, described as bugs and styles and as ID'd commands, and v1.2.1 from 2018-11-06, described as TTY and ANSI fixes plus a multiple selection prompt. The last push to the repository was on 2026-09-15.
So the last tagged release is nearly eight years old, and the branch has been receiving commits well after that. The README links the documentation for the master branch separately from the documentation of the Rubygems version, which is the tell: there are two states of this library, and they are not the same.
That distinction decides which one you get. A gem installed with gem install cli-ui, or with gem 'cli-ui' in a Gemfile and no version constraint, resolves to the latest published version on the index, which is the 2018 tag. Someone building from the repository gets the newer code, and a project that depends on the published gem and someone else who clones the repository are not running the same library.
The MIT licence means there is nothing legally preventing a fork or a vendored copy, so the practical answer for a team that needs the current behaviour is to decide explicitly which state you are depending on, and to record that decision, rather than discovering it later when a bug report does not reproduce.
Where it is the wrong tool
cli-ui is a set of widgets, and the limits of that shape are worth being explicit about.
It is not a full screen terminal application framework. There is no layout engine, no component tree, no event loop of its own, and nothing in the documented feature set redraws a full screen on a schedule the way a curses interface does. The spinners and the progress bar are line-oriented views over a stream, and they are good at that. A tool that wants a live table, a scrolling log panel or a split pane with a mouse cursor in it is asking for something this library does not model.
It is also not a replacement for a real prompt library, and the prompts are the most opinionated part of the surface. The arrow key, vim key and number input handling is the documented behaviour, and there is no option in the README for a different key map or a non-interactive mode, so a script that must run unattended has to bypass these calls rather than configure them.
The third limit is the one the dependency rule creates. Because the library owns stdout once the router is enabled, any code in the same process that writes directly to the output stream can interleave badly with a spinner or a frame. The router is the answer to that, and enabling it is the first thing the README tells you to do, but it also means the library has to be in charge of the process's output for the duration of those widgets.
The alternative approach is to write the escape sequences yourself, or to adopt a dependency that brings its own opinions along with it. Hand-rolling gives complete control and no dependency, at the price of owning cursor state, which is exactly the work cli-ui exists to take off your hands.
Editorial conclusion
cli-ui fits a Ruby team writing a command line tool that needs to ask questions, show progress over several processes, or print structured output without hand-writing escape sequences. It does not fit a tool that needs a rich terminal UI with continuous redraw, and it does not fit a team that wants its dependency surface governed by a lockfile policy, since the gem ships no runtime dependencies at all. Before you adopt it, read the release history rather than the commit history, because the tagged releases end in 2018 while the branch has moved on since, and check how your project loads Sorbet relative to the gem.
Frequently asked questions
How do I install and start using cli-ui?
Run gem install cli-ui, or add gem 'cli-ui' to your Gemfile, and require 'cli/ui' in your code. Most options assume that CLI::UI::StdoutRouter.enable has been called, so that is the first call to make.
What can the cli-ui interactive prompt do?
It prompts the user with options and accepts arrow keys, vim bindings with j and k, or numbers, and y or n for yes and no. For large option lists, e, : or G toggles a line select mode that allows numbers greater than 9, and f or / filters the options with a free-form text input.
Does cli-ui have any runtime dependencies?
No. The README states that no code, outside of development and tests needs, should use dependencies, and that this is a self contained library. The project uses Sorbet for types and provides stubs so the gem can be used without Sorbet, activating them if T is undefined when the gem loads.
How do I run the cli-ui tests?
Run them with bundle exec rake test, and the README states that all code should be tested. The repository also carries two rubocop configurations, one general and one for the Sorbet parts, plus a .ruby-version file.
How often is cli-ui released?
The listed releases are v1.1.2 and v1.1.3 from 2018-05-15 and v1.2.1 from 2018-11-06, while the last push to the repository was on 2026-09-15. The README links separate documentation for the master branch and for the released Rubygems version, so the published gem and the repository are two different states of the library.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/shopify-cli-ui)Was this useful?