howzit 1.2.13 → 1.2.16

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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.13'.freeze
5
+ VERSION = '1.2.16'.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'
@@ -16,3 +25,31 @@ CONFIG_DIR = '~/.config/howzit'
16
25
  CONFIG_FILE = 'howzit.yaml'
17
26
  IGNORE_FILE = 'ignore.yaml'
18
27
  MATCHING_OPTIONS = %w[partial exact fuzzy beginswith].freeze
28
+ MULTIPLE_OPTIONS = %w[first best all choose].freeze
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
@@ -1,40 +1,110 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe Howzit::BuildNotes do
4
- subject(:ruby_gem) { Howzit::BuildNotes.new([]) }
3
+ describe Howzit::BuildNote do
4
+ subject(:buildnote) { Howzit.buildnote }
5
5
 
6
6
  describe ".new" do
7
7
  it "makes a new instance" do
8
- expect(ruby_gem).to be_a Howzit::BuildNotes
8
+ expect(buildnote).to be_a Howzit::BuildNote
9
9
  end
10
10
  end
11
11
  end
12
12
 
13
- describe Howzit::BuildNotes do
13
+ describe Howzit::Task do
14
+ subject(:task) { Howzit::Task.new(:run, 'List Directory', 'ls') }
15
+
16
+ describe ".new" do
17
+ it "makes a new task instance" do
18
+ expect(task).to be_a Howzit::Task
19
+ end
20
+ end
21
+ end
22
+
23
+ describe Howzit::Topic do
24
+ title = 'Test Title'
25
+ content = 'Test Content'
26
+ subject(:topic) { Howzit::Topic.new(title, content) }
27
+
28
+ describe ".new" do
29
+ it "makes a new topic instance" do
30
+ expect(topic).to be_a Howzit::Topic
31
+ end
32
+ it "has the correct title" do
33
+ expect(topic.title).to eq title
34
+ end
35
+ it "has the correct content" do
36
+ expect(topic.content).to eq content
37
+ end
38
+ end
39
+ end
40
+
41
+ describe Howzit::BuildNote do
14
42
  Dir.chdir('spec')
15
- how = Howzit::BuildNotes.new(['--no-upstream', '--default'])
16
- how.create_note
17
- subject { how }
43
+ Howzit.options[:include_upstream] = false
44
+ Howzit.options[:default] = true
45
+ hz = Howzit.buildnote
46
+
47
+ hz.create_note
48
+ subject(:how) { hz }
18
49
 
19
50
  describe ".note_file" do
20
51
  it "locates a build note file" do
21
- expect(subject.note_file).not_to be_empty
52
+ expect(how.note_file).not_to be_empty
53
+ end
54
+ end
55
+
56
+ describe ".grep" do
57
+ it "finds topic containing 'editable'" do
58
+ expect(how.grep('editable').map { |topic| topic.title }).to include('File Structure')
59
+ end
60
+ it "does not return non-matching topic" do
61
+ expect(how.grep('editable').map { |topic| topic.title }).not_to include('Build')
22
62
  end
23
63
  end
24
64
 
25
- describe ".grep_topics" do
26
- it "found editable" do
27
- expect(subject.grep_topics('editable')).to include('File Structure')
28
- expect(subject.grep_topics('editable')).not_to include('Build')
65
+ describe ".find_topic" do
66
+ it "finds the File Structure topic" do
67
+ matches = how.find_topic('file struct')
68
+ expect(matches.count).to eq 1
69
+ expect(matches[0].title).to eq 'File Structure'
70
+ end
71
+ it "fuzzy matches" do
72
+ Howzit.options[:matching] = 'fuzzy'
73
+ matches = how.find_topic('flestct')
74
+ expect(matches.count).to eq 1
75
+ expect(matches[0].title).to eq 'File Structure'
76
+ end
77
+ it "succeeds with partial match" do
78
+ Howzit.options[:matching] = 'partial'
79
+ matches = how.find_topic('structure')
80
+ expect(matches.count).to eq 1
81
+ expect(matches[0].title).to eq 'File Structure'
82
+ end
83
+ it "succeeds with beginswith match" do
84
+ Howzit.options[:matching] = 'beginswith'
85
+ matches = how.find_topic('file')
86
+ expect(matches.count).to eq 1
87
+ expect(matches[0].title).to eq 'File Structure'
88
+ end
89
+ it "succeeds with exact match" do
90
+ Howzit.options[:matching] = 'exact'
91
+ matches = how.find_topic('file structure')
92
+ expect(matches.count).to eq 1
93
+ expect(matches[0].title).to eq 'File Structure'
94
+ end
95
+ it "fails with incomplete exact match" do
96
+ Howzit.options[:matching] = 'exact'
97
+ matches = how.find_topic('file struct')
98
+ expect(matches.count).to eq 0
29
99
  end
30
100
  end
31
101
 
32
- describe ".list_topic_titles" do
33
- it "found 4 topics" do
34
- expect(subject.topics.keys.count).to eq 4
102
+ describe ".topics" do
103
+ it "contains 4 topics" do
104
+ expect(how.list_topics.count).to eq 4
35
105
  end
36
- it "outputs a newline-separated string" do
37
- expect(subject.list_topic_titles.scan(/\n/).count).to eq 3
106
+ it "outputs a newline-separated string for completion" do
107
+ expect(how.list_completions.scan(/\n/).count).to eq 3
38
108
  end
39
109
  end
40
110
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: howzit
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.13
4
+ version: 1.2.16
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brett Terpstra
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-08-02 00:00:00.000000000 Z
11
+ date: 2022-08-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -268,10 +268,16 @@ files:
268
268
  - howzit.gemspec
269
269
  - lib/.rubocop.yml
270
270
  - lib/howzit.rb
271
+ - lib/howzit/buildnote.rb
271
272
  - lib/howzit/buildnotes.rb
272
273
  - lib/howzit/colors.rb
274
+ - lib/howzit/config.rb
275
+ - lib/howzit/hash.rb
273
276
  - lib/howzit/prompt.rb
274
277
  - lib/howzit/stringutils.rb
278
+ - lib/howzit/task.rb
279
+ - lib/howzit/topic.rb
280
+ - lib/howzit/util.rb
275
281
  - lib/howzit/version.rb
276
282
  - spec/.rubocop.yml
277
283
  - spec/ruby_gem_spec.rb