TTY::MenuTree
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?"
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
Installation
Add it to your Gemfile:
gem "tty-menu-tree"Then run:
bundle installOr install it directly:
gem install tty-menu-treeUsage
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!" },
).runThe 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 andNotImplementedErrorguards. -
TTY::MenuTree::Group— an action whosesubmenuis its child actions. -
TTY::MenuTree::ExitAction— an action whoseexit?istrue.
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).runYou 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 rspecLicense
Released under the MIT License.