howzit 2.0.8 → 2.0.11

Sign up to get free protection for your applications and to get access to all the features.
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