tty-menu-tree 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 401abaaf372d1d2c9cf1302166e8f10b59806e6fc5e12107119c9ad242b0ce38
4
+ data.tar.gz: b5dd398022905ea05cb357b4e92fd6894f3ce3669a2737a720c9f3cd00e68f4c
5
+ SHA512:
6
+ metadata.gz: d76b274a7e3f4ad52c9cb3835712315bac5bf2ebdf3c65ba45b7f5d025445b443ac989cc9faff734d09e62889feb719f4b679cfede4fbc8bf13541868bcb8971
7
+ data.tar.gz: 617e6ece447e68f1b7096c5f2d599e762fa2f2de25a4a7b12afa5ca261fb6c496a2ba981871bd1444b5c5d009c6e636f73bac713bfb7aaa21562e760a3fdb329
data/CHANGELOG.md ADDED
@@ -0,0 +1,25 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0]
11
+
12
+ ### Added
13
+
14
+ - `TTY::MenuTree::Menu` — runs a tree of actions as an interactive menu with
15
+ submenu descent, back navigation, and an exit action.
16
+ - `TTY::MenuTree::Action` — base class defining the `name`/`call`/`submenu`/`exit?`
17
+ protocol.
18
+ - `TTY::MenuTree::Group` — an action that opens a submenu of child actions.
19
+ - `TTY::MenuTree::ExitAction` — a ready-made action that ends the loop.
20
+ - `TTY::MenuTree::Prompt` — a `tty-prompt` wrapper providing `select_action`
21
+ with optional "Back" handling.
22
+ - `on_start` / `on_exit` hooks so banners and goodbyes stay out of the library.
23
+
24
+ [Unreleased]: https://github.com/xef5000/tty-menu-tree/compare/v0.1.0...HEAD
25
+ [0.1.0]: https://github.com/xef5000/tty-menu-tree/releases/tag/v0.1.0
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Romain Deshaies
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,110 @@
1
+ # TTY::MenuTree
2
+
3
+ Interactive, hierarchical command-line menus built on
4
+ [tty-prompt](https://github.com/piotrmurach/tty-prompt).
5
+
6
+ You model each menu entry as a small **action** object. `TTY::MenuTree::Menu`
7
+ runs them in a loop: leaf actions are invoked, groups open a submenu (with
8
+ automatic "Back" navigation), and an exit action ends the loop. Banners and
9
+ goodbyes are injectable hooks, so the menu itself stays free of
10
+ application-specific content.
11
+
12
+ ## Installation
13
+
14
+ Add it to your Gemfile:
15
+
16
+ ```ruby
17
+ gem "tty-menu-tree"
18
+ ```
19
+
20
+ Then run:
21
+
22
+ ```sh
23
+ bundle install
24
+ ```
25
+
26
+ Or install it directly:
27
+
28
+ ```sh
29
+ gem install tty-menu-tree
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ An action is any object that responds to `name`, `call`, `submenu`, and
35
+ `exit?`. Subclass `TTY::MenuTree::Action` to get sensible defaults.
36
+
37
+ ```ruby
38
+ require "tty/menu_tree"
39
+
40
+ class SayHello < TTY::MenuTree::Action
41
+ def name = "Say hello"
42
+ def call = puts("Hello! 👋")
43
+ end
44
+
45
+ class ShowTime < TTY::MenuTree::Action
46
+ def name = "Show the time"
47
+ def call = puts(Time.now)
48
+ end
49
+
50
+ actions = [
51
+ TTY::MenuTree::Group.new(name: "Greetings", actions: [SayHello.new]),
52
+ ShowTime.new,
53
+ TTY::MenuTree::ExitAction.new,
54
+ ]
55
+
56
+ TTY::MenuTree::Menu.new(
57
+ actions: actions,
58
+ title: "What would you like to do?",
59
+ on_start: -> { puts "Welcome!" },
60
+ on_exit: -> { puts "Goodbye!" },
61
+ ).run
62
+ ```
63
+
64
+ ### The action protocol
65
+
66
+ | Method | Returns | Meaning |
67
+ | ---------- | -------------------- | -------------------------------------------------- |
68
+ | `name` | `String` | Label shown in the menu. |
69
+ | `call` | anything | Runs a leaf action (skipped when `submenu` is set).|
70
+ | `submenu` | `Array<#name>`/`nil` | Child actions to descend into, or `nil` for leaves.|
71
+ | `exit?` | `Boolean` | When `true`, ends the top-level loop. |
72
+
73
+ You do not have to subclass anything — any duck-typed object works. The
74
+ provided building blocks are just conveniences:
75
+
76
+ - `TTY::MenuTree::Action` — base class with defaults and `NotImplementedError`
77
+ guards.
78
+ - `TTY::MenuTree::Group` — an action whose `submenu` is its child actions.
79
+ - `TTY::MenuTree::ExitAction` — an action whose `exit?` is `true`.
80
+
81
+ ### Customizing the prompt
82
+
83
+ `Menu` only needs an object responding to
84
+ `select_action(title, actions, back:)`. The default `TTY::MenuTree::Prompt`
85
+ wraps `tty-prompt` and appends a "Back" entry (returning `nil`) when
86
+ `back: true`. Pass your own prompt to fully control rendering:
87
+
88
+ ```ruby
89
+ TTY::MenuTree::Menu.new(actions: actions, prompt: MyPrompt.new).run
90
+ ```
91
+
92
+ You can also customize the default prompt's colors or Back label:
93
+
94
+ ```ruby
95
+ prompt = TTY::MenuTree::Prompt.new(
96
+ tty_prompt: TTY::Prompt.new(active_color: :magenta),
97
+ back_label: "⬅ Back",
98
+ )
99
+ ```
100
+
101
+ ## Development
102
+
103
+ ```sh
104
+ bin/setup # bundle install
105
+ bundle exec rspec
106
+ ```
107
+
108
+ ## License
109
+
110
+ Released under the [MIT License](LICENSE.txt).
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TTY
4
+ module MenuTree
5
+ # Base class for anything that can appear in a menu.
6
+ #
7
+ # An action is any object that responds to the four methods below. You do
8
+ # not have to subclass +Action+; duck typing is enough. Subclassing simply
9
+ # gives you sensible defaults ({#submenu} => +nil+, {#exit?} => +false+) and
10
+ # +NotImplementedError+ guards for the parts you must define.
11
+ #
12
+ # @example A leaf action
13
+ # class SayHello < TTY::MenuTree::Action
14
+ # def name = "Say hello"
15
+ # def call = puts("Hello!")
16
+ # end
17
+ class Action
18
+ # @return [String] the label shown for this action in a menu.
19
+ def name
20
+ raise NotImplementedError, "#{self.class}#name must be implemented"
21
+ end
22
+
23
+ # Invoked when a leaf action is selected.
24
+ #
25
+ # Not called for actions that return a {#submenu}; those are descended
26
+ # into instead.
27
+ #
28
+ # @return [void]
29
+ def call
30
+ raise NotImplementedError, "#{self.class}#call must be implemented"
31
+ end
32
+
33
+ # @return [Array<#name>, nil] child actions to descend into, or +nil+ for
34
+ # a leaf action.
35
+ def submenu
36
+ nil
37
+ end
38
+
39
+ # @return [Boolean] when +true+, selecting this action ends the top-level
40
+ # menu loop.
41
+ def exit?
42
+ false
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "action"
4
+
5
+ module TTY
6
+ module MenuTree
7
+ # A ready-made action that ends the top-level menu loop when selected.
8
+ #
9
+ # @example Custom label
10
+ # TTY::MenuTree::ExitAction.new(name: "Quit")
11
+ class ExitAction < Action
12
+ DEFAULT_NAME = "Exit"
13
+
14
+ # @return [String] the label shown for this action.
15
+ attr_reader :name
16
+
17
+ # @param name [String] label shown in the menu.
18
+ def initialize(name: DEFAULT_NAME)
19
+ super()
20
+ @name = name
21
+ end
22
+
23
+ # No-op; selecting this action ends the loop rather than doing work.
24
+ # @return [void]
25
+ def call; end
26
+
27
+ # @return [true]
28
+ def exit?
29
+ true
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "action"
4
+
5
+ module TTY
6
+ module MenuTree
7
+ # An action that, when selected, opens a submenu of child actions rather
8
+ # than performing work itself.
9
+ #
10
+ # @example
11
+ # TTY::MenuTree::Group.new(name: "Settings", actions: [ChangeTheme.new, Reset.new])
12
+ class Group < Action
13
+ # @return [String] the label shown for this group.
14
+ attr_reader :name
15
+
16
+ # @return [Array<#name>] the child actions.
17
+ attr_reader :submenu
18
+
19
+ # @param name [String] label shown in the parent menu.
20
+ # @param actions [Array<#name>] child actions to show when this group is
21
+ # selected.
22
+ def initialize(name:, actions:)
23
+ super()
24
+ @name = name
25
+ @submenu = actions
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "prompt"
4
+
5
+ module TTY
6
+ module MenuTree
7
+ # Runs a tree of actions as an interactive, hierarchical menu.
8
+ #
9
+ # The loop repeatedly asks the user to pick an action. Leaf actions are
10
+ # invoked via +#call+; actions that expose a +#submenu+ are descended into
11
+ # (with a "Back" entry); and an action whose +#exit?+ returns +true+ ends
12
+ # the loop.
13
+ #
14
+ # @example
15
+ # menu = TTY::MenuTree::Menu.new(
16
+ # actions: [group, TTY::MenuTree::ExitAction.new],
17
+ # title: "What would you like to do?",
18
+ # on_start: -> { puts "Welcome!" },
19
+ # on_exit: -> { puts "Goodbye!" },
20
+ # )
21
+ # menu.run
22
+ class Menu
23
+ # @param actions [Array<#name>] top-level actions.
24
+ # @param prompt [#select_action] object used to render menus. Defaults to
25
+ # a {Prompt}.
26
+ # @param title [String] heading shown for the top-level menu each loop.
27
+ # @param on_start [#call, nil] callable run once before the loop starts.
28
+ # @param on_exit [#call, nil] callable run once after an exit action is
29
+ # chosen.
30
+ def initialize(actions:, prompt: Prompt.new, title: "What would you like to do?",
31
+ on_start: nil, on_exit: nil)
32
+ @actions = actions
33
+ @prompt = prompt
34
+ @title = title
35
+ @on_start = on_start
36
+ @on_exit = on_exit
37
+ end
38
+
39
+ # Run the interactive loop until an exit action is chosen.
40
+ # @return [void]
41
+ def run
42
+ @on_start&.call
43
+
44
+ loop do
45
+ action = @prompt.select_action(@title, @actions)
46
+
47
+ if action.exit?
48
+ @on_exit&.call
49
+ break
50
+ end
51
+
52
+ run_action(action)
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def run_action(action)
59
+ return action.call unless action.submenu
60
+
61
+ loop do
62
+ child = @prompt.select_action(action.name, action.submenu, back: true)
63
+ return if child.nil?
64
+
65
+ run_action(child)
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tty-prompt"
4
+
5
+ module TTY
6
+ module MenuTree
7
+ # Thin wrapper around +TTY::Prompt+ that knows how to render a list of
8
+ # actions and, optionally, a "Back" entry.
9
+ #
10
+ # {Menu} only depends on the {#select_action} method, so you may pass any
11
+ # object that implements it if you want fully custom rendering.
12
+ class Prompt
13
+ DEFAULT_BACK_LABEL = "\u21A9\uFE0F Back"
14
+
15
+ # @param tty_prompt [TTY::Prompt] the underlying prompt to drive.
16
+ # @param back_label [String] label used for the "Back" entry.
17
+ def initialize(tty_prompt: TTY::Prompt.new(active_color: :cyan, help_color: :bright_black),
18
+ back_label: DEFAULT_BACK_LABEL)
19
+ @tty_prompt = tty_prompt
20
+ @back_label = back_label
21
+ end
22
+
23
+ # Present +actions+ and return the one the user picked.
24
+ #
25
+ # @param title [String] heading shown above the choices.
26
+ # @param actions [Array<#name>] selectable actions.
27
+ # @param back [Boolean] when +true+, append a "Back" entry that returns
28
+ # +nil+ when chosen.
29
+ # @return [Object, nil] the selected action, or +nil+ when "Back" was
30
+ # chosen.
31
+ def select_action(title, actions, back: false)
32
+ choices = actions.to_h { |action| [action.name, action] }
33
+ choices[@back_label] = nil if back
34
+ @tty_prompt.select("\n#{title}", choices, per_page: choices.size)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TTY
4
+ module MenuTree
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "menu_tree/version"
4
+ require_relative "menu_tree/action"
5
+ require_relative "menu_tree/group"
6
+ require_relative "menu_tree/exit_action"
7
+ require_relative "menu_tree/prompt"
8
+ require_relative "menu_tree/menu"
9
+
10
+ module TTY
11
+ # A small toolkit for building interactive, hierarchical command-line menus
12
+ # on top of +tty-prompt+.
13
+ #
14
+ # See {TTY::MenuTree::Menu} for the entry point.
15
+ module MenuTree
16
+ end
17
+ end
metadata ADDED
@@ -0,0 +1,85 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tty-menu-tree
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Romain Deshaies
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: tty-prompt
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.23'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.23'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rspec
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '3.0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '3.0'
40
+ description: |-
41
+ TTY::MenuTree runs a tree of polymorphic "action" objects as an interactive
42
+ command-line menu, with support for nested submenus, back navigation, and an
43
+ exit action. Content such as banners and goodbyes are injectable hooks so the
44
+ menu itself stays free of application-specific concerns.
45
+ email:
46
+ - romain.deshaies@shopify.com
47
+ executables: []
48
+ extensions: []
49
+ extra_rdoc_files: []
50
+ files:
51
+ - CHANGELOG.md
52
+ - LICENSE.txt
53
+ - README.md
54
+ - lib/tty/menu_tree.rb
55
+ - lib/tty/menu_tree/action.rb
56
+ - lib/tty/menu_tree/exit_action.rb
57
+ - lib/tty/menu_tree/group.rb
58
+ - lib/tty/menu_tree/menu.rb
59
+ - lib/tty/menu_tree/prompt.rb
60
+ - lib/tty/menu_tree/version.rb
61
+ homepage: https://github.com/xef5000/tty-menu-tree
62
+ licenses:
63
+ - MIT
64
+ metadata:
65
+ source_code_uri: https://github.com/xef5000/tty-menu-tree
66
+ changelog_uri: https://github.com/xef5000/tty-menu-tree/blob/main/CHANGELOG.md
67
+ rubygems_mfa_required: 'true'
68
+ rdoc_options: []
69
+ require_paths:
70
+ - lib
71
+ required_ruby_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '3.0'
76
+ required_rubygems_version: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ requirements: []
82
+ rubygems_version: 4.0.16
83
+ specification_version: 4
84
+ summary: Interactive, hierarchical command-line menus built on tty-prompt.
85
+ test_files: []