snibbets 2.0.7

Sign up to get free protection for your applications and to get access to all the features.
data/bin/snibbets ADDED
@@ -0,0 +1,427 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift File.join(__dir__, '..', 'lib')
5
+ require 'snibbets'
6
+
7
+ module Snibbets
8
+ class << self
9
+ # Search the snippets directory for query using find and grep
10
+ def search(try: 0)
11
+ folder = Snibbets.options[:source]
12
+ # start by doing a spotlight search, if that fails, start trying:
13
+ # First try only search by filenames
14
+ # Second try search with grep
15
+ ext = Snibbets.options[:extension] || 'md'
16
+ cmd = case try
17
+ when 1
18
+ %(find "#{folder}" -iregex '#{@query.rx}' -name '*.#{ext}')
19
+ when 2
20
+ rg = TTY::Which.which('rg')
21
+ ag = TTY::Which.which('ag')
22
+ ack = TTY::Which.which('ack')
23
+ grep = TTY::Which.which('grep')
24
+ if Snibbets.options[:name_only]
25
+ nil
26
+ elsif !rg.empty?
27
+ %(#{rg} -li --color=never --glob='*.#{ext}' '#{@query.rx}' "#{folder}")
28
+ elsif !ag.empty?
29
+ %(#{ag} -li --nocolor -G '.*.#{ext}' '#{@query.rx}' "#{folder}")
30
+ elsif !ack.empty?
31
+ %(#{ack} -li --nocolor --markdown '#{@query.rx}' "#{folder}")
32
+ elsif !grep.empty?
33
+ %(#{grep} -iEl '#{@query.rx}' "#{folder}"/**/*.#{ext})
34
+ else
35
+ nil
36
+ end
37
+ else
38
+ mdfind = TTY::Which.which('mdfind')
39
+ if mdfind.empty?
40
+ nil
41
+ else
42
+ name_only = Snibbets.options[:name_only] ? '-name ' : ''
43
+ %(mdfind -onlyin #{folder} #{name_only}'#{@query} filename:.#{ext}' 2>/dev/null)
44
+ end
45
+ end
46
+
47
+ if try == 2 && cmd.nil?
48
+ puts "No search method available on this system. Please install ripgrep, silver surfer, ack, or grep."
49
+ Process.exit 1
50
+ end
51
+
52
+ res = cmd.nil? ? '' : `#{cmd}`.strip
53
+
54
+ matches = []
55
+
56
+ unless res.empty?
57
+ lines = res.split(/\n/)
58
+ lines.each do |l|
59
+ matches << {
60
+ 'title' => File.basename(l, '.*'),
61
+ 'path' => l
62
+ }
63
+ end
64
+
65
+ matches.sort_by! { |a| a['title'] }.uniq!
66
+
67
+ return matches unless matches.empty?
68
+ end
69
+
70
+ return matches if try == 2
71
+
72
+ # if no results on the first try, try again searching all text
73
+ search(try: try + 1) if matches.empty?
74
+ end
75
+
76
+ def open_snippet_in_editor(filepath)
77
+ editor = Snibbets.options[:editor] || Snibbets::Config.best_editor
78
+
79
+ os = RbConfig::CONFIG['target_os']
80
+
81
+ if editor.nil?
82
+ OS.open(filepath)
83
+ else
84
+ if os =~ /darwin.*/i
85
+ if editor =~ /^TextEdit/
86
+ `open -a TextEdit "#{filepath}"`
87
+ elsif TTY::Which.bundle_id?(editor)
88
+ `open -b "#{editor}" "#{filepath}"`
89
+ elsif TTY::Which.app?(editor)
90
+ `open -a "#{editor}" "#{filepath}"`
91
+ elsif TTY::Which.exist?(editor)
92
+ editor = TTY::Which.which(editor)
93
+ system %(#{editor} "#{filepath}") if editor
94
+ else
95
+ puts "No editor configured, or editor is missing"
96
+ Process.exit 1
97
+ end
98
+ elsif TTY::Which.exist?(editor)
99
+ editor = TTY::Which.which(editor)
100
+ system %(#{editor} "#{filepath}") if editor
101
+ else
102
+ puts "No editor configured, or editor is missing"
103
+ Process.exit 1
104
+ end
105
+ end
106
+ end
107
+
108
+ def new_snippet_from_clipboard
109
+ trap('SIGINT') do
110
+ Howzit.console.info "\nCancelled"
111
+ exit!
112
+ end
113
+
114
+ build_lexers
115
+
116
+ pb = OS.paste.outdent
117
+
118
+ printf 'What does this snippet do? '
119
+ input = $stdin.gets.chomp
120
+ title = input unless input.empty?
121
+
122
+ printf 'What language(s) does it use (separate with spaces, full names or file extensions will work)? '
123
+ input = $stdin.gets.chomp
124
+ langs = input.split(/ +/).map(&:strip) unless input.empty?
125
+ exts = langs.map { |lang| Snibbets::Lexers.lang_to_ext(lang) }
126
+ tags = langs.map { |lang| Snibbets::Lexers.ext_to_lang(lang) }.concat(langs).sort.uniq
127
+
128
+ filename ="#{title}.#{exts.join('.')}.#{Snibbets.options[:extension]}"
129
+
130
+ File.open(File.join(Snibbets.options[:source], filename), 'w') do |f|
131
+ f.puts "tags: #{tags.join(', ')}
132
+
133
+ ```
134
+ #{pb}
135
+ ```"
136
+ end
137
+
138
+ puts "New snippet written to #{filename}."
139
+ end
140
+
141
+ def handle_launchbar(results)
142
+ output = []
143
+
144
+ if results.empty?
145
+ out = {
146
+ 'title' => 'No matching snippets found'
147
+ }.to_json
148
+ puts out
149
+ Process.exit
150
+ end
151
+
152
+ results.each do |result|
153
+ input = IO.read(result['path'])
154
+ snippets = input.snippets
155
+ next if snippets.empty?
156
+
157
+ children = []
158
+
159
+ if snippets.length == 1
160
+ output << {
161
+ 'title' => result['title'],
162
+ 'path' => result['path'],
163
+ 'action' => 'copyIt',
164
+ 'actionArgument' => snippets[0]['code'],
165
+ 'label' => 'Copy'
166
+ }
167
+ next
168
+ end
169
+
170
+ snippets.each { |s|
171
+ children << {
172
+ 'title' => s['title'],
173
+ 'path' => result['path'],
174
+ 'action' => 'copyIt',
175
+ 'actionArgument' => s['code'],
176
+ 'label' => 'Copy'
177
+ }
178
+ }
179
+
180
+ output << {
181
+ 'title' => result['title'],
182
+ 'path' => result['path'],
183
+ 'children' => children
184
+ }
185
+ end
186
+
187
+ puts output.to_json
188
+ end
189
+
190
+ def handle_results(results)
191
+ if Snibbets.options[:launchbar]
192
+ handle_launchbar(results)
193
+ else
194
+ filepath = nil
195
+ if results.empty?
196
+ warn 'No results'
197
+ Process.exit 0
198
+ elsif results.length == 1 || !Snibbets.options[:interactive]
199
+ filepath = results[0]['path']
200
+ input = IO.read(filepath)
201
+ else
202
+ answer = Snibbets::Menu.menu(results, title: 'Select a file')
203
+ filepath = answer['path']
204
+ input = IO.read(filepath)
205
+ end
206
+
207
+ if @arguments[:edit_snippet]
208
+ open_snippet_in_editor(filepath)
209
+ Process.exit 0
210
+ end
211
+
212
+ snippets = input.snippets
213
+
214
+ if snippets.empty?
215
+ warn 'No snippets found'
216
+ Process.exit 0
217
+ elsif snippets.length == 1 || !Snibbets.options[:interactive]
218
+ if Snibbets.options[:output] == 'json'
219
+ print(snippets.to_json)
220
+ else
221
+ snippets.each do |snip|
222
+ header = File.basename(filepath, '.md')
223
+ warn header
224
+ warn '-' * header.length
225
+ code = snip['code']
226
+ code = highlight(code, filepath) if Snibbets.options[:highlight]
227
+ print(code)
228
+ end
229
+ end
230
+ elsif snippets.length > 1
231
+ if Snibbets.options[:all]
232
+ if Snibbets.options[:output] == 'json'
233
+ print(snippets.to_json)
234
+ else
235
+ output = []
236
+ snippets.each do |snippet|
237
+ output << snippet['title']
238
+ output << '-' * snippet['title'].length
239
+ output << snippet['code']
240
+ output << "\n"
241
+ end
242
+ print(output.join("\n"))
243
+ end
244
+ else
245
+ snippets.push({ 'title' => 'All snippets', 'code' => '' })
246
+
247
+ answer = Snibbets::Menu.menu(snippets, filename: File.basename(filepath, '.md'), title: 'Select snippet', query: @query)
248
+
249
+ if answer['title'] == 'All snippets'
250
+ snippets.delete_if { |s| s['title'] == 'All snippets'}
251
+ if Snibbets.options[:output] == 'json'
252
+ print(snippets.to_json)
253
+ else
254
+ header = File.basename(filepath, '.md')
255
+ warn header
256
+ warn '=' * header.length
257
+ output = []
258
+ snippets.each do |snippet|
259
+ output << snippet['title']
260
+ output << '-' * snippet['title'].length
261
+ output << snippet['code']
262
+ output << "\n"
263
+ end
264
+ print(output.join("\n"))
265
+ end
266
+ elsif Snibbets.options[:output] == 'json'
267
+ print(answer.to_json)
268
+ else
269
+ header = "#{File.basename(filepath, '.md')}: #{answer['title']}"
270
+ warn header
271
+ warn '-' * header.length
272
+ code = answer['code']
273
+ code = highlight(code, filepath) if Snibbets.options[:highlight]
274
+ print(code)
275
+ end
276
+ end
277
+ end
278
+ end
279
+ end
280
+
281
+ def print(output)
282
+ $stdout.puts(output)
283
+ if Snibbets.options[:copy]
284
+ OS.copy(output)
285
+ $stderr.puts "Copied to clipboard"
286
+ end
287
+ end
288
+ end
289
+ end
290
+
291
+ module Snibbets
292
+ class << self
293
+ attr_reader :arguments, :query
294
+
295
+ def run
296
+ options = Snibbets.config.options
297
+
298
+ @arguments = {
299
+ save_config: false,
300
+ edit_config: false,
301
+ edit_snippet: false,
302
+ paste_snippet: false
303
+ }
304
+
305
+ optparse = OptionParser.new do |opts|
306
+ opts.banner = "Usage: #{File.basename(__FILE__)} [options] query"
307
+
308
+ opts.on('-a', '--all', 'If a file contains multiple snippets, output all of them (no menu)') do
309
+ options[:all] = true
310
+ end
311
+
312
+ opts.on('-c', '--[no-]copy', 'Copy the output to the clibpoard (also displays on STDOUT)') do |v|
313
+ options[:copy] = v
314
+ end
315
+
316
+ opts.on('-e', '--edit', 'Open the selected snippet in your configured editor') do
317
+ @arguments[:edit_snippet] = true
318
+ end
319
+
320
+ opts.on('-n', '--[no-]name-only', 'Only search file names, not content') do |v|
321
+ options[:name_only] = v
322
+ end
323
+
324
+ opts.on('-o', '--output FORMAT', 'Output format (json|launchbar|*raw)') do |outformat|
325
+ valid = %w[json launchbar lb raw]
326
+ if outformat.downcase =~ /(launchbar|lb)/
327
+ options[:launchbar] = true
328
+ options[:interactive] = false
329
+ elsif valid.include?(outformat.downcase)
330
+ options[:output] = outformat.downcase
331
+ end
332
+ end
333
+
334
+ opts.on('-p', '--paste', '--new', 'Interactively create a new snippet from clipboard contents (Mac only)') do
335
+ @arguments[:paste_snippet] = true
336
+ end
337
+
338
+ opts.on('-q', '--quiet', 'Skip menus and display first match') do
339
+ options[:interactive] = false
340
+ options[:launchbar] = false
341
+ end
342
+
343
+ opts.on('-s', '--source FOLDER', 'Snippets folder to search') do |folder|
344
+ options[:source] = File.expand_path(folder)
345
+ end
346
+
347
+ opts.on('--configure', 'Open the configuration file in your default editor') do
348
+ @arguments[:edit_config] = true
349
+ end
350
+
351
+ opts.on('--highlight', 'Use pygments or skylighting to syntax highlight (if installed)') do
352
+ options[:highlight] = true
353
+ end
354
+
355
+ opts.on('--save', 'Save the current command line options to the YAML configuration') do
356
+ @arguments[:save_config] = true
357
+ end
358
+
359
+ opts.on('-h', '--help', 'Display this screen') do
360
+ puts "Snibbets v#{VERSION}"
361
+ puts
362
+ puts optparse
363
+ Process.exit 0
364
+ end
365
+
366
+ opts.on('-v', '--version', 'Display version information') do
367
+ puts "Snibbets v#{VERSION}"
368
+ Process.exit 0
369
+ end
370
+ end
371
+
372
+ optparse.parse!
373
+
374
+ if @arguments[:save_config]
375
+ config = Snibbets::Config.new
376
+ config.write_config
377
+ puts "Configuration saved to #{config.config_file}"
378
+ end
379
+
380
+ if @arguments[:edit_config]
381
+ config = Snibbets::Config.new
382
+ config.write_config
383
+ open_snippet_in_editor(config.config_file)
384
+ Process.exit 0
385
+ end
386
+
387
+ unless File.directory?(options[:source])
388
+ puts 'The Snippets folder doesn\'t exist, please configure it.'
389
+ puts 'Run `snibbets --configure` to open the config file for editing.'
390
+ Process.exit 1
391
+ end
392
+
393
+ if @arguments[:paste_snippet]
394
+ Snibbets.new_snippet_from_clipboard
395
+ Process.exit 0
396
+ end
397
+
398
+ @query = ''
399
+
400
+ if options[:launchbar]
401
+ @query = if $stdin.stat.size.positive?
402
+ $stdin.read.force_encoding('utf-8')
403
+ else
404
+ ARGV.join(' ')
405
+ end
406
+ elsif ARGV.length
407
+ @query = ARGV.join(' ')
408
+ end
409
+
410
+ @query = CGI.unescape(@query)
411
+
412
+ if @query.strip.empty?
413
+ if @arguments[:save_config]
414
+ Process.exit 0
415
+ else
416
+ puts 'No search query'
417
+ puts optparse
418
+ Process.exit 1
419
+ end
420
+ end
421
+
422
+ handle_results(search(try: 0))
423
+ end
424
+ end
425
+ end
426
+
427
+ Snibbets.run
data/buildnotes.md ADDED
@@ -0,0 +1,39 @@
1
+ template: gem, git, gli, project
2
+ executable: na
3
+ readme: README.md
4
+ changelog: CHANGELOG.md
5
+ project: snibbets
6
+
7
+ # Snibbets
8
+
9
+ A plain text snippet manager
10
+
11
+ ## Test
12
+
13
+ @run(bundle exec bin/snibbets $@)
14
+
15
+ ## Deploy
16
+
17
+ You no longer need to manually bump the version, it will be incremented when this task runs.
18
+
19
+ ```run Update Changelog and README
20
+ #!/bin/bash
21
+
22
+ changelog -u
23
+ # scripts/fixreadme.rb
24
+ changelog | git commit -a -F -
25
+ git pull
26
+ git push
27
+ ```
28
+
29
+ @include(gem:Release Gem) Release Gem
30
+ @include(project:Update Blog Project) Update Blog Project
31
+ @run(rake bump[patch]) Bump Version
32
+
33
+ @after
34
+ Don't forget to publish the website!
35
+ @end
36
+
37
+ ## Plans
38
+
39
+
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Snibbets
4
+ class Config
5
+ attr_accessor :options
6
+
7
+ DEFAULT_OPTIONS = {
8
+ all: false,
9
+ copy: false,
10
+ editor: nil,
11
+ extension: 'md',
12
+ highlight: false,
13
+ interactive: true,
14
+ launchbar: false,
15
+ name_only: false,
16
+ output: 'raw',
17
+ source: File.expand_path('~/Dropbox/Snippets')
18
+ }.freeze
19
+
20
+ def initialize
21
+ custom_config = read_config
22
+ @options = DEFAULT_OPTIONS.merge(custom_config)
23
+ @options[:editor] ||= best_editor
24
+ write_config unless @options.equal?(custom_config)
25
+ end
26
+
27
+ def best_editor
28
+ if ENV['EDITOR']
29
+ ENV['EDITOR']
30
+ elsif ENV['GIT_EDITOR']
31
+ ENV['GIT_EDITOR']
32
+ else
33
+ return TTY::Which.which('code') if TTY::Which.exist?('code')
34
+
35
+ return TTY::Which.which('subl') if TTY::Which.exist?('subl')
36
+
37
+ return TTY::Which.which('bbedit') if TTY::Which.exist?('bbedit')
38
+
39
+ return TTY::Which.which('nano') if TTY::Which.exist?('nano')
40
+
41
+ return TTY::Which.which('vim') if TTY::Which.exist?('vim')
42
+
43
+ 'TextEdit'
44
+ end
45
+ end
46
+
47
+ def config_dir
48
+ File.expand_path('~/.config/snibbets')
49
+ end
50
+
51
+ def config_file
52
+ File.join(config_dir, 'snibbets.yml')
53
+ end
54
+
55
+ def read_config
56
+ if File.exist?(config_file)
57
+ YAML.safe_load(IO.read(config_file)).symbolize_keys
58
+ else
59
+ {}
60
+ end
61
+ end
62
+
63
+ def write_config
64
+ raise 'Error creating config directory, file exists' if File.exist?(config_dir) && !File.directory?(config_dir)
65
+
66
+ FileUtils.mkdir_p(config_dir) unless File.directory?(config_dir)
67
+ File.open(config_file, 'w') { |f| f.puts(YAML.dump(@options.stringify_keys)) }
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Snibbets
4
+ class ::Hash
5
+ # Turn all keys into string
6
+ #
7
+ # Return a copy of the hash where all its keys are strings
8
+ def stringify_keys
9
+ each_with_object({}) { |(k, v), hsh| hsh[k.to_s] = v.is_a?(Hash) ? v.stringify_keys : v }
10
+ end
11
+
12
+ # Turn all keys into symbols
13
+ def symbolize_keys
14
+ each_with_object({}) { |(k, v), hsh| hsh[k.to_sym] = v.is_a?(Hash) ? v.symbolize_keys : v }
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,26 @@
1
+ module Snibbets
2
+ class Highlight
3
+ def highlight_pygments(executable, code, syntax, theme)
4
+ syntax = syntax.empty? ? '-g' : "-l #{syntax}"
5
+ `echo #{Shellwords.escape(code)} | #{executable} #{syntax}`
6
+ end
7
+
8
+ def highlight_skylight(executable, code, syntax, theme)
9
+ return code if syntax.empty?
10
+
11
+ `echo #{Shellwords.escape(code)} | #{executable} --syntax #{syntax}`
12
+ end
13
+
14
+ def highlight(code, filename, theme = 'monokai')
15
+ syntax = syntax_from_extension(filename)
16
+
17
+ skylight = TTY::Which.which('skylighting')
18
+ return highlight_skylight(skylight, code, syntax, theme) unless skylight.empty?
19
+
20
+ pygments = TTY::Which.which('pygmentize')
21
+ return highlight_pygments(pygments, code, syntax, theme) unless pygments.empty?
22
+
23
+ code
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,34 @@
1
+ module Snibbets
2
+ class Lexers
3
+ attr_accessor :lexers
4
+
5
+ def build_lexers
6
+ IO.read('lexers_db.txt').split(/\n/).each do |line|
7
+ key = line.match(/(?mi)^((, )?[^,]+?)+?(?=\[)/)[0]
8
+ keys = key.split(/,/).map(&:strip)
9
+ value = line.match(/\[(.*?)\]/)[1]
10
+ values = value.split(/,/).map(&:strip)
11
+
12
+ @lexers << {
13
+ lexer: keys.shift,
14
+ aliases: keys,
15
+ extensions: values
16
+ }
17
+ end
18
+ end
19
+
20
+ def ext_to_lang(ext)
21
+ matches = @lexers.select { |lex| lex[:extensions].map(&:downcase).include?(ext.downcase) }
22
+ matches.map { |lex| lex[:lexer] }.first
23
+ end
24
+
25
+ def lang_to_ext(lexer)
26
+ matches = @lexers.select { |lex| lex[:lexer] == lexer || lex[:aliases].map(&:downcase).include?(lexer.downcase) }
27
+ matches.map { |lex| lex[:extensions].first }.first
28
+ end
29
+
30
+ def syntax_from_extension(filename)
31
+ ext_to_lang(filename.split(/\./)[1])
32
+ end
33
+ end
34
+ end