howzit 1.2.14 → 1.2.17

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,128 @@
1
+ module Howzit
2
+ # Config Class
3
+ class Config
4
+ attr_reader :options
5
+
6
+ DEFAULTS = {
7
+ color: true,
8
+ config_editor: ENV['EDITOR'] || nil,
9
+ editor: ENV['EDITOR'] || nil,
10
+ header_format: 'border',
11
+ highlight: true,
12
+ highlighter: 'auto',
13
+ include_upstream: false,
14
+ log_level: 1, # 0: debug, 1: info, 2: warn, 3: error
15
+ matching: 'partial', # exact, partial, fuzzy, beginswith
16
+ multiple_matches: 'choose',
17
+ output_title: false,
18
+ pager: 'auto',
19
+ paginate: true,
20
+ show_all_code: false,
21
+ show_all_on_error: false,
22
+ wrap: 0
23
+ }.deep_freeze
24
+
25
+ def initialize
26
+ load_options
27
+ end
28
+
29
+ def write_config(config)
30
+ File.open(config_file, 'w') { |f| f.puts config.to_yaml }
31
+ end
32
+
33
+ def should_ignore(filename)
34
+ return false unless File.exist?(ignore_file)
35
+
36
+ @ignore_patterns ||= YAML.safe_load(IO.read(ignore_file))
37
+
38
+ ignore = false
39
+
40
+ @ignore_patterns.each do |pat|
41
+ if filename =~ /#{pat}/
42
+ ignore = true
43
+ break
44
+ end
45
+ end
46
+
47
+ ignore
48
+ end
49
+
50
+ def template_folder
51
+ File.join(config_dir, 'templates')
52
+ end
53
+
54
+ def editor
55
+ edit_config(DEFAULTS)
56
+ end
57
+
58
+ private
59
+
60
+ def load_options
61
+ Color.coloring = $stdout.isatty
62
+ flags = {
63
+ choose: false,
64
+ default: false,
65
+ grep: nil,
66
+ list_runnable: false,
67
+ list_runnable_titles: false,
68
+ list_topic_titles: false,
69
+ list_topics: false,
70
+ quiet: false,
71
+ run: false,
72
+ title_only: false,
73
+ verbose: false
74
+ }
75
+
76
+ config = load_config
77
+ @options = flags.merge(config)
78
+ end
79
+
80
+ def config_dir
81
+ File.expand_path(CONFIG_DIR)
82
+ end
83
+
84
+ def config_file
85
+ File.join(config_dir, CONFIG_FILE)
86
+ end
87
+
88
+ def ignore_file
89
+ File.join(config_dir, IGNORE_FILE)
90
+ end
91
+
92
+ def create_config(d)
93
+ unless File.directory?(config_dir)
94
+ warn "Creating config directory at #{config_dir}"
95
+ FileUtils.mkdir_p(config_dir)
96
+ end
97
+
98
+ unless File.exist?(config_file)
99
+ warn "Writing fresh config file to #{config_file}"
100
+ write_config(d)
101
+ end
102
+ config_file
103
+ end
104
+
105
+ def load_config
106
+ file = create_config(DEFAULTS)
107
+ config = YAML.load(IO.read(file))
108
+ newconfig = config ? DEFAULTS.merge(config) : DEFAULTS
109
+ write_config(newconfig)
110
+ newconfig.dup
111
+ end
112
+
113
+ def edit_config(d)
114
+ editor = Howzit.options.fetch(:config_editor, ENV['EDITOR'])
115
+
116
+ raise 'No config_editor defined' if editor.nil?
117
+
118
+ # raise "Invalid editor (#{editor})" unless Util.valid_command?(editor)
119
+
120
+ load_config
121
+ if Util.valid_command?(editor.split(/ /).first)
122
+ system %(#{editor} "#{config_file}")
123
+ else
124
+ `open -a "#{editor}" "#{config_file}"`
125
+ end
126
+ end
127
+ end
128
+ end
@@ -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 + 2}",
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,233 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Howzit
4
+ # Topic Class
5
+ class Topic
6
+ attr_writer :parent
7
+
8
+ attr_reader :title, :content, :tasks, :prereqs, :postreqs
9
+
10
+ def initialize(title, content)
11
+ @title = title
12
+ @content = content
13
+ @parent = nil
14
+ @nest_level = 0
15
+ @tasks = gather_tasks
16
+ end
17
+
18
+ def grep(term)
19
+ @title =~ /#{term}/i || @content =~ /#{term}/i
20
+ end
21
+
22
+ # Handle run command, execute directives
23
+ def run(nested: false)
24
+ output = []
25
+ tasks = 0
26
+ if @tasks.count.positive?
27
+ unless @prereqs.empty?
28
+ puts @prereqs.join("\n\n")
29
+ res = Prompt.yn('This task has prerequisites, have they been met?', true)
30
+ Process.exit 1 unless res
31
+
32
+ end
33
+
34
+ @tasks.each do |task|
35
+ if task.type == :block
36
+ warn Color.template("{bg}Running block {bw}#{title}{x}") if Howzit.options[:log_level] < 2
37
+ block = task.action
38
+ script = Tempfile.new('howzit_script')
39
+ begin
40
+ script.write(block)
41
+ script.close
42
+ File.chmod(0777, script.path)
43
+ system(%(/bin/sh -c "#{script.path}"))
44
+ tasks += 1
45
+ ensure
46
+ script.close
47
+ script.unlink
48
+ end
49
+ else
50
+ case task.type
51
+ when :include
52
+ matches = Howzit.buildnote.find_topic(task.action)
53
+ raise "Topic not found: #{task.action}" if matches.empty?
54
+
55
+ warn Color.template("{by}Running tasks from {bw}#{matches[0].title}{x}") if Howzit.options[:log_level] < 2
56
+ output.push(matches[0].run(nested: true))
57
+ warn Color.template("{by}End include: #{matches[0].tasks.count} tasks") if Howzit.options[:log_level] < 2
58
+ tasks += matches[0].tasks.count
59
+ when :run
60
+ warn Color.template("{bg}Running {bw}#{task.title}{x}") if Howzit.options[:log_level] < 2
61
+ system(task.action)
62
+ tasks += 1
63
+ when :copy
64
+ warn Color.template("{bg}Copied {bw}#{task.title}{bg} to clipboard{x}") if Howzit.options[:log_level] < 2
65
+ `echo #{Shellwords.escape(task.action)}'\\c'|pbcopy`
66
+ tasks += 1
67
+ when :open
68
+ os_open(task.action)
69
+ tasks += 1
70
+ end
71
+ end
72
+ end
73
+ else
74
+ warn Color.template("{r}--run: No {br}@directive{xr} found in {bw}#{key}{x}")
75
+ end
76
+ output.push(Color.template("{bm}Ran #{tasks} #{tasks == 1 ? 'task' : 'tasks'}{x}")) if Howzit.options[:log_level] < 2 && !nested
77
+
78
+ puts postreqs.join("\n\n") unless postreqs.empty?
79
+
80
+ output
81
+ end
82
+
83
+ def os_open(command)
84
+ os = RbConfig::CONFIG['target_os']
85
+ out = Color.template("{bg}Opening {bw}#{command}")
86
+ case os
87
+ when /darwin.*/i
88
+ warn Color.template("#{out} (macOS){x}") if Howzit.options[:log_level] < 2
89
+ `open #{Shellwords.escape(command)}`
90
+ when /mingw|mswin/i
91
+ warn Color.template("#{out} (Windows){x}") if Howzit.options[:log_level] < 2
92
+ `start #{Shellwords.escape(command)}`
93
+ else
94
+ if 'xdg-open'.available?
95
+ warn Color.template("#{out} (Linux){x}") if Howzit.options[:log_level] < 2
96
+ `xdg-open #{Shellwords.escape(command)}`
97
+ else
98
+ warn out if Howzit.options[:log_level] < 2
99
+ warn 'Unable to determine executable for `open`.'
100
+ end
101
+ end
102
+ end
103
+
104
+ # Output a topic with fancy title and bright white text.
105
+ def print_out(options = {})
106
+ defaults = { single: false, header: true }
107
+ opt = defaults.merge(options)
108
+
109
+ output = []
110
+ if opt[:header]
111
+ output.push(@title.format_header)
112
+ output.push('')
113
+ end
114
+ topic = @content.dup
115
+ topic.gsub!(/(?mi)^(`{3,})run *([^\n]*)[\s\S]*?\n\1\s*$/, '@@@run \2') unless Howzit.options[:show_all_code]
116
+ topic.split(/\n/).each do |l|
117
+ case l
118
+ when /@(before|after|prereq|end)/
119
+ next
120
+ when /@include\((.*?)\)/
121
+
122
+ m = Regexp.last_match
123
+ matches = Howzit.buildnote.find_topic(m[1])
124
+ unless matches.empty?
125
+ if opt[:single]
126
+ title = "From #{matches[0].title}:"
127
+ color = '{Kyd}'
128
+ rule = '{kKd}'
129
+ else
130
+ title = "Include #{matches[0].title}"
131
+ color = '{Kyd}'
132
+ rule = '{kKd}'
133
+ end
134
+ unless Howzit.inclusions.include?(matches[0])
135
+ output.push("#{'> ' * @nest_level}#{title}".format_header({ color: color, hr: '.', border: rule }))
136
+ end
137
+
138
+ if opt[:single]
139
+ if Howzit.inclusions.include?(matches[0])
140
+ output.push("#{'> ' * @nest_level}#{title} included above".format_header({
141
+ color: color, hr: '.', border: rule }))
142
+ else
143
+ @nest_level += 1
144
+ output.concat(matches[0].print_out({ single: true, header: false }))
145
+ @nest_level -= 1
146
+ end
147
+ unless Howzit.inclusions.include?(matches[0])
148
+ output.push("#{'> ' * @nest_level}...".format_header({ color: color, hr: '.', border: rule }))
149
+ end
150
+ end
151
+ Howzit.inclusions.push(matches[0])
152
+ end
153
+
154
+ when /@(run|copy|open|url|include)\((.*?)\)/
155
+ m = Regexp.last_match
156
+ cmd = m[1]
157
+ obj = m[2]
158
+ icon = case cmd
159
+ when 'run'
160
+ "\u{25B6}"
161
+ when 'copy'
162
+ "\u{271A}"
163
+ when /open|url/
164
+ "\u{279A}"
165
+ end
166
+
167
+ output.push(Color.template("{bmK}#{icon} {bwK}#{obj.gsub(/\\n/, '\​n')}{x}"))
168
+ when /(`{3,})run *(.*?)$/i
169
+ m = Regexp.last_match
170
+ desc = m[2].length.positive? ? "Block: #{m[2]}" : 'Code Block'
171
+ output.push(Color.template("{bmK}\u{25B6} {bwK}#{desc}{x}\n```"))
172
+ when /@@@run *(.*?)$/i
173
+ m = Regexp.last_match
174
+ desc = m[1].length.positive? ? "Block: #{m[1]}" : 'Code Block'
175
+ output.push(Color.template("{bmK}\u{25B6} {bwK}#{desc}{x}"))
176
+ else
177
+ l.wrap!(Howzit.options[:wrap]) if (Howzit.options[:wrap]).positive?
178
+ output.push(l)
179
+ end
180
+ end
181
+ output.push('') # FIXME: Is this where the extra line is coming from?
182
+ end
183
+
184
+ private
185
+
186
+ def gather_tasks
187
+ runnable = []
188
+ @prereqs = @content.scan(/(?<=@before\n).*?(?=\n@end)/im).map(&:strip)
189
+ @postreqs = @content.scan(/(?<=@after\n).*?(?=\n@end)/im).map(&:strip)
190
+
191
+ rx = /(?:@(include|run|copy|open|url)\((.*?)\) *(.*?)(?=$)|(`{3,})run(?: +([^\n]+))?(.*?)\4)/mi
192
+ directives = @content.scan(rx)
193
+
194
+ directives.each do |c|
195
+ if c[0].nil?
196
+ title = c[4] ? c[4].strip : ''
197
+ block = c[5].strip
198
+ runnable << Howzit::Task.new(:block, title, block)
199
+ else
200
+ cmd = c[0]
201
+ obj = c[1]
202
+ title = c[3] || obj
203
+
204
+ case cmd
205
+ when /include/i
206
+ # matches = Howzit.buildnote.find_topic(obj)
207
+ # unless matches.empty? || Howzit.inclusions.include?(matches[0].title)
208
+ # tasks = matches[0].tasks.map do |inc|
209
+ # Howzit.inclusions.push(matches[0].title)
210
+ # inc.parent = matches[0]
211
+ # inc
212
+ # end
213
+ # runnable.concat(tasks)
214
+ # end
215
+ title = c[3] || obj
216
+ runnable << Howzit::Task.new(:include, title, obj)
217
+ when /run/i
218
+ title = c[3] || obj
219
+ # warn Color.template("{bg}Running {bw}#{obj}{x}") if Howzit.options[:log_level] < 2
220
+ runnable << Howzit::Task.new(:run, title, obj)
221
+ when /copy/i
222
+ # warn Color.template("{bg}Copied {bw}#{obj}{bg} to clipboard{x}") if Howzit.options[:log_level] < 2
223
+ runnable << Howzit::Task.new(:copy, title, Shellwords.escape(obj))
224
+ when /open|url/i
225
+ runnable << Howzit::Task.new(:open, title, obj)
226
+ end
227
+ end
228
+ end
229
+
230
+ runnable
231
+ end
232
+ end
233
+ end