howzit 1.2.13 → 1.2.16

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.
@@ -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