howzit 1.2.13 → 1.2.16

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,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