howzit 1.2.15 → 1.2.18

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.empty? ? default : 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 + 3}",
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
@@ -28,6 +28,19 @@ module Howzit
28
28
  end
29
29
  end
30
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
+
31
44
  # Just strip out color codes when requested
32
45
  def uncolor
33
46
  gsub(/\e\[[\d;]+m/, '').gsub(/\e\]1337;SetMark/,'')
@@ -103,20 +116,15 @@ module Howzit
103
116
  end
104
117
 
105
118
  def available?
106
- if File.exist?(File.expand_path(self))
107
- File.executable?(File.expand_path(self))
108
- else
109
- system "which #{self}", out: File::NULL
110
- end
119
+ Util.valid_command?(self)
111
120
  end
112
121
 
113
122
  def render_template(vars)
114
- content = dup
115
123
  vars.each do |k, v|
116
- content.gsub!(/\[%#{k}(:.*?)?\]/, v)
124
+ gsub!(/\[%#{k}(:.*?)?\]/, v)
117
125
  end
118
126
 
119
- content.gsub(/\[%(.*?):(.*?)\]/, '\2')
127
+ gsub(/\[%(.*?):(.*?)\]/, '\2')
120
128
  end
121
129
 
122
130
  def render_template!(vars)
@@ -154,6 +162,44 @@ module Howzit
154
162
  end
155
163
  data
156
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
157
203
  end
158
204
  end
159
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
@@ -0,0 +1,235 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Howzit
4
+ # Topic Class
5
+ class Topic
6
+ attr_writer :parent
7
+
8
+ attr_accessor :content
9
+
10
+ attr_reader :title, :tasks, :prereqs, :postreqs
11
+
12
+ def initialize(title, content)
13
+ @title = title
14
+ @content = content
15
+ @parent = nil
16
+ @nest_level = 0
17
+ @tasks = gather_tasks
18
+ end
19
+
20
+ def grep(term)
21
+ @title =~ /#{term}/i || @content =~ /#{term}/i
22
+ end
23
+
24
+ # Handle run command, execute directives
25
+ def run(nested: false)
26
+ output = []
27
+ tasks = 0
28
+ if @tasks.count.positive?
29
+ unless @prereqs.empty?
30
+ puts @prereqs.join("\n\n")
31
+ res = Prompt.yn('This task has prerequisites, have they been met?', true)
32
+ Process.exit 1 unless res
33
+
34
+ end
35
+
36
+ @tasks.each do |task|
37
+ if task.type == :block
38
+ warn Color.template("{bg}Running block {bw}#{title}{x}") if Howzit.options[:log_level] < 2
39
+ block = task.action
40
+ script = Tempfile.new('howzit_script')
41
+ begin
42
+ script.write(block)
43
+ script.close
44
+ File.chmod(0777, script.path)
45
+ system(%(/bin/sh -c "#{script.path}"))
46
+ tasks += 1
47
+ ensure
48
+ script.close
49
+ script.unlink
50
+ end
51
+ else
52
+ case task.type
53
+ when :include
54
+ matches = Howzit.buildnote.find_topic(task.action)
55
+ raise "Topic not found: #{task.action}" if matches.empty?
56
+
57
+ warn Color.template("{by}Running tasks from {bw}#{matches[0].title}{x}") if Howzit.options[:log_level] < 2
58
+ output.push(matches[0].run(nested: true))
59
+ warn Color.template("{by}End include: #{matches[0].tasks.count} tasks") if Howzit.options[:log_level] < 2
60
+ tasks += matches[0].tasks.count
61
+ when :run
62
+ warn Color.template("{bg}Running {bw}#{task.title}{x}") if Howzit.options[:log_level] < 2
63
+ system(task.action)
64
+ tasks += 1
65
+ when :copy
66
+ warn Color.template("{bg}Copied {bw}#{task.title}{bg} to clipboard{x}") if Howzit.options[:log_level] < 2
67
+ `echo #{Shellwords.escape(task.action)}'\\c'|pbcopy`
68
+ tasks += 1
69
+ when :open
70
+ os_open(task.action)
71
+ tasks += 1
72
+ end
73
+ end
74
+ end
75
+ else
76
+ warn Color.template("{r}--run: No {br}@directive{xr} found in {bw}#{key}{x}")
77
+ end
78
+ output.push(Color.template("{bm}Ran #{tasks} #{tasks == 1 ? 'task' : 'tasks'}{x}")) if Howzit.options[:log_level] < 2 && !nested
79
+
80
+ puts postreqs.join("\n\n") unless postreqs.empty?
81
+
82
+ output
83
+ end
84
+
85
+ def os_open(command)
86
+ os = RbConfig::CONFIG['target_os']
87
+ out = Color.template("{bg}Opening {bw}#{command}")
88
+ case os
89
+ when /darwin.*/i
90
+ warn Color.template("#{out} (macOS){x}") if Howzit.options[:log_level] < 2
91
+ `open #{Shellwords.escape(command)}`
92
+ when /mingw|mswin/i
93
+ warn Color.template("#{out} (Windows){x}") if Howzit.options[:log_level] < 2
94
+ `start #{Shellwords.escape(command)}`
95
+ else
96
+ if 'xdg-open'.available?
97
+ warn Color.template("#{out} (Linux){x}") if Howzit.options[:log_level] < 2
98
+ `xdg-open #{Shellwords.escape(command)}`
99
+ else
100
+ warn out if Howzit.options[:log_level] < 2
101
+ warn 'Unable to determine executable for `open`.'
102
+ end
103
+ end
104
+ end
105
+
106
+ # Output a topic with fancy title and bright white text.
107
+ def print_out(options = {})
108
+ defaults = { single: false, header: true }
109
+ opt = defaults.merge(options)
110
+
111
+ output = []
112
+ if opt[:header]
113
+ output.push(@title.format_header)
114
+ output.push('')
115
+ end
116
+ topic = @content.dup
117
+ topic.gsub!(/(?mi)^(`{3,})run *([^\n]*)[\s\S]*?\n\1\s*$/, '@@@run \2') unless Howzit.options[:show_all_code]
118
+ topic.split(/\n/).each do |l|
119
+ case l
120
+ when /@(before|after|prereq|end)/
121
+ next
122
+ when /@include\((.*?)\)/
123
+
124
+ m = Regexp.last_match
125
+ matches = Howzit.buildnote.find_topic(m[1])
126
+ unless matches.empty?
127
+ if opt[:single]
128
+ title = "From #{matches[0].title}:"
129
+ color = '{Kyd}'
130
+ rule = '{kKd}'
131
+ else
132
+ title = "Include #{matches[0].title}"
133
+ color = '{Kyd}'
134
+ rule = '{kKd}'
135
+ end
136
+ unless Howzit.inclusions.include?(matches[0])
137
+ output.push("#{'> ' * @nest_level}#{title}".format_header({ color: color, hr: '.', border: rule }))
138
+ end
139
+
140
+ if opt[:single]
141
+ if Howzit.inclusions.include?(matches[0])
142
+ output.push("#{'> ' * @nest_level}#{title} included above".format_header({
143
+ color: color, hr: '.', border: rule }))
144
+ else
145
+ @nest_level += 1
146
+ output.concat(matches[0].print_out({ single: true, header: false }))
147
+ @nest_level -= 1
148
+ end
149
+ unless Howzit.inclusions.include?(matches[0])
150
+ output.push("#{'> ' * @nest_level}...".format_header({ color: color, hr: '.', border: rule }))
151
+ end
152
+ end
153
+ Howzit.inclusions.push(matches[0])
154
+ end
155
+
156
+ when /@(run|copy|open|url|include)\((.*?)\)/
157
+ m = Regexp.last_match
158
+ cmd = m[1]
159
+ obj = m[2]
160
+ icon = case cmd
161
+ when 'run'
162
+ "\u{25B6}"
163
+ when 'copy'
164
+ "\u{271A}"
165
+ when /open|url/
166
+ "\u{279A}"
167
+ end
168
+
169
+ output.push(Color.template("{bmK}#{icon} {bwK}#{obj.gsub(/\\n/, '\​n')}{x}"))
170
+ when /(`{3,})run *(.*?)$/i
171
+ m = Regexp.last_match
172
+ desc = m[2].length.positive? ? "Block: #{m[2]}" : 'Code Block'
173
+ output.push(Color.template("{bmK}\u{25B6} {bwK}#{desc}{x}\n```"))
174
+ when /@@@run *(.*?)$/i
175
+ m = Regexp.last_match
176
+ desc = m[1].length.positive? ? "Block: #{m[1]}" : 'Code Block'
177
+ output.push(Color.template("{bmK}\u{25B6} {bwK}#{desc}{x}"))
178
+ else
179
+ l.wrap!(Howzit.options[:wrap]) if (Howzit.options[:wrap]).positive?
180
+ output.push(l)
181
+ end
182
+ end
183
+ output.push('') # FIXME: Is this where the extra line is coming from?
184
+ end
185
+
186
+ private
187
+
188
+ def gather_tasks
189
+ runnable = []
190
+ @prereqs = @content.scan(/(?<=@before\n).*?(?=\n@end)/im).map(&:strip)
191
+ @postreqs = @content.scan(/(?<=@after\n).*?(?=\n@end)/im).map(&:strip)
192
+
193
+ rx = /(?:@(include|run|copy|open|url)\((.*?)\) *(.*?)(?=$)|(`{3,})run(?: +([^\n]+))?(.*?)\4)/mi
194
+ directives = @content.scan(rx)
195
+
196
+ directives.each do |c|
197
+ if c[0].nil?
198
+ title = c[4] ? c[4].strip : ''
199
+ block = c[5].strip
200
+ runnable << Howzit::Task.new(:block, title, block)
201
+ else
202
+ cmd = c[0]
203
+ obj = c[1]
204
+ title = c[3] || obj
205
+
206
+ case cmd
207
+ when /include/i
208
+ # matches = Howzit.buildnote.find_topic(obj)
209
+ # unless matches.empty? || Howzit.inclusions.include?(matches[0].title)
210
+ # tasks = matches[0].tasks.map do |inc|
211
+ # Howzit.inclusions.push(matches[0].title)
212
+ # inc.parent = matches[0]
213
+ # inc
214
+ # end
215
+ # runnable.concat(tasks)
216
+ # end
217
+ title = c[3] || obj
218
+ runnable << Howzit::Task.new(:include, title, obj)
219
+ when /run/i
220
+ title = c[3] || obj
221
+ # warn Color.template("{bg}Running {bw}#{obj}{x}") if Howzit.options[:log_level] < 2
222
+ runnable << Howzit::Task.new(:run, title, obj)
223
+ when /copy/i
224
+ # warn Color.template("{bg}Copied {bw}#{obj}{bg} to clipboard{x}") if Howzit.options[:log_level] < 2
225
+ runnable << Howzit::Task.new(:copy, title, Shellwords.escape(obj))
226
+ when /open|url/i
227
+ runnable << Howzit::Task.new(:open, title, obj)
228
+ end
229
+ end
230
+ end
231
+
232
+ runnable
233
+ end
234
+ end
235
+ end
@@ -0,0 +1,149 @@
1
+ module Howzit
2
+ module Util
3
+ class << self
4
+ def valid_command?(command)
5
+ cmd = command.split(' ')[0]
6
+ command_exist?(cmd)
7
+ end
8
+
9
+ def command_exist?(command)
10
+ exts = ENV.fetch('PATHEXT', '').split(::File::PATH_SEPARATOR)
11
+ if Pathname.new(command).absolute?
12
+ ::File.exist?(command) ||
13
+ exts.any? { |ext| ::File.exist?("#{command}#{ext}") }
14
+ else
15
+ ENV.fetch('PATH', '').split(::File::PATH_SEPARATOR).any? do |dir|
16
+ file = ::File.join(dir, command)
17
+ ::File.exist?(file) ||
18
+ exts.any? { |ext| ::File.exist?("#{file}#{ext}") }
19
+ end
20
+ end
21
+ end
22
+
23
+ # If either mdless or mdcat are installed, use that for highlighting
24
+ # markdown
25
+ def which_highlighter
26
+ if Howzit.options[:highlighter] =~ /auto/i
27
+ highlighters = %w[mdcat mdless]
28
+ highlighters.delete_if(&:nil?).select!(&:available?)
29
+ return nil if highlighters.empty?
30
+
31
+ hl = highlighters.first
32
+ args = case hl
33
+ when 'mdless'
34
+ '--no-pager'
35
+ end
36
+
37
+ [hl, args].join(' ')
38
+ else
39
+ hl = Howzit.options[:highlighter].split(/ /)[0]
40
+ if hl.available?
41
+ Howzit.options[:highlighter]
42
+ else
43
+ warn Color.template("{Rw}Error:{xbw} Specified highlighter (#{Howzit.options[:highlighter]}) not found, switching to auto")
44
+ Howzit.options[:highlighter] = 'auto'
45
+ which_highlighter
46
+ end
47
+ end
48
+ end
49
+
50
+ # When pagination is enabled, find the best (in my opinion) option,
51
+ # favoring environment settings
52
+ def which_pager
53
+ if Howzit.options[:pager] =~ /auto/i
54
+ pagers = [ENV['PAGER'], ENV['GIT_PAGER'],
55
+ 'bat', 'less', 'more', 'pager']
56
+ pagers.delete_if(&:nil?).select!(&:available?)
57
+ return nil if pagers.empty?
58
+
59
+ pg = pagers.first
60
+ args = case pg
61
+ when 'delta'
62
+ '--pager="less -FXr"'
63
+ when /^(less|more)$/
64
+ '-FXr'
65
+ when 'bat'
66
+ if Howzit.options[:highlight]
67
+ '--language Markdown --style plain --pager="less -FXr"'
68
+ else
69
+ '--style plain --pager="less -FXr"'
70
+ end
71
+ else
72
+ ''
73
+ end
74
+
75
+ [pg, args].join(' ')
76
+ else
77
+ pg = Howzit.options[:pager].split(/ /)[0]
78
+ if pg.available?
79
+ Howzit.options[:pager]
80
+ else
81
+ warn Color.template("{Rw}Error:{xbw} Specified pager (#{Howzit.options[:pager]}) not found, switching to auto")
82
+ Howzit.options[:pager] = 'auto'
83
+ which_pager
84
+ end
85
+ end
86
+ end
87
+
88
+ # Paginate the output
89
+ def page(text)
90
+ read_io, write_io = IO.pipe
91
+
92
+ input = $stdin
93
+
94
+ pid = Kernel.fork do
95
+ write_io.close
96
+ input.reopen(read_io)
97
+ read_io.close
98
+
99
+ # Wait until we have input before we start the pager
100
+ IO.select [input]
101
+
102
+ pager = which_pager
103
+
104
+ begin
105
+ exec(pager)
106
+ rescue SystemCallError => e
107
+ @log.error(e)
108
+ exit 1
109
+ end
110
+ end
111
+
112
+ read_io.close
113
+ write_io.write(text)
114
+ write_io.close
115
+
116
+ _, status = Process.waitpid2(pid)
117
+
118
+ status.success?
119
+ end
120
+
121
+ # print output to terminal
122
+ def show(string, opts = {})
123
+ options = {
124
+ color: true,
125
+ highlight: false,
126
+ wrap: 0
127
+ }
128
+
129
+ options.merge!(opts)
130
+
131
+ string = string.uncolor unless options[:color]
132
+
133
+ pipes = ''
134
+ if options[:highlight]
135
+ hl = which_highlighter
136
+ pipes = "|#{hl}" if hl
137
+ end
138
+
139
+ output = `echo #{Shellwords.escape(string.strip)}#{pipes}`.strip
140
+
141
+ if Howzit.options[:paginate]
142
+ page(output)
143
+ else
144
+ puts output
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
@@ -2,5 +2,5 @@
2
2
  # Primary module for this gem.
3
3
  module Howzit
4
4
  # Current Howzit version.
5
- VERSION = '1.2.15'.freeze
5
+ VERSION = '1.2.18'.freeze
6
6
  end
data/lib/howzit.rb CHANGED
@@ -1,8 +1,17 @@
1
- require 'howzit/version'
2
- require 'howzit/prompt'
3
- require 'howzit/colors'
4
- require 'howzit/buildnotes'
5
- require 'howzit/stringutils'
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'howzit/version'
4
+ require_relative 'howzit/prompt'
5
+ require_relative 'howzit/colors'
6
+ require_relative 'howzit/stringutils'
7
+
8
+ require_relative 'howzit/util'
9
+ require_relative 'howzit/hash'
10
+ require_relative 'howzit/config'
11
+ require_relative 'howzit/task'
12
+ require_relative 'howzit/topic'
13
+ require_relative 'howzit/buildnote'
14
+
6
15
  require 'optparse'
7
16
  require 'shellwords'
8
17
  require 'pathname'
@@ -18,3 +27,29 @@ IGNORE_FILE = 'ignore.yaml'
18
27
  MATCHING_OPTIONS = %w[partial exact fuzzy beginswith].freeze
19
28
  MULTIPLE_OPTIONS = %w[first best all choose].freeze
20
29
  HEADER_FORMAT_OPTIONS = %w[border block].freeze
30
+
31
+ module Howzit
32
+ class << self
33
+ attr_accessor :arguments, :cli_args
34
+ ##
35
+ ## Holds a Configuration object with methods and a @settings hash
36
+ ##
37
+ ## @return [Configuration] Configuration object
38
+ ##
39
+ def config
40
+ @config ||= Config.new
41
+ end
42
+
43
+ def inclusions
44
+ @inclusions ||= []
45
+ end
46
+
47
+ def options
48
+ config.options
49
+ end
50
+
51
+ def buildnote
52
+ @buildnote ||= BuildNote.new
53
+ end
54
+ end
55
+ end