snibbets 2.0.7

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