tty2-prompt 0.23.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +14 -0
  3. data/LICENSE.txt +23 -0
  4. data/README.md +52 -0
  5. data/lib/tty2/prompt/answers_collector.rb +78 -0
  6. data/lib/tty2/prompt/block_paginator.rb +59 -0
  7. data/lib/tty2/prompt/choice.rb +147 -0
  8. data/lib/tty2/prompt/choices.rb +129 -0
  9. data/lib/tty2/prompt/confirm_question.rb +158 -0
  10. data/lib/tty2/prompt/const.rb +17 -0
  11. data/lib/tty2/prompt/converter_dsl.rb +21 -0
  12. data/lib/tty2/prompt/converter_registry.rb +69 -0
  13. data/lib/tty2/prompt/converters.rb +182 -0
  14. data/lib/tty2/prompt/distance.rb +49 -0
  15. data/lib/tty2/prompt/enum_list.rb +433 -0
  16. data/lib/tty2/prompt/errors.rb +31 -0
  17. data/lib/tty2/prompt/evaluator.rb +29 -0
  18. data/lib/tty2/prompt/expander.rb +321 -0
  19. data/lib/tty2/prompt/keypress.rb +98 -0
  20. data/lib/tty2/prompt/list.rb +589 -0
  21. data/lib/tty2/prompt/mask_question.rb +96 -0
  22. data/lib/tty2/prompt/multi_list.rb +224 -0
  23. data/lib/tty2/prompt/multiline.rb +72 -0
  24. data/lib/tty2/prompt/paginator.rb +111 -0
  25. data/lib/tty2/prompt/question/checks.rb +105 -0
  26. data/lib/tty2/prompt/question/modifier.rb +96 -0
  27. data/lib/tty2/prompt/question/validation.rb +72 -0
  28. data/lib/tty2/prompt/question.rb +391 -0
  29. data/lib/tty2/prompt/result.rb +42 -0
  30. data/lib/tty2/prompt/selected_choices.rb +77 -0
  31. data/lib/tty2/prompt/slider.rb +286 -0
  32. data/lib/tty2/prompt/statement.rb +55 -0
  33. data/lib/tty2/prompt/suggestion.rb +113 -0
  34. data/lib/tty2/prompt/symbols.rb +89 -0
  35. data/lib/tty2/prompt/test.rb +36 -0
  36. data/lib/tty2/prompt/timer.rb +75 -0
  37. data/lib/tty2/prompt/utils.rb +42 -0
  38. data/lib/tty2/prompt/version.rb +7 -0
  39. data/lib/tty2/prompt.rb +589 -0
  40. data/lib/tty2-prompt.rb +1 -0
  41. metadata +148 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a31ec4aa1ef32dbce0b75a62191dad2c4b108e7f801ba6918862a57021339acf
4
+ data.tar.gz: 7fba5b2d9b0f076dca7ef7a462c6abca5373a123538e7abc95c7821855bab79c
5
+ SHA512:
6
+ metadata.gz: b0b987b875d9fec6de8511467db39ea7aeff6db61561ed73490acbd7bbb69ff798ad4cc078a7bbb3af065e3702f916ff0353106e63e66615179ac790cd810b4b
7
+ data.tar.gz: 113405ed0aebead16dcc0739012e05eb7d5b430f923cba02c44b2d423e40dfc21273bcc7c741c2b247702af39a6a5ff1ec6ca626b0880b22047cfc80feb91074
data/CHANGELOG.md ADDED
@@ -0,0 +1,14 @@
1
+ # Change log
2
+
3
+ ## [v0.23.1.1] - 2021-12-13
4
+
5
+ ### Added
6
+
7
+ ### Changed
8
+ * Forked TTY::Prompt and reworked to TTY2::Prompt
9
+ * Change to make use of tty2-reader instead of tty-reader
10
+
11
+ ### Fix
12
+
13
+
14
+ [v0.23.1.1]: https://github.com/zzyzwicz/tty2-prompt/compare/v0.23.1.1
data/LICENSE.txt ADDED
@@ -0,0 +1,23 @@
1
+ Copyright for portions of project TTY2::Prompt are held by Piotr Murach, 2015 as part of project TTY::Prompt.
2
+ All other copyright for project TTY2::Prompt are held by zzyzwicz, 2021.
3
+
4
+ MIT License
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining
7
+ a copy of this software and associated documentation files (the
8
+ "Software"), to deal in the Software without restriction, including
9
+ without limitation the rights to use, copy, modify, merge, publish,
10
+ distribute, sublicense, and/or sell copies of the Software, and to
11
+ permit persons to whom the Software is furnished to do so, subject to
12
+ the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be
15
+ included in all copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
21
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
22
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
23
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,52 @@
1
+
2
+ # TTY2::Prompt
3
+
4
+ [![Gem Version](https://badge.fury.io/rb/tty2-prompt.svg)][gem]
5
+ [![Build status](https://ci.appveyor.com/api/projects/status/570nk071y68idah1?svg=true)][appveyor]
6
+
7
+ [gem]: http://badge.fury.io/rb/tty2-prompt
8
+ [appveyor]: https://ci.appveyor.com/project/zzyzwicz/tty2-prompt
9
+
10
+ > A tty-prompt fork with the objective of adding a customized word completion mechanism
11
+
12
+ **TTY2::Prompt** intends to be an up to date clone of [TTY::Prompt](https://github.com/piotrmurach/tty-prompt), solely extending it with a customized word completion mechanism.
13
+ This page only covers the applied modifications.
14
+
15
+ ## Modifications
16
+
17
+ * Renamed to TTY2::Prompt
18
+ * Make use of TTY2::Reader instead of TTY::Reader
19
+
20
+ ## Installation
21
+
22
+ Add this line to your application's Gemfile:
23
+
24
+ ```ruby
25
+ gem "tty2-prompt"
26
+ ```
27
+
28
+ And then execute:
29
+
30
+ $ bundle
31
+
32
+ Or install it yourself as:
33
+
34
+ $ gem install tty2-prompt
35
+
36
+
37
+ ## Contributing
38
+
39
+ Bug reports and pull requests are welcome on GitHub at https://github.com/zzyzwicz/tty2-prompt.
40
+
41
+ ## License
42
+
43
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
44
+
45
+ ## Code of Conduct
46
+
47
+ Everyone interacting in the TTY2::Reader project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/piotrmurach/tty-reader/blob/master/CODE_OF_CONDUCT.md).
48
+
49
+ ## Copyright
50
+
51
+ Copyright for portions of project TTY2::Prompt are held by Piotr Murach, 2015 as part of project TTY::Prompt.
52
+ All other copyright for project TTY2::Prompt are held by zzyzwicz, 2021.
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TTY2
4
+ class Prompt
5
+ class AnswersCollector
6
+ # Initialize answer collector
7
+ #
8
+ # @api public
9
+ def initialize(prompt, **options)
10
+ @prompt = prompt
11
+ @answers = options.fetch(:answers) { {} }
12
+ end
13
+
14
+ # Start gathering answers
15
+ #
16
+ # @return [Hash]
17
+ # the collection of all answers
18
+ #
19
+ # @api public
20
+ def call(&block)
21
+ instance_eval(&block)
22
+ @answers
23
+ end
24
+
25
+ # Create answer entry
26
+ #
27
+ # @example
28
+ # key(:name).ask("Name?")
29
+ #
30
+ # @api public
31
+ def key(name, &block)
32
+ @name = name
33
+ if block
34
+ answer = create_collector.call(&block)
35
+ add_answer(answer)
36
+ end
37
+ self
38
+ end
39
+
40
+ # Change to collect all values for a key
41
+ #
42
+ # @example
43
+ # key(:colors).values.ask("Color?")
44
+ #
45
+ # @api public
46
+ def values(&block)
47
+ @answers[@name] = Array(@answers[@name])
48
+ if block
49
+ answer = create_collector.call(&block)
50
+ add_answer(answer)
51
+ end
52
+ self
53
+ end
54
+
55
+ # @api public
56
+ def create_collector
57
+ self.class.new(@prompt)
58
+ end
59
+
60
+ # @api public
61
+ def add_answer(answer)
62
+ if @answers[@name].is_a?(Array)
63
+ @answers[@name] << answer
64
+ else
65
+ @answers[@name] = answer
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ # @api private
72
+ def method_missing(method, *args, **options, &block)
73
+ answer = @prompt.public_send(method, *args, **options, &block)
74
+ add_answer(answer)
75
+ end
76
+ end # AnswersCollector
77
+ end # Prompt
78
+ end # TTY2
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "paginator"
4
+
5
+ module TTY2
6
+ class Prompt
7
+ class BlockPaginator < Paginator
8
+ # Paginate list of choices based on current active choice.
9
+ # Move entire pages.
10
+ #
11
+ # @api public
12
+ def paginate(list, active, per_page = nil, &block)
13
+ default_size = (list.size <= DEFAULT_PAGE_SIZE ? list.size : DEFAULT_PAGE_SIZE)
14
+ @per_page = @per_page || per_page || default_size
15
+
16
+ check_page_size!
17
+
18
+ # Don't paginate short lists
19
+ if list.size <= @per_page
20
+ @start_index = 0
21
+ @end_index = list.size - 1
22
+ if block
23
+ return list.each_with_index(&block)
24
+ else
25
+ return list.each_with_index.to_enum
26
+ end
27
+ end
28
+
29
+ unless active.nil? # User may input index out of range
30
+ @last_index = active
31
+ end
32
+ page = (@last_index / @per_page.to_f).ceil
33
+ pages = (list.size / @per_page.to_f).ceil
34
+ if page == 0
35
+ @start_index = 0
36
+ @end_index = @start_index + @per_page - 1
37
+ elsif page > 0 && page < pages
38
+ @start_index = (page - 1) * @per_page
39
+ @end_index = @start_index + @per_page - 1
40
+ elsif page == pages
41
+ @start_index = (page - 1) * @per_page
42
+ @end_index = list.size - 1
43
+ else
44
+ @end_index = list.size - 1
45
+ @start_index = @end_index - @per_page + 1
46
+ end
47
+
48
+ sliced_list = list[@start_index..@end_index]
49
+ page_range = (@start_index..@end_index)
50
+
51
+ return sliced_list.zip(page_range).to_enum unless block_given?
52
+
53
+ sliced_list.each_with_index do |item, index|
54
+ block[item, @start_index + index]
55
+ end
56
+ end
57
+ end # EnumPaginator
58
+ end # Prompt
59
+ end # TTY2
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TTY2
4
+ class Prompt
5
+ # An immutable representation of a single choice option from select menu
6
+ #
7
+ # @api public
8
+ class Choice
9
+ # Create choice from value
10
+ #
11
+ # @examples
12
+ # Choice.from(:foo)
13
+ # # => <TTY2::Prompt::Choice @key=nil @name="foo" @value="foo" @disabled=false>
14
+ #
15
+ # Choice.from([:foo, 1])
16
+ # # => <TTY2::Prompt::Choice @key=nil @name="foo" @value=1 @disabled=false>
17
+ #
18
+ # Choice.from({name: :foo, value: 1, key: "f"}
19
+ # # => <TTY2::Prompt::Choice @key="f" @name="foo" @value=1 @disabled=false>
20
+ #
21
+ # @param [Object] val
22
+ # the value to be converted
23
+ #
24
+ # @raise [ArgumentError]
25
+ #
26
+ # @return [Choice]
27
+ #
28
+ # @api public
29
+ def self.from(val)
30
+ case val
31
+ when Choice
32
+ val
33
+ when Array
34
+ convert_array(val)
35
+ when Hash
36
+ convert_hash(val)
37
+ else
38
+ new(val, val)
39
+ end
40
+ end
41
+
42
+ # Convert an array into choice
43
+ #
44
+ # @param [Array<Object>]
45
+ #
46
+ # @return [Choice]
47
+ #
48
+ # @api public
49
+ def self.convert_array(val)
50
+ name, value, options = *val
51
+ if name.is_a?(Hash)
52
+ convert_hash(name)
53
+ elsif val.size == 1
54
+ new(name.to_s, name.to_s)
55
+ else
56
+ new(name.to_s, value, **(options || {}))
57
+ end
58
+ end
59
+
60
+ # Convert a hash into choice
61
+ #
62
+ # @param [Hash<Symbol,Object>]
63
+ #
64
+ # @return [Choice]
65
+ #
66
+ # @api public
67
+ def self.convert_hash(val)
68
+ if val.key?(:name) && val.key?(:value)
69
+ new(val[:name].to_s, val[:value], **val)
70
+ elsif val.key?(:name)
71
+ new(val[:name].to_s, val[:name].to_s, **val)
72
+ else
73
+ new(val.keys.first.to_s, val.values.first)
74
+ end
75
+ end
76
+
77
+ # The label name
78
+ #
79
+ # @api public
80
+ attr_reader :name
81
+
82
+ # The keyboard key to activate this choice
83
+ #
84
+ # @api public
85
+ attr_reader :key
86
+
87
+ # The text to display for disabled choice
88
+ #
89
+ # @api public
90
+ attr_reader :disabled
91
+
92
+ # Create a Choice instance
93
+ #
94
+ # @api public
95
+ def initialize(name, value, **options)
96
+ @name = name
97
+ @value = value
98
+ @key = options[:key]
99
+ @disabled = options[:disabled].nil? ? false : options[:disabled]
100
+ freeze
101
+ end
102
+
103
+ # Check if this choice is disabled
104
+ #
105
+ # @return [Boolean]
106
+ #
107
+ # @api public
108
+ def disabled?
109
+ !!@disabled
110
+ end
111
+
112
+ # Read value and evaluate
113
+ #
114
+ # @api public
115
+ def value
116
+ case @value
117
+ when Proc
118
+ @value.call
119
+ else
120
+ @value
121
+ end
122
+ end
123
+
124
+ # Object equality comparison
125
+ #
126
+ # @return [Boolean]
127
+ #
128
+ # @api public
129
+ def ==(other)
130
+ return false unless other.is_a?(self.class)
131
+
132
+ name == other.name &&
133
+ value == other.value &&
134
+ key == other.key
135
+ end
136
+
137
+ # Object string representation
138
+ #
139
+ # @return [String]
140
+ #
141
+ # @api public
142
+ def to_s
143
+ name.to_s
144
+ end
145
+ end # Choice
146
+ end # Prompt
147
+ end # TTY2
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ require_relative "choice"
6
+
7
+ module TTY2
8
+ class Prompt
9
+ # A class responsible for storing a collection of choices
10
+ #
11
+ # @api private
12
+ class Choices
13
+ include Enumerable
14
+ extend Forwardable
15
+
16
+ def_delegators :choices, :length, :size, :to_ary, :empty?,
17
+ :values_at, :index, :==
18
+
19
+ # Convenience for creating choices
20
+ #
21
+ # @param [Array[Object]] choices
22
+ # the choice objects
23
+ #
24
+ # @return [Choices]
25
+ # the choices collection
26
+ #
27
+ # @api public
28
+ def self.[](*choices)
29
+ new(choices)
30
+ end
31
+
32
+ # Create Choices collection
33
+ #
34
+ # @param [Array[Choice]] choices
35
+ # the choices to add to collection
36
+ #
37
+ # @api public
38
+ def initialize(choices = [])
39
+ @choices = choices.map do |choice|
40
+ Choice.from(choice)
41
+ end
42
+ end
43
+
44
+ # Scope of choices which are not disabled
45
+ #
46
+ # @api public
47
+ def enabled
48
+ reject(&:disabled?)
49
+ end
50
+
51
+ def enabled_indexes
52
+ each_with_index.reduce([]) do |acc, (choice, idx)|
53
+ acc << idx unless choice.disabled?
54
+ acc
55
+ end
56
+ end
57
+
58
+ # Iterate over all choices in the collection
59
+ #
60
+ # @yield [Choice]
61
+ #
62
+ # @api public
63
+ def each(&block)
64
+ return to_enum unless block_given?
65
+
66
+ choices.each(&block)
67
+ end
68
+
69
+ # Add choice to collection
70
+ #
71
+ # @param [Object] choice
72
+ # the choice to add
73
+ #
74
+ # @api public
75
+ def <<(choice)
76
+ choices << Choice.from(choice)
77
+ end
78
+
79
+ # Access choice by index
80
+ #
81
+ # @param [Integer] index
82
+ #
83
+ # @return [Choice]
84
+ #
85
+ # @api public
86
+ def [](index)
87
+ @choices[index]
88
+ end
89
+
90
+ # Pluck a choice by its name from collection
91
+ #
92
+ # @param [String] name
93
+ # the label name for the choice
94
+ #
95
+ # @return [Choice]
96
+ #
97
+ # @api public
98
+ def pluck(name)
99
+ map { |choice| choice.public_send(name) }
100
+ end
101
+
102
+ # Find a matching choice
103
+ #
104
+ # @example
105
+ # choices.find_by(:name, "small")
106
+ #
107
+ # @param [Symbol] attr
108
+ # the attribute name
109
+ # @param [Object] value
110
+ #
111
+ # @return [Choice]
112
+ #
113
+ # @api public
114
+ def find_by(attr, value)
115
+ find { |choice| choice.public_send(attr) == value }
116
+ end
117
+
118
+ protected
119
+
120
+ # The actual collection choices
121
+ #
122
+ # @return [Array[Choice]]
123
+ #
124
+ # @api private
125
+
126
+ attr_reader :choices
127
+ end # Choices
128
+ end # Prompt
129
+ end # TTY2
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "question"
4
+ require_relative "utils"
5
+
6
+ module TTY2
7
+ class Prompt
8
+ class ConfirmQuestion < Question
9
+ # Create confirmation question
10
+ #
11
+ # @param [Hash] options
12
+ # @option options [String] :suffix
13
+ # @option options [String] :positive
14
+ # @option options [String] :negative
15
+ #
16
+ # @api public
17
+ def initialize(prompt, **options)
18
+ super
19
+ @suffix = options.fetch(:suffix) { UndefinedSetting }
20
+ @positive = options.fetch(:positive) { UndefinedSetting }
21
+ @negative = options.fetch(:negative) { UndefinedSetting }
22
+ end
23
+
24
+ def positive?
25
+ @positive != UndefinedSetting
26
+ end
27
+
28
+ def negative?
29
+ @negative != UndefinedSetting
30
+ end
31
+
32
+ def suffix?
33
+ @suffix != UndefinedSetting
34
+ end
35
+
36
+ # Set question suffix
37
+ #
38
+ # @api public
39
+ def suffix(value = (not_set = true))
40
+ return @negative if not_set
41
+
42
+ @suffix = value
43
+ end
44
+
45
+ # Set value for matching positive choice
46
+ #
47
+ # @api public
48
+ def positive(value = (not_set = true))
49
+ return @positive if not_set
50
+
51
+ @positive = value
52
+ end
53
+
54
+ # Set value for matching negative choice
55
+ #
56
+ # @api public
57
+ def negative(value = (not_set = true))
58
+ return @negative if not_set
59
+
60
+ @negative = value
61
+ end
62
+
63
+ def call(message, &block)
64
+ return if Utils.blank?(message)
65
+
66
+ @message = message
67
+ block.call(self) if block
68
+ setup_defaults
69
+ render
70
+ end
71
+
72
+ # Render confirmation question
73
+ #
74
+ # @return [String]
75
+ #
76
+ # @api private
77
+ def render_question
78
+ header = "#{@prefix}#{message} "
79
+ if !@done
80
+ header += @prompt.decorate("(#{@suffix})", @help_color) + " "
81
+ else
82
+ answer = conversion.call(@input)
83
+ label = answer ? @positive : @negative
84
+ header += @prompt.decorate(label, @active_color)
85
+ end
86
+ header << "\n" if @done
87
+ header
88
+ end
89
+
90
+ protected
91
+
92
+ # Decide how to handle input from user
93
+ #
94
+ # @api private
95
+ def process_input(question)
96
+ @input = read_input(question)
97
+ if Utils.blank?(@input)
98
+ @input = default ? positive : negative
99
+ end
100
+ @evaluator.call(@input)
101
+ end
102
+
103
+ # @api private
104
+ def setup_defaults
105
+ infer_default
106
+ @convert = conversion
107
+ return if suffix? && positive?
108
+
109
+ if suffix? && (!positive? || !negative?)
110
+ parts = @suffix.split("/")
111
+ @positive = parts[0]
112
+ @negative = parts[1]
113
+ elsif !suffix? && positive?
114
+ @suffix = create_suffix
115
+ else
116
+ create_default_labels
117
+ end
118
+ end
119
+
120
+ # @api private
121
+ def infer_default
122
+ converted = Converters.convert(:bool, default.to_s)
123
+ if converted == Const::Undefined
124
+ raise InvalidArgument, "default needs to be `true` or `false`"
125
+ else
126
+ default(converted)
127
+ end
128
+ end
129
+
130
+ # @api private
131
+ def create_default_labels
132
+ @suffix = default ? "Y/n" : "y/N"
133
+ @positive = default ? "Yes" : "yes"
134
+ @negative = default ? "no" : "No"
135
+ @validation = /^(y(es)?|no?)$/i
136
+ @messages[:valid?] = "Invalid input."
137
+ end
138
+
139
+ # @api private
140
+ def create_suffix
141
+ (default ? positive.capitalize : positive.downcase) + "/" +
142
+ (default ? negative.downcase : negative.capitalize)
143
+ end
144
+
145
+ # Create custom conversion
146
+ #
147
+ # @api private
148
+ def conversion
149
+ ->(input) do
150
+ positive_word = Regexp.escape(positive)
151
+ positive_letter = Regexp.escape(positive[0])
152
+ pattern = Regexp.new("^(#{positive_word}|#{positive_letter})$", true)
153
+ !input.match(pattern).nil?
154
+ end
155
+ end
156
+ end # ConfirmQuestion
157
+ end # Prompt
158
+ end # TTY2
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TTY2
4
+ class Prompt
5
+ module Const
6
+ Undefined = Object.new.tap do |obj|
7
+ def obj.to_s
8
+ "undefined"
9
+ end
10
+
11
+ def obj.inspect
12
+ "undefined".inspect
13
+ end
14
+ end
15
+ end # Const
16
+ end # Prompt
17
+ end # TTY2
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "converter_registry"
4
+
5
+ module TTY2
6
+ class Prompt
7
+ module ConverterDSL
8
+ def converter_registry
9
+ @__converter_registry ||= ConverterRegistry.new
10
+ end
11
+
12
+ def converter(*names, &block)
13
+ converter_registry.register(*names, &block)
14
+ end
15
+
16
+ def convert(name, input)
17
+ converter_registry[name].call(input)
18
+ end
19
+ end # ConverterDSL
20
+ end # Prompt
21
+ end # TTY2