howzit 1.2.13 → 1.2.16

Sign up to get free protection for your applications and to get access to all the features.
@@ -124,7 +124,7 @@ module Howzit
124
124
  pipes = "|#{hl}" if hl
125
125
  end
126
126
 
127
- output = `echo #{Shellwords.escape(string.strip)}#{pipes}`
127
+ output = `echo #{Shellwords.escape(string.strip)}#{pipes}`.strip
128
128
 
129
129
  if @options[:paginate]
130
130
  page(output)
@@ -247,17 +247,22 @@ module Howzit
247
247
 
248
248
  options.merge!(opts)
249
249
 
250
- cols = TTY::Screen.columns
250
+ case @options[:header_format]
251
+ when :block
252
+ Color.template("#{options[:color]}\u{258C}#{title}#{should_mark_iterm? && options[:mark] ? iterm_marker : ''}{x}")
253
+ else
254
+ cols = TTY::Screen.columns
251
255
 
252
- cols = @options[:wrap] if (@options[:wrap]).positive? && cols > @options[:wrap]
253
- title = Color.template("#{options[:border]}#{options[:hr] * 2}( #{options[:color]}#{title}#{options[:border]} )")
256
+ cols = @options[:wrap] if (@options[:wrap]).positive? && cols > @options[:wrap]
257
+ title = Color.template("#{options[:border]}#{options[:hr] * 2}( #{options[:color]}#{title}#{options[:border]} )")
254
258
 
255
- tail = if should_mark_iterm?
256
- "#{options[:hr] * (cols - title.uncolor.length - 15)}#{options[:mark] ? iterm_marker : ''}"
257
- else
258
- options[:hr] * (cols - title.uncolor.length)
259
- end
260
- Color.template("#{title}#{tail}{x}")
259
+ tail = if should_mark_iterm?
260
+ "#{options[:hr] * (cols - title.uncolor.length - 15)}#{options[:mark] ? iterm_marker : ''}"
261
+ else
262
+ options[:hr] * (cols - title.uncolor.length)
263
+ end
264
+ Color.template("#{title}#{tail}{x}")
265
+ end
261
266
  end
262
267
 
263
268
  def os_open(command)
@@ -356,7 +361,9 @@ module Howzit
356
361
  end
357
362
  output.push("Ran #{tasks} #{tasks == 1 ? 'task' : 'tasks'}") if @options[:log_level] < 2
358
363
 
359
- puts postreqs.join("\n\n")
364
+ puts postreqs.join("\n\n") unless postreqs.empty?
365
+
366
+ output
360
367
  end
361
368
 
362
369
  # Output a topic with fancy title and bright white text.
@@ -382,12 +389,12 @@ module Howzit
382
389
  unless matches.empty?
383
390
  if opt[:single]
384
391
  title = "From #{matches[0]}:"
385
- color = '{yK}'
386
- rule = '{kK}'
392
+ color = '{Kyd}'
393
+ rule = '{kKd}'
387
394
  else
388
395
  title = "Include #{matches[0]}"
389
- color = '{yK}'
390
- rule = '{x}'
396
+ color = '{Kyd}'
397
+ rule = '{kKd}'
391
398
  end
392
399
  output.push(format_header("#{'> ' * @nest_level}#{title}", { color: color, hr: '.', border: rule })) unless @included.include?(matches[0])
393
400
 
@@ -666,7 +673,6 @@ module Howzit
666
673
  topics
667
674
  end
668
675
 
669
-
670
676
  def match_topic(search)
671
677
  matches = []
672
678
 
@@ -700,7 +706,8 @@ module Howzit
700
706
  choose: false,
701
707
  quiet: false,
702
708
  verbose: false,
703
- default: false
709
+ default: false,
710
+ grep: nil
704
711
  }
705
712
 
706
713
  defaults = {
@@ -715,7 +722,8 @@ module Howzit
715
722
  show_all_on_error: false,
716
723
  include_upstream: false,
717
724
  show_all_code: false,
718
- grep: nil,
725
+ multiple_matches: 'choose',
726
+ header_format: 'border',
719
727
  log_level: 1 # 0: debug, 1: info, 2: warn, 3: error
720
728
  }
721
729
 
@@ -767,6 +775,11 @@ module Howzit
767
775
  @options[:matching] = c
768
776
  end
769
777
 
778
+ opts.on('--multiple TYPE', MULTIPLE_OPTIONS,
779
+ 'Multiple result handling', "(#{MULTIPLE_OPTIONS.join(', ')}, default choose)") do |c|
780
+ @options[:multiple_matches] = c.to_sym
781
+ end
782
+
770
783
  opts.on('-R', '--list-runnable', 'List topics containing @ directives (verbose)') do
771
784
  @options[:list_runnable] = true
772
785
  end
@@ -808,6 +821,39 @@ module Howzit
808
821
  @options[:wrap] = w.to_i
809
822
  end
810
823
 
824
+ opts.on('--config-get [KEY]', 'Display the configuration settings or setting for a specific key') do |k|
825
+
826
+ if k.nil?
827
+ config.sort_by { |key, _| key }.each do |key, val|
828
+ print "#{key}: "
829
+ p val
830
+ end
831
+ else
832
+ k.sub!(/^:/, '')
833
+ if config.key?(k.to_sym)
834
+ puts config[k.to_sym]
835
+ else
836
+ puts "Key #{k} not found"
837
+ end
838
+ end
839
+ Process.exit 0
840
+ end
841
+
842
+ opts.on('--config-set KEY=VALUE', 'Set a config value (must be a valid key)') do |key|
843
+ raise 'Argument must be KEY=VALUE' unless key =~ /\S=\S/
844
+
845
+ k, v = key.split(/=/)
846
+ k.sub!(/^:/, '')
847
+
848
+ if config.key?(k.to_sym)
849
+ config[k.to_sym] = v.to_config_value(config[k.to_sym])
850
+ else
851
+ puts "Key #{k} not found"
852
+ end
853
+ write_config(config)
854
+ Process.exit 0
855
+ end
856
+
811
857
  opts.on('--edit-config', "Edit configuration file using default $EDITOR") do
812
858
  edit_config(defaults)
813
859
  Process.exit 0
@@ -841,6 +887,11 @@ module Howzit
841
887
  Process.exit 0
842
888
  end
843
889
 
890
+ opts.on('--header-format TYPE', HEADER_FORMAT_OPTIONS,
891
+ "Formatting style for topic titles (#{HEADER_FORMAT_OPTIONS.join(', ')})") do |t|
892
+ @options[:header_format] = t
893
+ end
894
+
844
895
  opts.on('--[no-]color', 'Colorize output (default on)') do |c|
845
896
  @options[:color] = c
846
897
  @options[:highlight] = false unless c
@@ -869,6 +920,9 @@ module Howzit
869
920
  end
870
921
  end.parse!(args)
871
922
 
923
+ @options[:multiple_matches] = @options[:multiple_matches].to_sym
924
+ @options[:header_format] = @options[:header_format].to_sym
925
+
872
926
  @cli_args = args
873
927
  end
874
928
 
@@ -1001,12 +1055,20 @@ module Howzit
1001
1055
 
1002
1056
  def choose(matches)
1003
1057
  if command_exist?('fzf')
1004
- res = `echo #{Shellwords.escape(matches.join("\n"))} | fzf -0 -1 --height #{matches.count + 2} --prompt 'Select a section > '`.strip
1058
+ settings = [
1059
+ '-0',
1060
+ '-1',
1061
+ '-m',
1062
+ "--height=#{matches.count + 2}",
1063
+ '--header="Use tab to mark multiple selections, enter to display/run"',
1064
+ '--prompt="Select a section > "'
1065
+ ]
1066
+ res = `echo #{Shellwords.escape(matches.join("\n"))} | fzf #{settings.join(' ')}`.strip
1005
1067
  if res.nil? || res.empty?
1006
1068
  warn 'Cancelled'
1007
1069
  Process.exit 0
1008
1070
  end
1009
- return res
1071
+ return res.split(/\n/)
1010
1072
  end
1011
1073
 
1012
1074
  res = matches[0..9]
@@ -1107,7 +1169,7 @@ module Howzit
1107
1169
  out = get_note_title(20)
1108
1170
  $stdout.print(out.strip)
1109
1171
  Process.exit(0)
1110
- elsif @options[:output_title]
1172
+ elsif @options[:output_title] && !@options[:run]
1111
1173
  title = get_note_title
1112
1174
  if title && !title.empty?
1113
1175
  header = format_header(title, { hr: "\u{2550}", color: '{bwK}' })
@@ -1136,33 +1198,49 @@ module Howzit
1136
1198
  Process.exit(0)
1137
1199
  end
1138
1200
 
1139
- topic_match = nil
1201
+ topic_matches = []
1140
1202
  if @options[:grep]
1141
- topic_match = choose(grep_topics(@options[:grep]))
1203
+ matches = grep_topics(@options[:grep])
1204
+ case @options[:multiple_matches]
1205
+ when :all
1206
+ topic_matches.concat(matches.sort)
1207
+ else
1208
+ topic_matches.concat(choose(matches))
1209
+ end
1142
1210
  elsif @options[:choose]
1143
- topic_match = choose(topics.keys)
1211
+ topic_matches.concat(choose(topics.keys))
1144
1212
  # If there are arguments use those to search for a matching topic
1145
1213
  elsif !@cli_args.empty?
1214
+ search = @cli_args.join(' ').strip.downcase.split(/ *, */).map(&:strip)
1146
1215
 
1147
- search = @cli_args.join(' ').strip.downcase
1148
- matches = match_topic(search)
1216
+ search.each do |s|
1217
+ matches = match_topic(s)
1149
1218
 
1150
- if matches.empty?
1151
- output.push(Color.template(%({bR}ERROR:{xr} No topic match found for {bw}#{search}{x}\n)))
1152
- unless @options[:show_all_on_error]
1153
- show(output.join("\n"), { color: true, highlight: false, paginate: false, wrap: 0 })
1154
- Process.exit 1
1219
+ if matches.empty?
1220
+ output.push(Color.template(%({bR}ERROR:{xr} No topic match found for {bw}#{s}{x}\n)))
1221
+ else
1222
+ case @options[:multiple_matches]
1223
+ when :first
1224
+ topic_matches.push(matches[0])
1225
+ when :best
1226
+ topic_matches.push(matches.sort.min_by(&:length))
1227
+ when :all
1228
+ topic_matches.concat(matches)
1229
+ else
1230
+ topic_matches.concat(choose(matches))
1231
+ end
1155
1232
  end
1156
- elsif matches.length == 1
1157
- topic_match = matches[0]
1158
- else
1159
- topic_match = choose(matches)
1233
+ end
1234
+
1235
+ if topic_matches.empty? && !@options[:show_all_on_error]
1236
+ show(output.join("\n"), { color: true, highlight: false, paginate: false, wrap: 0 })
1237
+ Process.exit 1
1160
1238
  end
1161
1239
  end
1162
1240
 
1163
- if topic_match
1241
+ if !topic_matches.empty?
1164
1242
  # If we found a match
1165
- output.push(process_topic(topic_match, @options[:run], true))
1243
+ topic_matches.each { |topic_match| output.push(process_topic(topic_match, @options[:run], true)) }
1166
1244
  else
1167
1245
  # If there's no argument or no match found, output all
1168
1246
  topics.each_key { |k| output.push(process_topic(k, false, false)) }
data/lib/howzit/colors.rb CHANGED
@@ -223,8 +223,8 @@ module Howzit
223
223
  ##
224
224
  def template(input)
225
225
  input = input.join(' ') if input.is_a? Array
226
- input.gsub!(/%/, '%%')
227
- fmt = input.gsub(/\{(\w+)\}/) do
226
+ fmt = input.gsub(/%/, '%%')
227
+ fmt = fmt.gsub(/\{(\w+)\}/) do
228
228
  Regexp.last_match(1).split('').map { |c| "%<#{c}>s" }.join('')
229
229
  end
230
230
 
@@ -232,7 +232,7 @@ module Howzit
232
232
  y: yellow, c: cyan, m: magenta, r: red,
233
233
  W: bgwhite, K: bgblack, G: bggreen, L: bgblue,
234
234
  Y: bgyellow, C: bgcyan, M: bgmagenta, R: bgred,
235
- b: bold, u: underline, i: italic, x: reset }
235
+ d: dark, b: bold, u: underline, i: italic, x: reset }
236
236
 
237
237
  format(fmt, colors)
238
238
  end
@@ -0,0 +1,128 @@
1
+ module Howzit
2
+ # Config Class
3
+ class Config
4
+ attr_reader :options
5
+
6
+ DEFAULTS = {
7
+ color: true,
8
+ config_editor: ENV['EDITOR'] || nil,
9
+ editor: ENV['EDITOR'] || nil,
10
+ header_format: 'border',
11
+ highlight: true,
12
+ highlighter: 'auto',
13
+ include_upstream: false,
14
+ log_level: 1, # 0: debug, 1: info, 2: warn, 3: error
15
+ matching: 'partial', # exact, partial, fuzzy, beginswith
16
+ multiple_matches: 'choose',
17
+ output_title: false,
18
+ pager: 'auto',
19
+ paginate: true,
20
+ show_all_code: false,
21
+ show_all_on_error: false,
22
+ wrap: 0
23
+ }.deep_freeze
24
+
25
+ def initialize
26
+ load_options
27
+ end
28
+
29
+ def write_config(config)
30
+ File.open(config_file, 'w') { |f| f.puts config.to_yaml }
31
+ end
32
+
33
+ def should_ignore(filename)
34
+ return false unless File.exist?(ignore_file)
35
+
36
+ @ignore_patterns ||= YAML.safe_load(IO.read(ignore_file))
37
+
38
+ ignore = false
39
+
40
+ @ignore_patterns.each do |pat|
41
+ if filename =~ /#{pat}/
42
+ ignore = true
43
+ break
44
+ end
45
+ end
46
+
47
+ ignore
48
+ end
49
+
50
+ def template_folder
51
+ File.join(config_dir, 'templates')
52
+ end
53
+
54
+ def editor
55
+ edit_config(DEFAULTS)
56
+ end
57
+
58
+ private
59
+
60
+ def load_options
61
+ Color.coloring = $stdout.isatty
62
+ flags = {
63
+ choose: false,
64
+ default: false,
65
+ grep: nil,
66
+ list_runnable: false,
67
+ list_runnable_titles: false,
68
+ list_topic_titles: false,
69
+ list_topics: false,
70
+ quiet: false,
71
+ run: false,
72
+ title_only: false,
73
+ verbose: false
74
+ }
75
+
76
+ config = load_config
77
+ @options = flags.merge(config)
78
+ end
79
+
80
+ def config_dir
81
+ File.expand_path(CONFIG_DIR)
82
+ end
83
+
84
+ def config_file
85
+ File.join(config_dir, CONFIG_FILE)
86
+ end
87
+
88
+ def ignore_file
89
+ File.join(config_dir, IGNORE_FILE)
90
+ end
91
+
92
+ def create_config(d)
93
+ unless File.directory?(config_dir)
94
+ warn "Creating config directory at #{config_dir}"
95
+ FileUtils.mkdir_p(config_dir)
96
+ end
97
+
98
+ unless File.exist?(config_file)
99
+ warn "Writing fresh config file to #{config_file}"
100
+ write_config(d)
101
+ end
102
+ config_file
103
+ end
104
+
105
+ def load_config
106
+ file = create_config(DEFAULTS)
107
+ config = YAML.load(IO.read(file))
108
+ newconfig = config ? DEFAULTS.merge(config) : DEFAULTS
109
+ write_config(newconfig)
110
+ newconfig.dup
111
+ end
112
+
113
+ def edit_config(d)
114
+ editor = Howzit.options.fetch(:config_editor, ENV['EDITOR'])
115
+
116
+ raise 'No config_editor defined' if editor.nil?
117
+
118
+ # raise "Invalid editor (#{editor})" unless Util.valid_command?(editor)
119
+
120
+ load_config
121
+ if Util.valid_command?(editor.split(/ /).first)
122
+ system %(#{editor} "#{config_file}")
123
+ else
124
+ `open -a "#{editor}" "#{config_file}"`
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Hash helpers
4
+ class ::Hash
5
+ ##
6
+ ## Freeze all values in a hash
7
+ ##
8
+ ## @return Hash with all values frozen
9
+ ##
10
+ def deep_freeze
11
+ chilled = {}
12
+ each do |k, v|
13
+ chilled[k] = v.is_a?(Hash) ? v.deep_freeze : v.freeze
14
+ end
15
+
16
+ chilled.freeze
17
+ end
18
+
19
+ def deep_freeze!
20
+ replace deep_thaw.deep_freeze
21
+ end
22
+
23
+ def deep_thaw
24
+ chilled = {}
25
+ each do |k, v|
26
+ chilled[k] = v.is_a?(Hash) ? v.deep_thaw : v.dup
27
+ end
28
+
29
+ chilled.dup
30
+ end
31
+
32
+ def deep_thaw!
33
+ replace deep_thaw
34
+ end
35
+ end
data/lib/howzit/prompt.rb CHANGED
@@ -3,17 +3,90 @@
3
3
  module Howzit
4
4
  # Command line prompt utils
5
5
  module Prompt
6
- def yn(prompt, default = true)
7
- return default if !$stdout.isatty
8
-
9
- system 'stty cbreak'
10
- yn = color_single_options(default ? %w[Y n] : %w[y N])
11
- $stdout.syswrite "\e[1;37m#{prompt} #{yn}\e[1;37m? \e[0m"
12
- res = $stdin.sysread 1
13
- res.chomp!
14
- puts
15
- system 'stty cooked'
16
- res =~ /y/i
6
+ class << self
7
+ def yn(prompt, default = true)
8
+ return default if !$stdout.isatty
9
+
10
+ system 'stty cbreak'
11
+ yn = color_single_options(default ? %w[Y n] : %w[y N])
12
+ $stdout.syswrite "\e[1;37m#{prompt} #{yn}\e[1;37m? \e[0m"
13
+ res = $stdin.sysread 1
14
+ res.chomp!
15
+ puts
16
+ system 'stty cooked'
17
+ res =~ /y/i
18
+ end
19
+
20
+ def color_single_options(choices = %w[y n])
21
+ out = []
22
+ choices.each do |choice|
23
+ case choice
24
+ when /[A-Z]/
25
+ out.push(Color.template("{bg}#{choice}{xg}"))
26
+ else
27
+ out.push(Color.template("{w}#{choice}"))
28
+ end
29
+ end
30
+ Color.template("{g}[#{out.join('/')}{g}]{x}")
31
+ end
32
+
33
+ def options_list(matches)
34
+ counter = 1
35
+ puts
36
+ matches.each do |match|
37
+ printf("%<counter>2d ) %<option>s\n", counter: counter, option: match)
38
+ counter += 1
39
+ end
40
+ puts
41
+ end
42
+
43
+ def choose(matches)
44
+ if Util.command_exist?('fzf')
45
+ settings = [
46
+ '-0',
47
+ '-1',
48
+ '-m',
49
+ "--height=#{matches.count + 2}",
50
+ '--header="Use tab to mark multiple selections, enter to display/run"',
51
+ '--prompt="Select a section > "'
52
+ ]
53
+ res = `echo #{Shellwords.escape(matches.join("\n"))} | fzf #{settings.join(' ')}`.strip
54
+ if res.nil? || res.empty?
55
+ warn 'Cancelled'
56
+ Process.exit 0
57
+ end
58
+ return res.split(/\n/)
59
+ end
60
+
61
+ res = matches[0..9]
62
+ stty_save = `stty -g`.chomp
63
+
64
+ trap('INT') do
65
+ system('stty', stty_save)
66
+ exit
67
+ end
68
+
69
+ options_list(matches)
70
+
71
+ begin
72
+ printf("Type 'q' to cancel, enter for first item", res.length)
73
+ while (line = Readline.readline(': ', true))
74
+ if line =~ /^[a-z]/i
75
+ system('stty', stty_save) # Restore
76
+ exit
77
+ end
78
+ line = line == '' ? 1 : line.to_i
79
+
80
+ return matches[line - 1] if line.positive? && line <= matches.length
81
+
82
+ puts 'Out of range'
83
+ options_list(matches)
84
+ end
85
+ rescue Interrupt
86
+ system('stty', stty_save)
87
+ exit
88
+ end
89
+ end
17
90
  end
18
91
  end
19
92
  end
@@ -3,6 +3,44 @@
3
3
  module Howzit
4
4
  # String Extensions
5
5
  module StringUtils
6
+ # Convert a string to a valid YAML value
7
+ def to_config_value(orig_value = nil)
8
+ if orig_value
9
+ case orig_value.class.to_s
10
+ when /Integer/
11
+ to_i
12
+ when /(True|False)Class/
13
+ self =~ /^(t(rue)?|y(es)?|1)$/i ? true : false
14
+ else
15
+ self
16
+ end
17
+ else
18
+ case self
19
+ when /^[0-9]+$/
20
+ to_i
21
+ when /^(t(rue)?|y(es)?)$/i
22
+ true
23
+ when /^(f(alse)?|n(o)?)$/i
24
+ false
25
+ else
26
+ self
27
+ end
28
+ end
29
+ end
30
+
31
+ def to_rx
32
+ case Howzit.options[:matching]
33
+ when 'exact'
34
+ /^#{self}$/i
35
+ when 'beginswith'
36
+ /^#{self}/i
37
+ when 'fuzzy'
38
+ /#{split(//).join('.*?')}/i
39
+ else
40
+ /#{self}/i
41
+ end
42
+ end
43
+
6
44
  # Just strip out color codes when requested
7
45
  def uncolor
8
46
  gsub(/\e\[[\d;]+m/, '').gsub(/\e\]1337;SetMark/,'')
@@ -78,20 +116,15 @@ module Howzit
78
116
  end
79
117
 
80
118
  def available?
81
- if File.exist?(File.expand_path(self))
82
- File.executable?(File.expand_path(self))
83
- else
84
- system "which #{self}", out: File::NULL
85
- end
119
+ Util.valid_command?(self)
86
120
  end
87
121
 
88
122
  def render_template(vars)
89
- content = dup
90
123
  vars.each do |k, v|
91
- content.gsub!(/\[%#{k}(:.*?)?\]/, v)
124
+ gsub!(/\[%#{k}(:.*?)?\]/, v)
92
125
  end
93
126
 
94
- content.gsub(/\[%(.*?):(.*?)\]/, '\2')
127
+ gsub(/\[%(.*?):(.*?)\]/, '\2')
95
128
  end
96
129
 
97
130
  def render_template!(vars)
@@ -129,6 +162,44 @@ module Howzit
129
162
  end
130
163
  data
131
164
  end
165
+
166
+ def should_mark_iterm?
167
+ ENV['TERM_PROGRAM'] =~ /^iTerm/ && !Howzit.options[:run] && !Howzit.options[:paginate]
168
+ end
169
+
170
+ def iterm_marker
171
+ "\e]1337;SetMark\a" if should_mark_iterm?
172
+ end
173
+
174
+ # Make a fancy title line for the topic
175
+ def format_header(opts = {})
176
+ title = dup
177
+ options = {
178
+ hr: "\u{254C}",
179
+ color: '{bg}',
180
+ border: '{x}',
181
+ mark: should_mark_iterm?
182
+ }
183
+
184
+ options.merge!(opts)
185
+
186
+ case Howzit.options[:header_format]
187
+ when :block
188
+ Color.template("#{options[:color]}\u{258C}#{title}#{should_mark_iterm? && options[:mark] ? iterm_marker : ''}{x}")
189
+ else
190
+ cols = TTY::Screen.columns
191
+
192
+ cols = Howzit.options[:wrap] if (Howzit.options[:wrap]).positive? && cols > Howzit.options[:wrap]
193
+ title = Color.template("#{options[:border]}#{options[:hr] * 2}( #{options[:color]}#{title}#{options[:border]} )")
194
+
195
+ tail = if should_mark_iterm?
196
+ "#{options[:hr] * (cols - title.uncolor.length - 15)}#{options[:mark] ? iterm_marker : ''}"
197
+ else
198
+ options[:hr] * (cols - title.uncolor.length)
199
+ end
200
+ Color.template("#{title}#{tail}{x}")
201
+ end
202
+ end
132
203
  end
133
204
  end
134
205
 
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Howzit
4
+ class Task
5
+ attr_reader :type, :title, :action, :parent
6
+
7
+ def initialize(type, title, action, parent = nil)
8
+ @type = type
9
+ @title = title
10
+ @action = action
11
+ @parent = parent
12
+ end
13
+
14
+ def to_s
15
+ @title
16
+ end
17
+
18
+ def to_list
19
+ " * #{@type}: #{@title}"
20
+ end
21
+ end
22
+ end