howzit 2.0.8 → 2.0.11

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/lib/howzit/prompt.rb CHANGED
@@ -4,34 +4,61 @@ module Howzit
4
4
  # Command line prompt utils
5
5
  module Prompt
6
6
  class << self
7
- def yn(prompt, default: true)
7
+
8
+ ##
9
+ ## Display and read a Yes/No prompt
10
+ ##
11
+ ## @param prompt [String] The prompt string
12
+ ## @param default [Boolean] default value if
13
+ ## return is pressed or prompt is
14
+ ## skipped
15
+ ##
16
+ ## @return [Boolean] result
17
+ ##
18
+ def yn(prompt, default: true)
8
19
  return default unless $stdout.isatty
9
20
 
10
21
  return default if Howzit.options[:default]
11
22
 
12
- system 'stty cbreak'
23
+ tty_state = `stty -g`
24
+ system 'stty raw -echo cbreak isig'
13
25
  yn = color_single_options(default ? %w[Y n] : %w[y N])
14
26
  $stdout.syswrite "\e[1;37m#{prompt} #{yn}\e[1;37m? \e[0m"
15
27
  res = $stdin.sysread 1
16
28
  res.chomp!
17
29
  puts
18
30
  system 'stty cooked'
31
+ system "stty #{tty_state}"
19
32
  res.empty? ? default : res =~ /y/i
20
33
  end
21
34
 
35
+ ##
36
+ ## Helper function to colorize the Y/N prompt
37
+ ##
38
+ ## @param choices [Array] The choices with
39
+ ## default capitalized
40
+ ##
41
+ ## @return [String] colorized string
42
+ ##
22
43
  def color_single_options(choices = %w[y n])
23
44
  out = []
24
45
  choices.each do |choice|
25
46
  case choice
26
47
  when /[A-Z]/
27
- out.push(Color.template("{bg}#{choice}{xg}"))
48
+ out.push(Color.template("{bw}#{choice}{x}"))
28
49
  else
29
- out.push(Color.template("{w}#{choice}"))
50
+ out.push(Color.template("{dw}#{choice}{xg}"))
30
51
  end
31
52
  end
32
- Color.template("{g}[#{out.join('/')}{g}]{x}")
53
+ Color.template("{xg}[#{out.join('/')}{xg}]{x}")
33
54
  end
34
55
 
56
+ ##
57
+ ## Create a numbered list of options. Outputs directly
58
+ ## to console, returns nothing
59
+ ##
60
+ ## @param matches [Array] The list items
61
+ ##
35
62
  def options_list(matches)
36
63
  counter = 1
37
64
  puts
@@ -42,6 +69,15 @@ module Howzit
42
69
  puts
43
70
  end
44
71
 
72
+ ##
73
+ ## Choose from a list of items. If fzf is available,
74
+ ## uses that, otherwise generates its own list of
75
+ ## options and accepts a numeric response
76
+ ##
77
+ ## @param matches [Array] The options list
78
+ ##
79
+ ## @return [Array] the selected results
80
+ ##
45
81
  def choose(matches)
46
82
  if Util.command_exist?('fzf')
47
83
  settings = [
@@ -79,7 +115,7 @@ module Howzit
79
115
  end
80
116
  line = line == '' ? 1 : line.to_i
81
117
 
82
- return matches[line - 1] if line.positive? && line <= matches.length
118
+ return [matches[line - 1]] if line.positive? && line <= matches.length
83
119
 
84
120
  puts 'Out of range'
85
121
  options_list(matches)
@@ -3,7 +3,37 @@
3
3
  module Howzit
4
4
  # String Extensions
5
5
  module StringUtils
6
+ ##
7
+ ## Test if the filename matches the conditions to be a build note
8
+ ##
9
+ ## @return [Boolean] true if filename passes test
10
+ ##
11
+ def build_note?
12
+ return false if downcase !~ /^(howzit[^.]*|build[^.]+)/
13
+
14
+ return false if Howzit.config.should_ignore(self)
15
+
16
+ true
17
+ end
18
+
19
+ ##
20
+ ## Replace slash escaped characters in a string with a
21
+ ## zero-width space that will prevent a shell from
22
+ ## interpreting them when output to console
23
+ ##
24
+ ## @return [String] new string
25
+ ##
26
+ def preserve_escapes
27
+ gsub(/\\([a-z])/, '\​\1')
28
+ end
29
+
6
30
  # Convert a string to a valid YAML value
31
+ #
32
+ # @param orig_value The original value from which
33
+ # type will be determined
34
+ #
35
+ # @return coerced value
36
+ #
7
37
  def to_config_value(orig_value = nil)
8
38
  if orig_value
9
39
  case orig_value.class.to_s
@@ -28,10 +58,21 @@ module Howzit
28
58
  end
29
59
  end
30
60
 
61
+ ##
62
+ ## Shortcut for calling Color.template
63
+ ##
64
+ ## @return [String] colorized string
65
+ ##
31
66
  def c
32
67
  Color.template(self)
33
68
  end
34
69
 
70
+
71
+ ##
72
+ ## Convert a string to a regex object based on matching settings
73
+ ##
74
+ ## @return [Regexp] Receive regex representation of the object.
75
+ ##
35
76
  def to_rx
36
77
  case Howzit.options[:matching]
37
78
  when 'exact'
@@ -50,9 +91,17 @@ module Howzit
50
91
  gsub(/\e\[[\d;]+m/, '').gsub(/\e\]1337;SetMark/,'')
51
92
  end
52
93
 
94
+ # Wrap text at a specified width.
95
+ #
53
96
  # Adapted from https://github.com/pazdera/word_wrap/,
54
- # copyright (c) 2014, 2015 Radek Pazdera
55
- # Distributed under the MIT License
97
+ # copyright (c) 2014, 2015 Radek Pazdera Distributed
98
+ # under the MIT License
99
+ #
100
+ # @param width [Integer] The width at which to
101
+ # wrap lines
102
+ #
103
+ # @return [String] wrapped string
104
+ #
56
105
  def wrap(width)
57
106
  width ||= 80
58
107
  output = []
@@ -84,12 +133,19 @@ module Howzit
84
133
  output.join("\n")
85
134
  end
86
135
 
136
+ ##
137
+ ## Wrap string in place (destructive)
138
+ ##
139
+ ## @param width [Integer] The width at which to wrap
140
+ ##
87
141
  def wrap!(width)
88
142
  replace(wrap(width))
89
143
  end
90
144
 
91
145
  # Truncate string to nearest word
92
- # @param len <number> max length of string
146
+ #
147
+ # @param len [Integer] max length of string
148
+ #
93
149
  def trunc(len)
94
150
  split(/ /).each_with_object([]) do |x, ob|
95
151
  break ob unless ob.join(' ').length + ' '.length + x.length <= len
@@ -98,10 +154,21 @@ module Howzit
98
154
  end.join(' ').strip
99
155
  end
100
156
 
157
+ ##
158
+ ## Truncate string in place (destructive)
159
+ ##
160
+ ## @param len [Integer] The length to truncate at
161
+ ##
101
162
  def trunc!(len)
102
163
  replace trunc(len)
103
164
  end
104
165
 
166
+ ##
167
+ ## Splits a line at nearest word break
168
+ ##
169
+ ## @param width [Integer] The width of the first segment
170
+ ## @param indent [String] The indent string
171
+ ##
105
172
  def split_line(width, indent = '')
106
173
  line = dup
107
174
  at = line.index(/\s/)
@@ -119,10 +186,23 @@ module Howzit
119
186
  end
120
187
  end
121
188
 
189
+ ##
190
+ ## Test if an executable is available on the system
191
+ ##
192
+ ## @return [Boolean] executable is available
193
+ ##
122
194
  def available?
123
195
  Util.valid_command?(self)
124
196
  end
125
197
 
198
+ ##
199
+ ## Render [%variable] placeholders in a templated string
200
+ ##
201
+ ## @param vars [Hash] Key/value pairs of variable
202
+ ## values
203
+ ##
204
+ ## @return [String] Rendered string
205
+ ##
126
206
  def render_template(vars)
127
207
  vars.each do |k, v|
128
208
  gsub!(/\[%#{k}(:.*?)?\]/, v)
@@ -131,10 +211,20 @@ module Howzit
131
211
  gsub(/\[%(.*?):(.*?)\]/, '\2')
132
212
  end
133
213
 
214
+ ##
215
+ ## Render [%variable] placeholders in place
216
+ ##
217
+ ## @param vars [Hash] Key/value pairs of variable values
218
+ ##
134
219
  def render_template!(vars)
135
220
  replace render_template(vars)
136
221
  end
137
222
 
223
+ ##
224
+ ## Render $X placeholders based on positional arguments
225
+ ##
226
+ ## @return [String] rendered string
227
+ ##
138
228
  def render_arguments
139
229
  return self if Howzit.arguments.nil? || Howzit.arguments.empty?
140
230
 
@@ -145,15 +235,27 @@ module Howzit
145
235
  gsub(/\$[@*]/, Shellwords.join(Howzit.arguments))
146
236
  end
147
237
 
238
+ ##
239
+ ## Split the content at the first top-level header and
240
+ ## assume everything before it is metadata. Passes to
241
+ ## #get_metadata for processing
242
+ ##
243
+ ## @return [Hash] key/value pairs
244
+ ##
148
245
  def extract_metadata
149
246
  if File.exist?(self)
150
- leader = IO.read(self).split(/^#/)[0].strip
247
+ leader = Util.read_file(self).split(/^#/)[0].strip
151
248
  leader.length > 0 ? leader.get_metadata : {}
152
249
  else
153
250
  {}
154
251
  end
155
252
  end
156
253
 
254
+ ##
255
+ ## Examine text for multimarkdown-style metadata and return key/value pairs
256
+ ##
257
+ ## @return [Hash] The metadata as key/value pairs
258
+ ##
157
259
  def get_metadata
158
260
  data = {}
159
261
  scan(/(?mi)^(\S[\s\S]+?): ([\s\S]*?)(?=\n\S[\s\S]*?:|\Z)/).each do |m|
@@ -162,6 +264,13 @@ module Howzit
162
264
  normalize_metadata(data)
163
265
  end
164
266
 
267
+ ##
268
+ ## Autocorrect some keys
269
+ ##
270
+ ## @param meta [Hash] The metadata
271
+ ##
272
+ ## @return [Hash] corrected metadata
273
+ ##
165
274
  def normalize_metadata(meta)
166
275
  data = {}
167
276
  meta.each do |k, v|
@@ -177,15 +286,33 @@ module Howzit
177
286
  data
178
287
  end
179
288
 
289
+ ##
290
+ ## Test if iTerm markers should be output. Requires that
291
+ ## the $TERM_PROGRAM be iTerm and howzit is not running
292
+ ## directives or paginating output
293
+ ##
294
+ ## @return [Boolean] should mark?
295
+ ##
180
296
  def should_mark_iterm?
181
297
  ENV['TERM_PROGRAM'] =~ /^iTerm/ && !Howzit.options[:run] && !Howzit.options[:paginate]
182
298
  end
183
299
 
300
+ ##
301
+ ## Output an iTerm marker
302
+ ##
303
+ ## @return [String] ANSI escape sequence for iTerm
304
+ ## marker
305
+ ##
184
306
  def iterm_marker
185
307
  "\e]1337;SetMark\a" if should_mark_iterm?
186
308
  end
187
309
 
188
310
  # Make a fancy title line for the topic
311
+ #
312
+ # @param opts [Hash] options
313
+ #
314
+ # @return [String] formatted string
315
+ #
189
316
  def format_header(opts = {})
190
317
  title = dup
191
318
  options = {
data/lib/howzit/task.rb CHANGED
@@ -1,14 +1,18 @@
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
 
7
- def initialize(type, title, action, parent = nil, optional: false, default: true)
8
- @type = type
9
- @title = title
10
- @action = action.render_arguments
11
- @parent = parent
8
+ ##
9
+ ## Initialize a Task object
10
+ ##
11
+ def initialize(params, optional: false, default: true)
12
+ @type = params[:type]
13
+ @title = params[:title]
14
+ @action = params[:action].render_arguments
15
+ @parent = params[:parent] || nil
12
16
  @optional = optional
13
17
  @default = default
14
18
  end
@@ -22,7 +26,7 @@ module Howzit
22
26
  end
23
27
 
24
28
  def to_list
25
- " * #{@type}: #{@title.gsub(/\\n/, '\​n')}"
29
+ " * #{@type}: #{@title.preserve_escapes}"
26
30
  end
27
31
  end
28
32
  end
data/lib/howzit/topic.rb CHANGED
@@ -9,6 +9,12 @@ module Howzit
9
9
 
10
10
  attr_reader :title, :tasks, :prereqs, :postreqs
11
11
 
12
+ ##
13
+ ## Initialize a topic object
14
+ ##
15
+ ## @param title [String] The topic title
16
+ ## @param content [String] The raw topic content
17
+ ##
12
18
  def initialize(title, content)
13
19
  @title = title
14
20
  @content = content
@@ -17,18 +23,29 @@ module Howzit
17
23
  @tasks = gather_tasks
18
24
  end
19
25
 
26
+ ##
27
+ ## Search title and contents for a pattern
28
+ ##
29
+ ## @param term [String] the search pattern
30
+ ##
20
31
  def grep(term)
21
32
  @title =~ /#{term}/i || @content =~ /#{term}/i
22
33
  end
23
34
 
24
- # Handle run command, execute directives
35
+ # Handle run command, execute directives in topic
25
36
  def run(nested: false)
26
37
  output = []
27
38
  tasks = 0
39
+ cols = begin
40
+ TTY::Screen.columns > 60 ? 60 : TTY::Screen.columns
41
+ rescue StandardError
42
+ 60
43
+ end
44
+
28
45
  if @tasks.count.positive?
29
46
  unless @prereqs.empty?
30
- puts @prereqs.join("\n\n")
31
- res = Prompt.yn('This topic has prerequisites, have they been met?', default: true)
47
+ puts TTY::Box.frame("{by}#{@prereqs.join("\n\n").wrap(cols - 4)}{x}".c, width: cols)
48
+ res = Prompt.yn('Have the above prerequisites been met?', default: true)
32
49
  Process.exit 1 unless res
33
50
 
34
51
  end
@@ -85,11 +102,16 @@ module Howzit
85
102
  end
86
103
  output.push("{bm}Ran #{tasks} #{tasks == 1 ? 'task' : 'tasks'}{x}".c) if Howzit.options[:log_level] < 2 && !nested
87
104
 
88
- puts postreqs.join("\n\n") unless postreqs.empty?
105
+ puts TTY::Box.frame("{bw}#{@postreqs.join("\n\n").wrap(cols - 4)}{x}".c, width: cols) unless @postreqs.empty?
89
106
 
90
107
  output
91
108
  end
92
109
 
110
+ ##
111
+ ## Platform-agnostic copy-to-clipboard
112
+ ##
113
+ ## @param string [String] The string to copy
114
+ ##
93
115
  def os_copy(string)
94
116
  os = RbConfig::CONFIG['target_os']
95
117
  out = "{bg}Copying {bw}#{string}".c
@@ -114,6 +136,11 @@ module Howzit
114
136
  end
115
137
  end
116
138
 
139
+ ##
140
+ ## Platform-agnostic open command
141
+ ##
142
+ ## @param command [String] The command
143
+ ##
117
144
  def os_open(command)
118
145
  os = RbConfig::CONFIG['target_os']
119
146
  out = "{bg}Opening {bw}#{command}".c
@@ -136,6 +163,11 @@ module Howzit
136
163
  end
137
164
 
138
165
  # Output a topic with fancy title and bright white text.
166
+ #
167
+ # @param options [Hash] The options
168
+ #
169
+ # @return [Array] array of formatted lines
170
+ #
139
171
  def print_out(options = {})
140
172
  defaults = { single: false, header: true }
141
173
  opt = defaults.merge(options)
@@ -206,7 +238,7 @@ module Howzit
206
238
  "\u{279A}"
207
239
  end
208
240
 
209
- output.push("{bmK}#{icon} {bwK}#{title.gsub(/\\n/, '\​n')}{x}#{option}".c)
241
+ output.push("{bmK}#{icon} {bwK}#{title.preserve_escapes}{x}#{option}".c)
210
242
  when /(?<fence>`{3,})run(?<optional>[!?]{1,2})? *(?<title>.*?)$/i
211
243
  m = Regexp.last_match.named_captures.symbolize_keys
212
244
  optional = m[:optional] =~ /[?!]+/ ? true : false
@@ -239,6 +271,11 @@ module Howzit
239
271
 
240
272
  private
241
273
 
274
+ ##
275
+ ## Collect all directives in the topic content
276
+ ##
277
+ ## @return [Array] array of Task objects
278
+ ##
242
279
  def gather_tasks
243
280
  runnable = []
244
281
  @prereqs = @content.scan(/(?<=@before\n).*?(?=\n@end)/im).map(&:strip)
@@ -258,7 +295,12 @@ module Howzit
258
295
  default = c[:optional2] =~ /!/ ? false : true
259
296
  title = c[:title2].nil? ? '' : c[:title2].strip
260
297
  block = c[:block]&.strip
261
- runnable << Howzit::Task.new(:block, title, block, optional: optional, default: default)
298
+ runnable << Howzit::Task.new({ type: :block,
299
+ title: title,
300
+ action: block,
301
+ parent: nil },
302
+ optional: optional,
303
+ default: default)
262
304
  else
263
305
  cmd = c[:cmd]
264
306
  optional = c[:optional] =~ /[?!]{1,2}/ ? true : false
@@ -277,15 +319,35 @@ module Howzit
277
319
  # end
278
320
  # runnable.concat(tasks)
279
321
  # end
280
- runnable << Howzit::Task.new(:include, title, obj, optional: optional, default: default)
322
+ runnable << Howzit::Task.new({ type: :include,
323
+ title: title,
324
+ action: obj,
325
+ parent: nil },
326
+ optional: optional,
327
+ default: default)
281
328
  when /run/i
282
329
  # warn "{bg}Running {bw}#{obj}{x}".c if Howzit.options[:log_level] < 2
283
- runnable << Howzit::Task.new(:run, title, obj, optional: optional, default: default)
330
+ runnable << Howzit::Task.new({ type: :run,
331
+ title: title,
332
+ action: obj,
333
+ parent: nil },
334
+ optional: optional,
335
+ default: default)
284
336
  when /copy/i
285
337
  # warn "{bg}Copied {bw}#{obj}{bg} to clipboard{x}".c if Howzit.options[:log_level] < 2
286
- runnable << Howzit::Task.new(:copy, title, Shellwords.escape(obj), optional: optional, default: default)
338
+ runnable << Howzit::Task.new({ type: :copy,
339
+ title: title,
340
+ action: Shellwords.escape(obj),
341
+ parent: nil },
342
+ optional: optional,
343
+ default: default)
287
344
  when /open|url/i
288
- runnable << Howzit::Task.new(:open, title, obj, optional: optional, default: default)
345
+ runnable << Howzit::Task.new({ type: :open,
346
+ title: title,
347
+ action: obj,
348
+ parent: nil },
349
+ optional: optional,
350
+ default: default)
289
351
  end
290
352
  end
291
353
  end
data/lib/howzit/util.rb CHANGED
@@ -1,21 +1,51 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Howzit
4
+ # Util class
2
5
  module Util
3
6
  class << self
7
+
8
+ ##
9
+ ## Read a file with UTF-8 encoding and
10
+ ## leading/trailing whitespace removed
11
+ ##
12
+ ## @param path [String] The path to read
13
+ ##
14
+ ## @return [String] UTF-8 encoded string
15
+ ##
16
+ def read_file(path)
17
+ IO.read(path).force_encoding('utf-8').strip
18
+ end
19
+
20
+ ##
21
+ ## Test if an external command exists and is
22
+ ## executable. Removes additional arguments and passes
23
+ ## just the executable to #command_exist?
24
+ ##
25
+ ## @param command [String] The command
26
+ ##
27
+ ## @return [Boolean] command is valid
28
+ ##
4
29
  def valid_command?(command)
5
30
  cmd = command.split(' ')[0]
6
31
  command_exist?(cmd)
7
32
  end
8
-
33
+
34
+ ##
35
+ ## Test if external command exists
36
+ ##
37
+ ## @param command [String] The command
38
+ ##
39
+ ## @return [Boolean] command exists
40
+ ##
9
41
  def command_exist?(command)
10
42
  exts = ENV.fetch('PATHEXT', '').split(::File::PATH_SEPARATOR)
11
43
  if Pathname.new(command).absolute?
12
- ::File.exist?(command) ||
13
- exts.any? { |ext| ::File.exist?("#{command}#{ext}") }
44
+ ::File.exist?(command) || exts.any? { |ext| ::File.exist?("#{command}#{ext}") }
14
45
  else
15
46
  ENV.fetch('PATH', '').split(::File::PATH_SEPARATOR).any? do |dir|
16
47
  file = ::File.join(dir, command)
17
- ::File.exist?(file) ||
18
- exts.any? { |ext| ::File.exist?("#{file}#{ext}") }
48
+ ::File.exist?(file) || exts.any? { |ext| ::File.exist?("#{file}#{ext}") }
19
49
  end
20
50
  end
21
51
  end
@@ -3,5 +3,5 @@
3
3
  # Primary module for this gem.
4
4
  module Howzit
5
5
  # Current Howzit version.
6
- VERSION = '2.0.8'
6
+ VERSION = '2.0.11'
7
7
  end
data/lib/howzit.rb CHANGED
@@ -21,15 +21,28 @@ require 'tempfile'
21
21
  require 'yaml'
22
22
 
23
23
  require 'tty/screen'
24
+ require 'tty/box'
24
25
  # require 'tty/prompt'
25
26
 
27
+ # Main config dir
26
28
  CONFIG_DIR = '~/.config/howzit'
29
+
30
+ # Config file name
27
31
  CONFIG_FILE = 'howzit.yaml'
32
+
33
+ # Ignore file name
28
34
  IGNORE_FILE = 'ignore.yaml'
35
+
36
+ # Available options for matching method
29
37
  MATCHING_OPTIONS = %w[partial exact fuzzy beginswith].freeze
38
+
39
+ # Available options for multiple_matches method
30
40
  MULTIPLE_OPTIONS = %w[first best all choose].freeze
41
+
42
+ # Available options for header formatting
31
43
  HEADER_FORMAT_OPTIONS = %w[border block].freeze
32
44
 
45
+ # Main module for howzit
33
46
  module Howzit
34
47
  class << self
35
48
  attr_accessor :arguments, :cli_args
data/spec/cli_spec.rb ADDED
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ # https://github.com/thoiberg/cli-test
6
+ describe 'CLI' do
7
+ include CliTest
8
+
9
+ it 'executes successfully' do
10
+ execute_script('bin/howzit', use_bundler: true)
11
+ expect(last_execution).to be_successful
12
+ end
13
+
14
+ it 'lists available topics' do
15
+ execute_script('bin/howzit', use_bundler: true, args: %w[-L])
16
+ expect(last_execution).to be_successful
17
+ expect(last_execution.stdout).to match(/Topic Balogna/)
18
+ expect(last_execution.stdout.split(/\n/).count).to eq 3
19
+ end
20
+
21
+ it 'lists available tasks' do
22
+ execute_script('bin/howzit', use_bundler: true, args: %w[-T])
23
+ expect(last_execution).to be_successful
24
+ expect(last_execution.stdout).to match(/Topic Balogna/)
25
+ expect(last_execution.stdout.split(/\n/).count).to eq 2
26
+ end
27
+ end
data/spec/spec_helper.rb CHANGED
@@ -10,6 +10,7 @@
10
10
  # end
11
11
 
12
12
  require 'howzit'
13
+ require 'cli-test'
13
14
 
14
15
  RSpec.configure do |c|
15
16
  c.expect_with(:rspec) { |e| e.syntax = :expect }
data/spec/task_spec.rb CHANGED
@@ -3,7 +3,12 @@
3
3
  require 'spec_helper'
4
4
 
5
5
  describe Howzit::Task do
6
- subject(:task) { Howzit::Task.new(:run, 'List Directory', 'ls') }
6
+ subject(:task) do
7
+ Howzit::Task.new({ type: :run,
8
+ title: 'List Directory',
9
+ action: 'ls',
10
+ parent: nil })
11
+ end
7
12
 
8
13
  describe ".new" do
9
14
  it "makes a new task instance" do