howzit 1.2.14 → 1.2.17

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