howzit 2.0.7 → 2.0.10

Sign up to get free protection for your applications and to get access to all the features.
data/howzit.gemspec CHANGED
@@ -30,6 +30,7 @@ Gem::Specification.new do |spec|
30
30
 
31
31
  spec.add_development_dependency 'rubocop', '~> 0.28'
32
32
  spec.add_development_dependency 'rspec', '~> 3.1'
33
+ spec.add_development_dependency 'cli-test', '~> 1.0'
33
34
  spec.add_development_dependency 'simplecov', '~> 0.9'
34
35
  # spec.add_development_dependency 'codecov', '~> 0.1'
35
36
  spec.add_development_dependency 'fuubar', '~> 2.0'
@@ -40,5 +41,6 @@ Gem::Specification.new do |spec|
40
41
 
41
42
  spec.add_runtime_dependency 'mdless', '~> 1.0', '>= 1.0.28'
42
43
  spec.add_runtime_dependency 'tty-screen', '~> 0.8'
44
+ spec.add_runtime_dependency 'tty-box', '~> 0.7'
43
45
  # spec.add_runtime_dependency 'tty-prompt', '~> 0.23'
44
46
  end
@@ -7,10 +7,27 @@ module Howzit
7
7
 
8
8
  attr_reader :metadata, :title
9
9
 
10
+ ##
11
+ ## Initialize a build note
12
+ ##
13
+ ## @param file [String] The path to the build note file
14
+ ## @param args [Array] additional args
15
+ ##
10
16
  def initialize(file: nil, args: [])
11
17
  @topics = []
12
- create_note if note_file.nil?
13
- @metadata = IO.read(note_file).split(/^#/)[0].strip.get_metadata
18
+ if note_file.nil?
19
+ res = Prompt.yn('No build notes file found, create one?', default: true)
20
+
21
+ create_note if res
22
+ Process.exit 0
23
+ end
24
+ content = Util.read_file(note_file)
25
+ if content.nil? || content.empty?
26
+ Howzit.console.error("{br}No content found in build note (#{note_file}){x}".c)
27
+ Process.exit 1
28
+ else
29
+ @metadata = content.split(/^#/)[0].strip.get_metadata
30
+ end
14
31
 
15
32
  read_help(file)
16
33
  end
@@ -19,14 +36,25 @@ module Howzit
19
36
  puts "#<Howzit::BuildNote @topics=[#{@topics.count}]>"
20
37
  end
21
38
 
39
+ ##
40
+ ## Public method to begin processing the build note based on command line options
41
+ ##
22
42
  def run
23
43
  process
24
44
  end
25
45
 
46
+ ##
47
+ ## Public method to open build note in editor
48
+ ##
26
49
  def edit
27
50
  edit_note
28
51
  end
29
52
 
53
+ ##
54
+ ## Find a topic based on a fuzzy match
55
+ ##
56
+ ## @param term [String] The search term
57
+ ##
30
58
  def find_topic(term)
31
59
  @topics.filter do |topic|
32
60
  rx = term.to_rx
@@ -34,11 +62,19 @@ module Howzit
34
62
  end
35
63
  end
36
64
 
65
+ ##
66
+ ## Call grep on all topics, filtering out those that don't match
67
+ ##
68
+ ## @param term [String] The search pattern
69
+ ##
37
70
  def grep(term)
38
71
  @topics.filter { |topic| topic.grep(term) }
39
72
  end
40
73
 
41
74
  # Output a list of topic titles
75
+ #
76
+ # @return [String] formatted list of topics in build note
77
+ #
42
78
  def list
43
79
  output = []
44
80
  output.push("{bg}Topics:{x}\n".c)
@@ -48,14 +84,32 @@ module Howzit
48
84
  output.join("\n")
49
85
  end
50
86
 
87
+
88
+ ##
89
+ ## Return an array of topic titles
90
+ ##
91
+ ## @return [Array] array of topic titles
92
+ ##
51
93
  def list_topics
52
94
  @topics.map { |topic| topic.title }
53
95
  end
54
96
 
97
+ ##
98
+ ## Return a list of topic titles suitable for shell completion
99
+ ##
100
+ ## @return [String] newline-separated list of topic titles
101
+ ##
55
102
  def list_completions
56
103
  list_topics.join("\n")
57
104
  end
58
105
 
106
+ ##
107
+ ## Return a list of topics containing @directives,
108
+ ## suitable for shell completion
109
+ ##
110
+ ## @return [String] newline-separated list of topic
111
+ ## titles
112
+ ##
59
113
  def list_runnable_completions
60
114
  output = []
61
115
  @topics.each do |topic|
@@ -64,6 +118,12 @@ module Howzit
64
118
  output.join("\n")
65
119
  end
66
120
 
121
+ ##
122
+ ## Return a formatted list of topics containing
123
+ ## @directives suitable for console output
124
+ ##
125
+ ## @return [String] formatted list
126
+ ##
67
127
  def list_runnable
68
128
  output = []
69
129
  output.push(%({bg}"Runnable" Topics:{x}\n).c)
@@ -82,6 +142,11 @@ module Howzit
82
142
  output.join("\n")
83
143
  end
84
144
 
145
+ ##
146
+ ## Read the help file contents
147
+ ##
148
+ ## @param file [String] The filepath
149
+ ##
85
150
  def read_file(file)
86
151
  read_help_file(file)
87
152
  end
@@ -89,7 +154,7 @@ module Howzit
89
154
  # Create a buildnotes skeleton
90
155
  def create_note
91
156
  trap('SIGINT') do
92
- Howzit.console.info "\nCanceled"
157
+ Howzit.console.info "\nCancelled"
93
158
  exit!
94
159
  end
95
160
  default = !$stdout.isatty || Howzit.options[:default]
@@ -210,19 +275,33 @@ module Howzit
210
275
  buildnotes.reverse
211
276
  end
212
277
 
213
- def is_build_notes(filename)
214
- return false if filename.downcase !~ /(^howzit[^.]*|build[^.]+)/
278
+ ##
279
+ ## Test if the filename matches the conditions to be a build note
280
+ ##
281
+ ## @param filename [String] The filename to test
282
+ ##
283
+ ## @return [Boolean] true if filename passes test
284
+ ##
285
+ def build_note?(filename)
286
+ return false if filename.downcase !~ /^(howzit[^.]*|build[^.]+)/
287
+
215
288
  return false if Howzit.config.should_ignore(filename)
289
+
216
290
  true
217
291
  end
218
292
 
293
+ ##
294
+ ## Glob current directory for valid build note filenames
295
+ ##
296
+ ## @return [String] file path
297
+ ##
219
298
  def glob_note
220
299
  filename = nil
221
300
  # Check for a build note file in the current folder. Filename must start
222
301
  # with "build" and have an extension of txt, md, or markdown.
223
302
 
224
303
  Dir.glob('*.{txt,md,markdown}').each do |f|
225
- if is_build_notes(f)
304
+ if build_note?(f)
226
305
  filename = f
227
306
  break
228
307
  end
@@ -230,6 +309,13 @@ module Howzit
230
309
  filename
231
310
  end
232
311
 
312
+ ##
313
+ ## Search for a valid build note, checking current
314
+ ## directory, git top level directory, and parent
315
+ ## directories
316
+ ##
317
+ ## @return [String] filepath
318
+ ##
233
319
  def find_note_file
234
320
  filename = glob_note
235
321
 
@@ -251,6 +337,11 @@ module Howzit
251
337
  File.expand_path(filename)
252
338
  end
253
339
 
340
+ ##
341
+ ## Search upstream directories for build notes
342
+ ##
343
+ ## @return [Array] array of build note paths
344
+ ##
254
345
  def read_upstream
255
346
  buildnotes = glob_upstream
256
347
 
@@ -261,16 +352,23 @@ module Howzit
261
352
  topics_dict
262
353
  end
263
354
 
355
+ ##
356
+ ## Test to ensure that any `required` metadata in a
357
+ ## template is fulfilled by the build note
358
+ ##
359
+ ## @param template [String] The template to read
360
+ ## from
361
+ ##
264
362
  def ensure_requirements(template)
265
- t_leader = IO.read(template).split(/^#/)[0].strip
363
+ t_leader = Util.read_file(template).split(/^#/)[0].strip
266
364
  if t_leader.length > 0
267
365
  t_meta = t_leader.get_metadata
268
366
  if t_meta.key?('required')
269
367
  required = t_meta['required'].strip.split(/\s*,\s*/)
270
368
  required.each do |req|
271
369
  unless @metadata.keys.include?(req.downcase)
272
- Howzit.console.error %({xr}ERROR: Missing required metadata key from template '{bw}#{File.basename(template, '.md')}{xr}'{x}).c
273
- Howzit.console.error %({xr}Please define {by}#{req.downcase}{xr} in build notes{x}).c
370
+ Howzit.console.error %({bRw}ERROR:{xbr} Missing required metadata key from template '{bw}#{File.basename(template, '.md')}{xr}'{x}).c
371
+ Howzit.console.error %({br}Please define {by}#{req.downcase}{xr} in build notes{x}).c
274
372
  Process.exit 1
275
373
  end
276
374
  end
@@ -278,6 +376,11 @@ module Howzit
278
376
  end
279
377
  end
280
378
 
379
+ ##
380
+ ## Read a list of topics from an included template
381
+ ##
382
+ ## @param content [String] The template contents
383
+ ##
281
384
  def get_template_topics(content)
282
385
  leader = content.split(/^#/)[0].strip
283
386
 
@@ -325,12 +428,18 @@ module Howzit
325
428
  template_topics
326
429
  end
327
430
 
328
- def include_file(m)
329
- file = File.expand_path(m[1])
431
+ ##
432
+ ## Import the contents of a filename as new topics
433
+ ##
434
+ ## @param mtch [MatchData] the filename match from
435
+ ## the include directive
436
+ ##
437
+ def include_file(mtch)
438
+ file = File.expand_path(mtch[1])
330
439
 
331
- return m[0] unless File.exist?(file)
440
+ return mtch[0] unless File.exist?(file)
332
441
 
333
- content = IO.read(file)
442
+ content = Util.read_file(file)
334
443
  home = ENV['HOME']
335
444
  short_path = File.dirname(file.sub(/^#{home}/, '~'))
336
445
  prefix = "#{short_path}/#{File.basename(file)}:"
@@ -343,8 +452,13 @@ module Howzit
343
452
  end
344
453
  end
345
454
 
455
+ ##
456
+ ## Get the title of the build note (top level header)
457
+ ##
458
+ ## @param truncate [Integer] Truncate to width
459
+ ##
346
460
  def note_title(truncate = 0)
347
- help = IO.read(note_file).strip
461
+ help = Util.read_file(note_file)
348
462
  title = help.match(/(?:^(\S.*?)(?=\n==)|^# ?(.*?)$)/)
349
463
  title = if title
350
464
  title[1].nil? ? title[2] : title[1]
@@ -355,13 +469,24 @@ module Howzit
355
469
  title && truncate.positive? ? title.trunc(truncate) : title
356
470
  end
357
471
 
358
- # Read in the build notes file and output a hash of "Title" => contents
472
+ # Read in the build notes file and output a hash of
473
+ # "Title" => contents
474
+ #
475
+ # @param path [String] The build note path
476
+ #
477
+ # @return [Array] array of Topics
478
+ #
359
479
  def read_help_file(path = nil)
360
480
  topics = []
361
481
 
362
482
  filename = path.nil? ? note_file : path
363
483
 
364
- help = IO.read(filename)
484
+ help = Util.read_file(filename)
485
+
486
+ if help.nil? || help.empty?
487
+ Howzit.console.error("{br}No content found in #{filename}{x}".c)
488
+ Process.exit 1
489
+ end
365
490
 
366
491
  @title = note_title
367
492
 
@@ -401,6 +526,11 @@ module Howzit
401
526
  topics
402
527
  end
403
528
 
529
+ ##
530
+ ## Read build note and include upstream topics
531
+ ##
532
+ ## @param path [String] The build note path
533
+ ##
404
534
  def read_help(path = nil)
405
535
  @topics = read_help_file(path)
406
536
  return unless path.nil? && Howzit.options[:include_upstream]
@@ -410,8 +540,17 @@ module Howzit
410
540
  upstream_topics.each do |topic|
411
541
  @topics.push(topic) unless find_topic(title.sub(/^.+:/, '')).count.positive?
412
542
  end
543
+
544
+ if note_file && @topics.empty?
545
+ Howzit.console.error("{br}Note file found but no topics detected in #{note_file}{x}".c)
546
+ Process.exit 1
547
+ end
548
+
413
549
  end
414
550
 
551
+ ##
552
+ ## Open build note in editor
553
+ ##
415
554
  def edit_note
416
555
  editor = Howzit.options.fetch(:editor, ENV['EDITOR'])
417
556
 
@@ -429,7 +568,16 @@ module Howzit
429
568
  end
430
569
  end
431
570
 
432
- def process_topic(topic, run, single = false)
571
+ ##
572
+ ## Run or print a topic
573
+ ##
574
+ ## @param topic [Topic] The topic
575
+ ## @param run [Boolean] execute directives if
576
+ ## true
577
+ ## @param single [Boolean] is being output as a
578
+ ## single topic
579
+ ##
580
+ def process_topic(topic, run, single: false)
433
581
  new_topic = topic.dup
434
582
 
435
583
  # Handle variable replacement
@@ -443,6 +591,9 @@ module Howzit
443
591
  output.nil? ? '' : output.join("\n")
444
592
  end
445
593
 
594
+ ##
595
+ ## Search and process the build note
596
+ ##
446
597
  def process
447
598
  output = []
448
599
 
@@ -535,10 +686,10 @@ module Howzit
535
686
 
536
687
  if !topic_matches.empty?
537
688
  # If we found a match
538
- topic_matches.each { |topic_match| output.push(process_topic(topic_match, Howzit.options[:run], true)) }
689
+ topic_matches.each { |topic_match| output.push(process_topic(topic_match, Howzit.options[:run], single: true)) }
539
690
  else
540
691
  # If there's no argument or no match found, output all
541
- topics.each { |k| output.push(process_topic(k, false, false)) }
692
+ topics.each { |k| output.push(process_topic(k, false, single: false)) }
542
693
  end
543
694
  Howzit.options[:paginate] = false if Howzit.options[:run]
544
695
  Util.show(output.join("\n").strip, Howzit.options)
data/lib/howzit/colors.rb CHANGED
@@ -4,7 +4,9 @@
4
4
  module Howzit
5
5
  # Terminal output color functions.
6
6
  module Color
7
+ # Regexp to match excape sequences
7
8
  ESCAPE_REGEX = /(?<=\[)(?:(?:(?:[349]|10)[0-9]|[0-9])?;?)+(?=m)/.freeze
9
+
8
10
  # All available color names. Available as methods and string extensions.
9
11
  #
10
12
  # @example Use a color as a method. Color reset will be added to end of string.
@@ -79,6 +81,7 @@ module Howzit
79
81
  [:default, '0;39']
80
82
  ].map(&:freeze).freeze
81
83
 
84
+ # Array of attribute keys only
82
85
  ATTRIBUTE_NAMES = ATTRIBUTES.transpose.first
83
86
 
84
87
  # Returns true if Howzit::Color supports the +feature+.
data/lib/howzit/config.rb CHANGED
@@ -3,6 +3,7 @@ module Howzit
3
3
  class Config
4
4
  attr_reader :options
5
5
 
6
+ # Configuration defaults
6
7
  DEFAULTS = {
7
8
  color: true,
8
9
  config_editor: ENV['EDITOR'] || nil,
@@ -22,18 +23,31 @@ module Howzit
22
23
  wrap: 0
23
24
  }.deep_freeze
24
25
 
26
+ ##
27
+ ## Initialize a config object
28
+ ##
25
29
  def initialize
26
30
  load_options
27
31
  end
28
32
 
33
+ ##
34
+ ## Write a config to a file
35
+ ##
36
+ ## @param config The configuration
37
+ ##
29
38
  def write_config(config)
30
39
  File.open(config_file, 'w') { |f| f.puts config.to_yaml }
31
40
  end
32
41
 
42
+ ##
43
+ ## Test if a file should be ignored based on YAML file
44
+ ##
45
+ ## @param filename The filename to test
46
+ ##
33
47
  def should_ignore(filename)
34
48
  return false unless File.exist?(ignore_file)
35
49
 
36
- @ignore_patterns ||= YAML.safe_load(IO.read(ignore_file))
50
+ @ignore_patterns ||= YAML.safe_load(Util.read_file(ignore_file))
37
51
 
38
52
  ignore = false
39
53
 
@@ -47,16 +61,29 @@ module Howzit
47
61
  ignore
48
62
  end
49
63
 
64
+ ##
65
+ ## Find the template folder
66
+ ##
67
+ ## @return [String] path to template folder
68
+ ##
50
69
  def template_folder
51
70
  File.join(config_dir, 'templates')
52
71
  end
53
72
 
73
+ ##
74
+ ## Initiate the editor for the config
75
+ ##
54
76
  def editor
55
77
  edit_config(DEFAULTS)
56
78
  end
57
79
 
58
80
  private
59
81
 
82
+ ##
83
+ ## Load command line options
84
+ ##
85
+ ## @return [Hash] options with command line flags merged in
86
+ ##
60
87
  def load_options
61
88
  Color.coloring = $stdout.isatty
62
89
  flags = {
@@ -77,40 +104,68 @@ module Howzit
77
104
  @options = flags.merge(config)
78
105
  end
79
106
 
107
+ ##
108
+ ## Get the config directory
109
+ ##
110
+ ## @return [String] path to config directory
111
+ ##
80
112
  def config_dir
81
113
  File.expand_path(CONFIG_DIR)
82
114
  end
83
115
 
116
+ ##
117
+ ## Get the config file
118
+ ##
119
+ ## @return [String] path to config file
120
+ ##
84
121
  def config_file
85
122
  File.join(config_dir, CONFIG_FILE)
86
123
  end
87
124
 
125
+ ##
126
+ ## Get the ignore config file
127
+ ##
128
+ ## @return [String] path to ignore config file
129
+ ##
88
130
  def ignore_file
89
131
  File.join(config_dir, IGNORE_FILE)
90
132
  end
91
133
 
92
- def create_config(d)
134
+ ##
135
+ ## Create a new config file (and directory if needed)
136
+ ##
137
+ ## @param default [Hash] default configuration to write
138
+ ##
139
+ def create_config(default)
93
140
  unless File.directory?(config_dir)
94
- Howzit.console.info "Creating config directory at #{config_dir}"
141
+ Howzit::ConsoleLogger.new(1).info "Creating config directory at #{config_dir}"
95
142
  FileUtils.mkdir_p(config_dir)
96
143
  end
97
144
 
98
145
  unless File.exist?(config_file)
99
- Howzit.console.info "Writing fresh config file to #{config_file}"
100
- write_config(d)
146
+ Howzit::ConsoleLogger.new(1).info "Writing fresh config file to #{config_file}"
147
+ write_config(default)
101
148
  end
102
149
  config_file
103
150
  end
104
151
 
152
+ ##
153
+ ## Load the config file
154
+ ##
155
+ ## @return [Hash] configuration object
156
+ ##
105
157
  def load_config
106
158
  file = create_config(DEFAULTS)
107
- config = YAML.load(IO.read(file))
159
+ config = YAML.load(Util.read_file(file))
108
160
  newconfig = config ? DEFAULTS.merge(config) : DEFAULTS
109
161
  write_config(newconfig)
110
162
  newconfig.dup
111
163
  end
112
164
 
113
- def edit_config(d)
165
+ ##
166
+ ## Open the config in an editor
167
+ ##
168
+ def edit_config
114
169
  editor = Howzit.options.fetch(:config_editor, ENV['EDITOR'])
115
170
 
116
171
  raise 'No config_editor defined' if editor.nil?
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Available log levels
3
4
  LOG_LEVELS = {
4
5
  debug: 0,
5
6
  info: 1,
@@ -12,30 +13,67 @@ module Howzit
12
13
  class ConsoleLogger
13
14
  attr_accessor :log_level
14
15
 
16
+ ##
17
+ ## Init the console logging object
18
+ ##
19
+ ## @param level [Integer] log level
20
+ ##
15
21
  def initialize(level = nil)
16
- @log_level = level || Howzit.options[:log_level]
22
+ @log_level = level.to_i || Howzit.options[:log_level]
17
23
  end
18
24
 
25
+ ##
26
+ ## Get the log level from options
27
+ ##
28
+ ## @return [Integer] log level
29
+ ##
19
30
  def reset_level
20
31
  @log_level = Howzit.options[:log_level]
21
32
  end
22
33
 
34
+ ##
35
+ ## Write a message to the console based on the urgency
36
+ ## level and user's log level setting
37
+ ##
38
+ ## @param msg [String] The message
39
+ ## @param level [Symbol] The level
40
+ ##
23
41
  def write(msg, level = :info)
24
42
  $stderr.puts msg if LOG_LEVELS[level] >= @log_level
25
43
  end
26
44
 
45
+ ##
46
+ ## Write a message at debug level
47
+ ##
48
+ ## @param msg The message
49
+ ##
27
50
  def debug(msg)
28
51
  write msg, :debug
29
52
  end
30
53
 
54
+ ##
55
+ ## Write a message at info level
56
+ ##
57
+ ## @param msg The message
58
+ ##
31
59
  def info(msg)
32
60
  write msg, :info
33
61
  end
34
62
 
63
+ ##
64
+ ## Write a message at warn level
65
+ ##
66
+ ## @param msg The message
67
+ ##
35
68
  def warn(msg)
36
69
  write msg, :warn
37
70
  end
38
71
 
72
+ ##
73
+ ## Write a message at error level
74
+ ##
75
+ ## @param msg The message
76
+ ##
39
77
  def error(msg)
40
78
  write msg, :error
41
79
  end
data/lib/howzit/hash.rb CHANGED
@@ -41,7 +41,7 @@ class ::Hash
41
41
  end
42
42
 
43
43
  def stringify_keys!
44
- replace stringify_keys
44
+ replace stringify_keys
45
45
  end
46
46
 
47
47
  # Turn all keys into symbols
@@ -50,6 +50,6 @@ class ::Hash
50
50
  end
51
51
 
52
52
  def symbolize_keys!
53
- replace symbolize_keys
53
+ replace symbolize_keys
54
54
  end
55
55
  end
data/lib/howzit/prompt.rb CHANGED
@@ -8,14 +8,15 @@ module Howzit
8
8
  return default unless $stdout.isatty
9
9
 
10
10
  return default if Howzit.options[:default]
11
-
12
- system 'stty cbreak'
11
+ tty_state = `stty -g`
12
+ system 'stty raw -echo cbreak isig'
13
13
  yn = color_single_options(default ? %w[Y n] : %w[y N])
14
14
  $stdout.syswrite "\e[1;37m#{prompt} #{yn}\e[1;37m? \e[0m"
15
15
  res = $stdin.sysread 1
16
16
  res.chomp!
17
17
  puts
18
18
  system 'stty cooked'
19
+ system "stty #{tty_state}"
19
20
  res.empty? ? default : res =~ /y/i
20
21
  end
21
22
 
@@ -24,12 +25,12 @@ module Howzit
24
25
  choices.each do |choice|
25
26
  case choice
26
27
  when /[A-Z]/
27
- out.push(Color.template("{bg}#{choice}{xg}"))
28
+ out.push(Color.template("{bw}#{choice}{x}"))
28
29
  else
29
- out.push(Color.template("{w}#{choice}"))
30
+ out.push(Color.template("{dw}#{choice}{xg}"))
30
31
  end
31
32
  end
32
- Color.template("{g}[#{out.join('/')}{g}]{x}")
33
+ Color.template("{xg}[#{out.join('/')}{xg}]{x}")
33
34
  end
34
35
 
35
36
  def options_list(matches)
@@ -147,7 +147,7 @@ module Howzit
147
147
 
148
148
  def extract_metadata
149
149
  if File.exist?(self)
150
- leader = IO.read(self).split(/^#/)[0].strip
150
+ leader = Util.read_file(self).split(/^#/)[0].strip
151
151
  leader.length > 0 ? leader.get_metadata : {}
152
152
  else
153
153
  {}
data/lib/howzit/task.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Howzit
4
+ # Task object
4
5
  class Task
5
6
  attr_reader :type, :title, :action, :parent, :optional, :default
6
7