0.0
The project is in a healthy, maintained state
TTY::MenuTree runs a tree of polymorphic "action" objects as an interactive command-line menu, with support for nested submenus, back navigation, and an exit action. Content such as banners and goodbyes are injectable hooks so the menu itself stays free of application-specific concerns.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

~> 3.0

Runtime

~> 0.23
 Project Readme

TTY::MenuTree

CI Gem Version

Screen.Recording.2026-09-14.at.4.47.10.PM.mov

Interactive, hierarchical command-line menus built on tty-prompt.

You model each menu entry as a small action object. TTY::MenuTree::Menu runs them in a loop: leaf actions are invoked, groups open a submenu (with automatic "Back" navigation), and an exit action ends the loop. Banners and goodbyes are injectable hooks, so the menu itself stays free of application-specific content.

Architecture

The library is a small skeleton: a Menu runner, a Prompt for rendering, and a family of Action objects that describe what appears in the menu. Everything application-specific (what each action does, how it fetches data, how it formats output) lives in your own action subclasses.

classDiagram
    class Menu {
      +run()
      -run_action(action)
    }
    class Prompt {
      +select_action(title, actions, back)
    }
    class Action {
      +name() String
      +call()
      +submenu() Array|nil
      +exit?() Boolean
    }
    class Group {
      +name
      +submenu
    }
    class ExitAction {
      +exit?() true
    }

    Menu --> Prompt : renders via
    Menu --> Action : runs a tree of
    Action <|-- Group
    Action <|-- ExitAction
    Group o-- Action : contains children
    note for Action "Duck typing is enough:\nany object with name/call/submenu/exit?"
Loading

At runtime, Menu#run loops over the top-level actions. Leaf actions are invoked; groups are descended into (with a "Back" entry that returns to the parent); and an exit action ends the loop.

flowchart TD
    start([run]) --> on_start["on_start hook (optional)"]
    on_start --> select["prompt.select_action(title, actions)"]
    select --> is_exit{"action.exit?"}
    is_exit -- yes --> on_exit["on_exit hook (optional)"] --> stop([end loop])
    is_exit -- no --> has_sub{"action.submenu?"}
    has_sub -- no --> call["action.call"] --> select
    has_sub -- yes --> sub["select_action(name, submenu, back: true)"]
    sub --> back{"chose Back?"}
    back -- yes --> select
    back -- no --> recurse["run_action(child)"]
    recurse --> sub
Loading

Installation

Add it to your Gemfile:

gem "tty-menu-tree"

Then run:

bundle install

Or install it directly:

gem install tty-menu-tree

Usage

An action is any object that responds to name, call, submenu, and exit?. Subclass TTY::MenuTree::Action to get sensible defaults.

require "tty/menu_tree"

class SayHello < TTY::MenuTree::Action
  def name = "Say hello"
  def call = puts("Hello! 👋")
end

class ShowTime < TTY::MenuTree::Action
  def name = "Show the time"
  def call = puts(Time.now)
end

actions = [
  TTY::MenuTree::Group.new(name: "Greetings", actions: [SayHello.new]),
  ShowTime.new,
  TTY::MenuTree::ExitAction.new,
]

TTY::MenuTree::Menu.new(
  actions: actions,
  title: "What would you like to do?",
  on_start: -> { puts "Welcome!" },
  on_exit: -> { puts "Goodbye!" },
).run

The action protocol

Method Returns Meaning
name String Label shown in the menu.
call anything Runs a leaf action (skipped when submenu is set).
submenu Array<#name>/nil Child actions to descend into, or nil for leaves.
exit? Boolean When true, ends the top-level loop.

You do not have to subclass anything — any duck-typed object works. The provided building blocks are just conveniences:

  • TTY::MenuTree::Action — base class with defaults and NotImplementedError guards.
  • TTY::MenuTree::Group — an action whose submenu is its child actions.
  • TTY::MenuTree::ExitAction — an action whose exit? is true.

Customizing the prompt

Menu only needs an object responding to select_action(title, actions, back:). The default TTY::MenuTree::Prompt wraps tty-prompt and appends a "Back" entry (returning nil) when back: true. Pass your own prompt to fully control rendering:

TTY::MenuTree::Menu.new(actions: actions, prompt: MyPrompt.new).run

You can also customize the default prompt's colors or Back label:

prompt = TTY::MenuTree::Prompt.new(
  tty_prompt: TTY::Prompt.new(active_color: :magenta),
  back_label: "⬅ Back",
)

Development

bin/setup       # bundle install
bundle exec rspec

License

Released under the MIT License.