dev-ui 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: cb7feeed4cd2083e294dd368521e85d4240cb954
4
- data.tar.gz: d87bb0bab774010578bfaed70966e87e8a0222b6
3
+ metadata.gz: 32b6a7470b4f74f2448604df24a809d4dd5b6176
4
+ data.tar.gz: 47da2a0c8007c5232d7e447ba74406586fa740f4
5
5
  SHA512:
6
- metadata.gz: b371ef8177c5d3626d75c940d56b3afa40924a70fbdc0bb941b3f33c1e5253eb41118a023c6c3a72e92bd15df2bacc65dc361874cc9f4dd08162d402c9215100
7
- data.tar.gz: 0dfc477dd137c74de7a99336478c7c120c3cd394c5e259e2b758a6777a7d1fe1eae0ab859c0728c9a224d80bd0f28601e5c2088488bf4ba4025c32dc0b42005f
6
+ metadata.gz: 51d64a4852b8b4981c778be9d1e0bbbd98bbe58a18917af5fcf2c05f2f53494380726a9ddb1c48e10481afbeac7ea99a9101562cb9b5464fcbddeef2f4debb40
7
+ data.tar.gz: 3749f235fc1b4e30ac3836aec348f8848172ef6d4ae35299c30b6dfc291caab6e4dca9afa891d5bde83c5e3d21dacb736f34c95d370af5a1d900e407cae86813
@@ -2,13 +2,7 @@ inherit_from:
2
2
  - http://shopify.github.io/ruby-style-guide/rubocop.yml
3
3
 
4
4
  AllCops:
5
- Exclude:
6
- - 'vendor/**/*'
7
- TargetRubyVersion: 2.0
8
-
9
- # This doesn't understand that <<~ doesn't exist in 2.0
10
- Style/IndentHeredoc:
11
- Enabled: false
5
+ TargetRubyVersion: 2.1
12
6
 
13
7
  # This doesn't take into account retrying from an exception
14
8
  Lint/HandleExceptions:
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- dev-ui (0.1.0)
4
+ dev-ui (0.1.2)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -20,22 +20,21 @@ GEM
20
20
  ruby-progressbar
21
21
  mocha (1.2.1)
22
22
  metaclass (~> 0.0.1)
23
- parallel (1.11.2)
24
- parser (2.4.0.0)
25
- ast (~> 2.2)
23
+ parallel (1.12.0)
24
+ parser (2.4.0.2)
25
+ ast (~> 2.3)
26
26
  powerpack (0.1.1)
27
- rainbow (2.2.2)
28
- rake
27
+ rainbow (3.0.0)
29
28
  rake (10.5.0)
30
- rubocop (0.49.1)
29
+ rubocop (0.52.0)
31
30
  parallel (~> 1.10)
32
- parser (>= 2.3.3.1, < 3.0)
31
+ parser (>= 2.4.0.2, < 3.0)
33
32
  powerpack (~> 0.1)
34
- rainbow (>= 1.99.1, < 3.0)
33
+ rainbow (>= 2.2.2, < 4.0)
35
34
  ruby-progressbar (~> 1.7)
36
35
  unicode-display_width (~> 1.0, >= 1.0.1)
37
- ruby-progressbar (1.8.1)
38
- unicode-display_width (1.2.1)
36
+ ruby-progressbar (1.9.0)
37
+ unicode-display_width (1.3.0)
39
38
 
40
39
  PLATFORMS
41
40
  ruby
data/README.md CHANGED
@@ -47,7 +47,6 @@ Prompt user with options and ask them to choose. Can answer using arrow keys, nu
47
47
 
48
48
  ```ruby
49
49
  Dev::UI.ask('What language/framework do you use?', options: %w(rails go ruby python))
50
- Dev::UI::InteractivePrompt.call(%w(rails go ruby python))
51
50
  ```
52
51
 
53
52
  ![Interactive Prompt](https://user-images.githubusercontent.com/3074765/33797984-0ebb5e64-dcdf-11e7-9e7e-7204f279cece.gif)
@@ -150,3 +149,16 @@ end
150
149
  Output:
151
150
 
152
151
  ![Example Output](https://user-images.githubusercontent.com/3074765/33797758-7a54c7cc-dcdb-11e7-918e-a47c9689f068.gif)
152
+
153
+ ## Development
154
+
155
+ - Run tests using `bundle exec rake test`. All code should be tested.
156
+ - No code, outside of development and tests needs, should use dependencies. This is a self contained library
157
+
158
+ ## Contributing
159
+
160
+ Bug reports and pull requests are welcome on GitHub at https://github.com/Shopify/dev-ui.
161
+
162
+ ## License
163
+
164
+ The code is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
@@ -0,0 +1,19 @@
1
+ require 'dev/ui'
2
+ require 'rake/testtask'
3
+ require 'rubocop/rake_task'
4
+ require 'bundler/gem_tasks'
5
+
6
+ TEST_ROOT = File.expand_path('../test', __FILE__)
7
+
8
+ Rake::TestTask.new do |t|
9
+ t.libs += ["test"]
10
+ t.test_files = FileList[File.join(TEST_ROOT, '**', '*_test.rb')]
11
+ t.verbose = false
12
+ t.warning = false
13
+ end
14
+
15
+ RuboCop::RakeTask.new(:style) do |t|
16
+ t.options = ['--display-cop-names']
17
+ end
18
+
19
+ task default: [:test]
@@ -6,11 +6,11 @@ require "dev/ui/version"
6
6
  Gem::Specification.new do |spec|
7
7
  spec.name = "dev-ui"
8
8
  spec.version = Dev::UI::VERSION
9
- spec.authors = ["Burke Libbey", "Julian Nadeau"]
10
- spec.email = ["burke.libbey@shopify.com", "julian.nadeau@shopify.com"]
9
+ spec.authors = ["Burke Libbey", "Julian Nadeau", "Lisa Ugray"]
10
+ spec.email = ["burke.libbey@shopify.com", "julian.nadeau@shopify.com", "lisa.ugray@shopify.com"]
11
11
 
12
- spec.summary = %q{Terminal UI framework}
13
- spec.description = %q{Terminal UI framework}
12
+ spec.summary = 'Terminal UI framework'
13
+ spec.description = 'Terminal UI framework'
14
14
  spec.homepage = "https://github.com/shopify/dev-ui"
15
15
  spec.license = "MIT"
16
16
 
data/dev.yml CHANGED
@@ -3,5 +3,12 @@ up:
3
3
  - bundler
4
4
 
5
5
  commands:
6
- test: bin/testunit
6
+ test:
7
+ run: |
8
+ if [ "$#" -eq 1 ] && [[ -f $1 ]];
9
+ then
10
+ rake test TEST=$1
11
+ else
12
+ rake test $@
13
+ fi
7
14
  style: "bundle exec rubocop -D"
@@ -5,7 +5,6 @@ module Dev
5
5
  autoload :Color, 'dev/ui/color'
6
6
  autoload :Box, 'dev/ui/box'
7
7
  autoload :Frame, 'dev/ui/frame'
8
- autoload :InteractivePrompt, 'dev/ui/interactive_prompt'
9
8
  autoload :Progress, 'dev/ui/progress'
10
9
  autoload :Prompt, 'dev/ui/prompt'
11
10
  autoload :Terminal, 'dev/ui/terminal'
@@ -140,7 +139,7 @@ module Dev
140
139
  yield
141
140
  ensure
142
141
  if file_descriptor = Dev::UI::StdoutRouter.duplicate_output_to
143
- file_descriptor.close
142
+ file_descriptor.close
144
143
  Dev::UI::StdoutRouter.duplicate_output_to = nil
145
144
  end
146
145
  end
@@ -37,14 +37,14 @@ module Dev
37
37
 
38
38
  SCAN_FUNCNAME = /\w+:/
39
39
  SCAN_GLYPH = /.}}/
40
- SCAN_BODY = %r{
40
+ SCAN_BODY = /
41
41
  .*?
42
42
  (
43
43
  #{BEGIN_EXPR} |
44
44
  #{END_EXPR} |
45
45
  \z
46
46
  )
47
- }mx
47
+ /mx
48
48
 
49
49
  DISCARD_BRACES = 0..-3
50
50
 
@@ -29,7 +29,7 @@ module Dev
29
29
  # * +:timing+ - How long did the frame content take? Invalid for blockless. Defaults to true for the block form
30
30
  #
31
31
  # ==== Example
32
- #
32
+ #
33
33
  # ===== Block Form (Assumes +Dev::UI::StdoutRouter.enable+ has been called)
34
34
  #
35
35
  # Dev::UI::Frame.open('Open') { puts 'hi' }
@@ -38,7 +38,7 @@ module Dev
38
38
  # ┏━━ Open ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
39
39
  # ┃ hi
40
40
  # ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ (0.0s) ━━
41
- #
41
+ #
42
42
  # ===== Blockless Form
43
43
  #
44
44
  # Dev::UI::Frame.open('Open')
@@ -46,7 +46,7 @@ module Dev
46
46
  # Output:
47
47
  # ┏━━ Open ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
48
48
  #
49
- #
49
+ #
50
50
  def open(
51
51
  text,
52
52
  color: DEFAULT_FRAME_COLOR,
@@ -80,7 +80,7 @@ module Dev
80
80
  begin
81
81
  success = false
82
82
  success = yield
83
- rescue Exception
83
+ rescue
84
84
  closed = true
85
85
  t_diff = timing ? (Time.now.to_f - t_start) : nil
86
86
  close(failure_text, color: :red, elapsed: t_diff)
@@ -118,7 +118,7 @@ module Dev
118
118
  # Output:
119
119
  # ┗━━ Close ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
120
120
  #
121
- #
121
+ #
122
122
  def close(text, color: DEFAULT_FRAME_COLOR, elapsed: nil)
123
123
  color = Dev::UI.resolve_color(color)
124
124
 
@@ -188,7 +188,7 @@ module Dev
188
188
  pfx
189
189
  end
190
190
 
191
- # Override a color for a given thread.
191
+ # Override a color for a given thread.
192
192
  #
193
193
  # ==== Attributes
194
194
  #
@@ -202,7 +202,7 @@ module Dev
202
202
  Thread.current[:devui_frame_color_override] = prev
203
203
  end
204
204
 
205
- # The width of a prefix given the number of Frames in the stack
205
+ # The width of a prefix given the number of Frames in the stack
206
206
  #
207
207
  def prefix_width
208
208
  w = FrameStack.items.size
@@ -254,15 +254,15 @@ module Dev
254
254
  # Jumping around the line can cause some unwanted flashes
255
255
  o << Dev::UI::ANSI.hide_cursor
256
256
 
257
- if is_ci
258
- # In CI, we can't use absolute horizontal positions because of timestamps.
259
- # So we move around the line by offset from this cursor position.
260
- o << Dev::UI::ANSI.cursor_save
261
- else
262
- # Outside of CI, we reset to column 1 so that things like ^C don't
263
- # cause output misformatting.
264
- o << "\r"
265
- end
257
+ o << if is_ci
258
+ # In CI, we can't use absolute horizontal positions because of timestamps.
259
+ # So we move around the line by offset from this cursor position.
260
+ Dev::UI::ANSI.cursor_save
261
+ else
262
+ # Outside of CI, we reset to column 1 so that things like ^C don't
263
+ # cause output misformatting.
264
+ "\r"
265
+ end
266
266
 
267
267
  o << color.code
268
268
  o << Dev::UI::Box::Heavy::HORZ * termwidth # draw a full line
@@ -4,6 +4,9 @@ require 'readline'
4
4
  module Dev
5
5
  module UI
6
6
  module Prompt
7
+ autoload :InteractiveOptions, 'dev/ui/prompt/interactive_options'
8
+ private_constant :InteractiveOptions
9
+
7
10
  class << self
8
11
  # Ask a user a question with either free form answer or a set of answers
9
12
  # Do not use this method for yes/no questions. Use +confirm+
@@ -12,7 +15,7 @@ module Dev
12
15
  # * Handles free form answers (options are nil)
13
16
  # * Handles default answers for free form text
14
17
  # * Handles file auto completion for file input
15
- # * Handles interactively choosing answers using +InteractivePrompt+
18
+ # * Handles interactively choosing answers using +InteractiveOptions+
16
19
  #
17
20
  # https://user-images.githubusercontent.com/3074765/33799822-47f23302-dd01-11e7-82f3-9072a5a5f611.png
18
21
  #
@@ -22,7 +25,7 @@ module Dev
22
25
  #
23
26
  # ==== Options
24
27
  #
25
- # * +:options+ - Options to ask the user. Will use +InteractivePrompt+ to do so
28
+ # * +:options+ - Options to ask the user. Will use +InteractiveOptions+ to do so
26
29
  # * +:default+ - The default answer to the question (e.g. they just press enter and don't input anything)
27
30
  # * +:is_file+ - Tells the input to use file auto-completion (tab completion)
28
31
  # * +:allow_empty+ - Allows the answer to be empty
@@ -62,8 +65,21 @@ module Dev
62
65
  puts_question(question)
63
66
  end
64
67
 
65
- return InteractivePrompt.call(options) if options
68
+ # Present the user with options
69
+ if options
70
+ resp = InteractiveOptions.call(options)
71
+
72
+ # Clear the line, and reset the question to include the answer
73
+ print(ANSI.previous_line + ANSI.end_of_line + ' ')
74
+ print(ANSI.cursor_save)
75
+ print(' ' * Dev::UI::Terminal.width)
76
+ print(ANSI.cursor_restore)
77
+ puts_question("#{question} (You chose: {{italic:#{resp}}})")
66
78
 
79
+ return resp
80
+ end
81
+
82
+ # Ask a free form question
67
83
  loop do
68
84
  line = readline(is_file: is_file)
69
85
 
@@ -83,12 +99,11 @@ module Dev
83
99
  #
84
100
  # ==== Example Usage:
85
101
  #
86
- # Free form question
102
+ # Confirmation question
87
103
  # Dev::UI::Prompt.confirm('Is the sky blue?')
88
104
  #
89
105
  def confirm(question)
90
- puts_question("#{question} {{yellow:(choose with ↓ ⏎)}}")
91
- InteractivePrompt.call(%w(yes no)) == 'yes'
106
+ ask(question, options: %w(yes no)) == 'yes'
92
107
  end
93
108
 
94
109
  private
@@ -0,0 +1,169 @@
1
+ require 'io/console'
2
+
3
+ module Dev
4
+ module UI
5
+ module Prompt
6
+ class InteractiveOptions
7
+ # Prompts the user with options
8
+ # Uses an interactive session to allow the user to pick an answer
9
+ # Can use arrows, y/n, numbers (1/2), and vim bindings to control
10
+ #
11
+ # https://user-images.githubusercontent.com/3074765/33797984-0ebb5e64-dcdf-11e7-9e7e-7204f279cece.gif
12
+ #
13
+ # ==== Example Usage:
14
+ #
15
+ # Ask an interactive question
16
+ # Dev::UI::Prompt::InteractiveOptions.call(%w(rails go python))
17
+ #
18
+ def self.call(options)
19
+ list = new(options)
20
+ options[list.call - 1]
21
+ end
22
+
23
+ # Initializes a new +InteractiveOptions+
24
+ # Usually called from +self.call+
25
+ #
26
+ # ==== Example Usage:
27
+ #
28
+ # Dev::UI::Prompt::InteractiveOptions.new(%w(rails go python))
29
+ #
30
+ def initialize(options)
31
+ @options = options
32
+ @active = 1
33
+ @marker = '>'
34
+ @answer = nil
35
+ @state = :root
36
+ end
37
+
38
+ # Calls the +InteractiveOptions+ and asks the question
39
+ # Usually used from +self.call+
40
+ #
41
+ def call
42
+ Dev::UI.raw { print(ANSI.hide_cursor) }
43
+ while @answer.nil?
44
+ render_options
45
+ wait_for_user_input
46
+ reset_position
47
+ end
48
+ clear_output
49
+ @answer
50
+ ensure
51
+ Dev::UI.raw do
52
+ print(ANSI.show_cursor)
53
+ puts(ANSI.previous_line + ANSI.end_of_line)
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ def reset_position
60
+ # This will put us back at the beginning of the options
61
+ # When we redraw the options, they will be overwritten
62
+ Dev::UI.raw do
63
+ num_lines.times { print(ANSI.previous_line) }
64
+ print(ANSI.previous_line + ANSI.end_of_line + "\n")
65
+ end
66
+ end
67
+
68
+ def clear_output
69
+ Dev::UI.raw do
70
+ # Write over all lines with whitespace
71
+ num_lines.times { puts(' ' * Dev::UI::Terminal.width) }
72
+ end
73
+ reset_position
74
+ end
75
+
76
+ def num_lines
77
+ # @options will be an array of questions but each option can be multi-line
78
+ # so to get the # of lines, you need to join then split
79
+ joined_options = @options.join("\n")
80
+ joined_options.split("\n").reject(&:empty?).size
81
+ end
82
+
83
+ ESC = "\e"
84
+
85
+ def up
86
+ @active = @active - 1 >= 1 ? @active - 1 : @options.length
87
+ end
88
+
89
+ def down
90
+ @active = @active + 1 <= @options.length ? @active + 1 : 1
91
+ end
92
+
93
+ def select_n(n)
94
+ @active = n
95
+ @answer = n
96
+ end
97
+
98
+ def select_bool(char)
99
+ return unless (@options - %w(yes no)).empty?
100
+ opt = @options.detect { |o| o.start_with?(char) }
101
+ @active = @options.index(opt) + 1
102
+ @answer = @options.index(opt) + 1
103
+ end
104
+
105
+ # rubocop:disable Style/WhenThen,Layout/SpaceBeforeSemicolon
106
+ def wait_for_user_input
107
+ char = read_char
108
+ case @state
109
+ when :root
110
+ case char
111
+ when ESC ; @state = :esc
112
+ when 'k' ; up
113
+ when 'j' ; down
114
+ when ('1'..@options.size.to_s) ; select_n(char.to_i)
115
+ when 'y', 'n' ; select_bool(char)
116
+ when " ", "\r", "\n" ; @answer = @active # <enter>
117
+ when "\u0003" ; raise Interrupt # Ctrl-c
118
+ end
119
+ when :esc
120
+ case char
121
+ when '[' ; @state = :esc_bracket
122
+ else ; raise Interrupt # unhandled escape sequence.
123
+ end
124
+ when :esc_bracket
125
+ @state = :root
126
+ case char
127
+ when 'A' ; up
128
+ when 'B' ; down
129
+ else ; raise Interrupt # unhandled escape sequence.
130
+ end
131
+ end
132
+ end
133
+ # rubocop:enable Style/WhenThen,Layout/SpaceBeforeSemicolon
134
+
135
+ def read_char
136
+ raw_tty! { $stdin.getc.chr }
137
+ rescue IOError
138
+ "\e"
139
+ end
140
+
141
+ def raw_tty!
142
+ if ENV['TEST'] || !$stdin.tty?
143
+ yield
144
+ else
145
+ $stdin.raw { yield }
146
+ end
147
+ end
148
+
149
+ def render_options
150
+ @options.each_with_index do |choice, index|
151
+ num = index + 1
152
+ message = " #{num}."
153
+ message += choice.split("\n").map { |l| " {{bold:#{l}}}" }.join("\n")
154
+
155
+ if num == @active
156
+ message = message.split("\n").map.with_index do |l, idx|
157
+ idx == 0 ? "{{blue:> #{l.strip}}}" : "{{blue:>#{l.strip}}}"
158
+ end.join("\n")
159
+ end
160
+
161
+ Dev::UI.with_frame_color(:blue) do
162
+ puts Dev::UI.fmt(message)
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
@@ -201,7 +201,7 @@ module Dev
201
201
 
202
202
  Dev::UI::Frame.open('Task Failed: ' + task.title, color: :red) do
203
203
  if e
204
- puts"#{e.class}: #{e.message}"
204
+ puts "#{e.class}: #{e.message}"
205
205
  puts "\tfrom #{e.backtrace.join("\n\tfrom ")}"
206
206
  end
207
207
 
@@ -8,7 +8,7 @@ module Dev
8
8
  # Otherwise will return 80
9
9
  #
10
10
  def self.width
11
- if console = IO.console
11
+ if console = IO.respond_to?(:console) && IO.console
12
12
  console.winsize[1]
13
13
  else
14
14
  80
@@ -1,5 +1,5 @@
1
1
  module Dev
2
2
  module UI
3
- VERSION = "0.1.1"
3
+ VERSION = "0.1.2"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,15 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dev-ui
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Burke Libbey
8
8
  - Julian Nadeau
9
+ - Lisa Ugray
9
10
  autorequire:
10
11
  bindir: exe
11
12
  cert_chain: []
12
- date: 2017-12-11 00:00:00.000000000 Z
13
+ date: 2017-12-13 00:00:00.000000000 Z
13
14
  dependencies:
14
15
  - !ruby/object:Gem::Dependency
15
16
  name: bundler
@@ -57,6 +58,7 @@ description: Terminal UI framework
57
58
  email:
58
59
  - burke.libbey@shopify.com
59
60
  - julian.nadeau@shopify.com
61
+ - lisa.ugray@shopify.com
60
62
  executables: []
61
63
  extensions: []
62
64
  extra_rdoc_files: []
@@ -68,8 +70,8 @@ files:
68
70
  - Gemfile.lock
69
71
  - LICENSE.txt
70
72
  - README.md
73
+ - Rakefile
71
74
  - bin/console
72
- - bin/testunit
73
75
  - dev-ui.gemspec
74
76
  - dev.yml
75
77
  - lib/dev/ui.rb
@@ -79,9 +81,9 @@ files:
79
81
  - lib/dev/ui/formatter.rb
80
82
  - lib/dev/ui/frame.rb
81
83
  - lib/dev/ui/glyph.rb
82
- - lib/dev/ui/interactive_prompt.rb
83
84
  - lib/dev/ui/progress.rb
84
85
  - lib/dev/ui/prompt.rb
86
+ - lib/dev/ui/prompt/interactive_options.rb
85
87
  - lib/dev/ui/spinner.rb
86
88
  - lib/dev/ui/spinner/async.rb
87
89
  - lib/dev/ui/spinner/spin_group.rb
@@ -108,7 +110,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
108
110
  version: '0'
109
111
  requirements: []
110
112
  rubyforge_project:
111
- rubygems_version: 2.6.14
113
+ rubygems_version: 2.5.2.1
112
114
  signing_key:
113
115
  specification_version: 4
114
116
  summary: Terminal UI framework
@@ -1,20 +0,0 @@
1
- #!/usr/bin/env ruby --disable-gems
2
-
3
- root = File.expand_path('../..', __FILE__)
4
- DEV_TEST_ROOT = root + '/test'
5
-
6
- $LOAD_PATH.unshift(DEV_TEST_ROOT)
7
-
8
- def test_files
9
- Dir.glob(DEV_TEST_ROOT + "/**/*_test.rb")
10
- end
11
-
12
- if ARGV.empty?
13
- test_files.each { |f| require(f) }
14
- exit 0
15
- end
16
-
17
- # A list of files is presumed to be specified
18
- ARGV.each do |a|
19
- require a.sub(%r{^test/}, '').sub(%r{^misc/gatekeeper/test/}, '')
20
- end
@@ -1,150 +0,0 @@
1
- require 'io/console'
2
-
3
- module Dev
4
- module UI
5
- class InteractivePrompt
6
- # Prompts the user with options
7
- # Uses an interactive session to allow the user to pick an answer
8
- # Can use arrows, y/n, numbers (1/2), and vim bindings to control
9
- #
10
- # https://user-images.githubusercontent.com/3074765/33797984-0ebb5e64-dcdf-11e7-9e7e-7204f279cece.gif
11
- #
12
- # ==== Example Usage:
13
- #
14
- # Ask an interactive question
15
- # Dev::UI::InteractivePrompt.call(%w(rails go python))
16
- #
17
- def self.call(options)
18
- list = new(options)
19
- options[list.call - 1]
20
- end
21
-
22
- # Initializes a new +InteractivePrompt+
23
- # Usually called from +self.call+
24
- #
25
- # ==== Example Usage:
26
- #
27
- # Dev::UI::InteractivePrompt.new(%w(rails go python))
28
- #
29
- def initialize(options)
30
- @options = options
31
- @active = 1
32
- @marker = '>'
33
- @answer = nil
34
- @state = :root
35
- end
36
-
37
- # Calls the +InteractivePrompt+ and asks the question
38
- # Usually used from +self.call+
39
- #
40
- def call
41
- Dev::UI.raw { print(ANSI.hide_cursor) }
42
- while @answer.nil?
43
- render_options
44
- wait_for_user_input
45
-
46
- # This will put us back at the beginning of the options
47
- # When we redraw the options, they will be overwritten
48
- Dev::UI.raw do
49
- num_lines = @options.join("\n").split("\n").reject(&:empty?).size
50
- num_lines.times { print(ANSI.previous_line) }
51
- print(ANSI.previous_line + ANSI.end_of_line + "\n")
52
- end
53
- end
54
- render_options
55
- @answer
56
- ensure
57
- Dev::UI.raw do
58
- print(ANSI.show_cursor)
59
- puts(ANSI.previous_line + ANSI.end_of_line)
60
- end
61
- end
62
-
63
- private
64
-
65
- ESC = "\e"
66
-
67
- def up
68
- @active = @active - 1 >= 1 ? @active - 1 : @options.length
69
- end
70
-
71
- def down
72
- @active = @active + 1 <= @options.length ? @active + 1 : 1
73
- end
74
-
75
- def select_n(n)
76
- @active = n
77
- @answer = n
78
- end
79
-
80
- def select_bool(char)
81
- return unless (@options - %w(yes no)).empty?
82
- opt = @options.detect { |o| o.start_with?(char) }
83
- @active = @options.index(opt) + 1
84
- @answer = @options.index(opt) + 1
85
- end
86
-
87
- # rubocop:disable Style/WhenThen,Layout/SpaceBeforeSemicolon
88
- def wait_for_user_input
89
- char = read_char
90
- case @state
91
- when :root
92
- case char
93
- when ESC ; @state = :esc
94
- when 'k' ; up
95
- when 'j' ; down
96
- when ('1'..@options.size.to_s) ; select_n(char.to_i)
97
- when 'y', 'n' ; select_bool(char)
98
- when " ", "\r", "\n" ; @answer = @active # <enter>
99
- when "\u0003" ; raise Interrupt # Ctrl-c
100
- end
101
- when :esc
102
- case char
103
- when '[' ; @state = :esc_bracket
104
- else ; raise Interrupt # unhandled escape sequence.
105
- end
106
- when :esc_bracket
107
- @state = :root
108
- case char
109
- when 'A' ; up
110
- when 'B' ; down
111
- else ; raise Interrupt # unhandled escape sequence.
112
- end
113
- end
114
- end
115
- # rubocop:enable Style/WhenThen,Layout/SpaceBeforeSemicolon
116
-
117
- def read_char
118
- raw_tty! { $stdin.getc.chr }
119
- rescue IOError
120
- "\e"
121
- end
122
-
123
- def raw_tty!
124
- if ENV['TEST'] || !$stdin.tty?
125
- yield
126
- else
127
- $stdin.raw { yield }
128
- end
129
- end
130
-
131
- def render_options
132
- @options.each_with_index do |choice, index|
133
- num = index + 1
134
- message = " #{num}."
135
- message += choice.split("\n").map { |l| " {{bold:#{l}}}" }.join("\n")
136
-
137
- if num == @active
138
- message = message.split("\n").map.with_index do |l, idx|
139
- idx == 0 ? "{{blue:> #{l.strip}}}" : "{{blue:>#{l.strip}}}"
140
- end.join("\n")
141
- end
142
-
143
- Dev::UI.with_frame_color(:blue) do
144
- puts Dev::UI.fmt(message)
145
- end
146
- end
147
- end
148
- end
149
- end
150
- end