howzit 2.0.7 → 2.0.10

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