searchlink 2.3.59

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.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/bin/searchlink +84 -0
  3. data/lib/searchlink/array.rb +7 -0
  4. data/lib/searchlink/config.rb +230 -0
  5. data/lib/searchlink/curl/html.rb +482 -0
  6. data/lib/searchlink/curl/json.rb +90 -0
  7. data/lib/searchlink/curl.rb +7 -0
  8. data/lib/searchlink/help.rb +103 -0
  9. data/lib/searchlink/output.rb +270 -0
  10. data/lib/searchlink/parse.rb +668 -0
  11. data/lib/searchlink/plist.rb +213 -0
  12. data/lib/searchlink/search.rb +70 -0
  13. data/lib/searchlink/searches/amazon.rb +25 -0
  14. data/lib/searchlink/searches/applemusic.rb +123 -0
  15. data/lib/searchlink/searches/bitly.rb +50 -0
  16. data/lib/searchlink/searches/definition.rb +67 -0
  17. data/lib/searchlink/searches/duckduckgo.rb +167 -0
  18. data/lib/searchlink/searches/github.rb +245 -0
  19. data/lib/searchlink/searches/google.rb +67 -0
  20. data/lib/searchlink/searches/helpers/chromium.rb +318 -0
  21. data/lib/searchlink/searches/helpers/firefox.rb +135 -0
  22. data/lib/searchlink/searches/helpers/safari.rb +133 -0
  23. data/lib/searchlink/searches/history.rb +166 -0
  24. data/lib/searchlink/searches/hook.rb +77 -0
  25. data/lib/searchlink/searches/itunes.rb +97 -0
  26. data/lib/searchlink/searches/lastfm.rb +41 -0
  27. data/lib/searchlink/searches/lyrics.rb +91 -0
  28. data/lib/searchlink/searches/pinboard.rb +183 -0
  29. data/lib/searchlink/searches/social.rb +105 -0
  30. data/lib/searchlink/searches/software.rb +27 -0
  31. data/lib/searchlink/searches/spelling.rb +59 -0
  32. data/lib/searchlink/searches/spotlight.rb +28 -0
  33. data/lib/searchlink/searches/stackoverflow.rb +31 -0
  34. data/lib/searchlink/searches/tmdb.rb +52 -0
  35. data/lib/searchlink/searches/twitter.rb +46 -0
  36. data/lib/searchlink/searches/wikipedia.rb +33 -0
  37. data/lib/searchlink/searches/youtube.rb +48 -0
  38. data/lib/searchlink/searches.rb +194 -0
  39. data/lib/searchlink/semver.rb +140 -0
  40. data/lib/searchlink/string.rb +469 -0
  41. data/lib/searchlink/url.rb +153 -0
  42. data/lib/searchlink/util.rb +87 -0
  43. data/lib/searchlink/version.rb +93 -0
  44. data/lib/searchlink/which.rb +175 -0
  45. data/lib/searchlink.rb +66 -0
  46. data/lib/tokens.rb +3 -0
  47. metadata +299 -0
@@ -0,0 +1,105 @@
1
+ module SL
2
+ class SocialSearch
3
+ class << self
4
+ def settings
5
+ {
6
+ trigger: '@[tfilm]',
7
+ searches: [
8
+ ['@t', 'Twitter Handle'],
9
+ ['@f', 'Facebook Handle'],
10
+ ['@i', 'Instagram Handle'],
11
+ ['@l', 'LinkedIn Handle'],
12
+ ['@m', 'Mastodon Handle']
13
+ ]
14
+ }
15
+ end
16
+
17
+ def search(search_type, search_terms, link_text = '')
18
+ type = case search_type
19
+ when /^@t/ # twitter-ify username
20
+ unless search_terms.strip =~ /^@?[0-9a-z_$]+$/i
21
+ return [false, "#{search_terms} is not a valid Twitter handle", link_text]
22
+
23
+ end
24
+
25
+ 't'
26
+ when /^@fb?/ # fb-ify username
27
+ unless search_terms.strip =~ /^@?[0-9a-z_]+$/i
28
+ return [false, "#{search_terms} is not a valid Facebook username", link_text]
29
+
30
+ end
31
+
32
+ 'f'
33
+ when /^@i/ # intagramify username
34
+ unless search_terms.strip =~ /^@?[0-9a-z_]+$/i
35
+ return [false, "#{search_terms} is not a valid Instagram username", link_text]
36
+
37
+ end
38
+
39
+ 'i'
40
+ when /^@l/ # linked-inify username
41
+ unless search_terms.strip =~ /^@?[0-9a-z_]+$/i
42
+ return [false, "#{search_terms} is not a valid LinkedIn username", link_text]
43
+
44
+ end
45
+
46
+ 'l'
47
+ when /^@m/ # mastodonify username
48
+ unless search_terms.strip =~ /^@?[0-9a-z_]+@[0-9a-z_.]+$/i
49
+ return [false, "#{search_terms} is not a valid Mastodon username", link_text]
50
+
51
+ end
52
+
53
+ 'm'
54
+ else
55
+ 't'
56
+ end
57
+
58
+ url, title = social_handle(type, search_terms)
59
+ link_text = title if link_text == ''
60
+ [url, title, link_text]
61
+ end
62
+
63
+ def template_social(user, url, service)
64
+ template = SL.config['social_template'].dup
65
+
66
+ template.sub!(/%user%/, user)
67
+ template.sub!(/%service%/, service)
68
+ template.sub!(/%url%/, url.sub(%r{^https?://(www\.)?}, '').sub(%r{/$}, ''))
69
+
70
+ template
71
+ end
72
+
73
+ def social_handle(type, term)
74
+ handle = term.sub(/^@/, '').strip
75
+
76
+ case type
77
+ when /^t/i
78
+ url = "https://twitter.com/#{handle}"
79
+ title = template_social(handle, url, 'Twitter')
80
+ when /^f/i
81
+ url = "https://www.facebook.com/#{handle}"
82
+ title = template_social(handle, url, 'Facebook')
83
+ when /^l/i
84
+ url = "https://www.linkedin.com/in/#{handle}/"
85
+ title = template_social(handle, url, 'LinkedIn')
86
+ when /^i/i
87
+ url = "https://www.instagram.com/#{handle}/"
88
+ title = template_social(handle, url, 'Instagram')
89
+ when /^m/i
90
+ parts = handle.split(/@/)
91
+ return [false, term] unless parts.count == 2
92
+
93
+ url = "https://#{parts[1]}/@#{parts[0]}"
94
+ title = template_social(handle, url, 'Mastodon')
95
+ else
96
+ [false, term]
97
+ end
98
+
99
+ [url, title]
100
+ end
101
+ end
102
+
103
+ SL::Searches.register 'social', :search, self
104
+ end
105
+ end
@@ -0,0 +1,27 @@
1
+ module SL
2
+ # Software Search
3
+ class SoftwareSearch
4
+ class << self
5
+ def settings
6
+ {
7
+ trigger: 's',
8
+ searches: [
9
+ ['s', 'Software Search']
10
+ ]
11
+ }
12
+ end
13
+
14
+ def search(_, search_terms, link_text)
15
+ excludes = %w[apple.com postmates.com download.cnet.com softpedia.com softonic.com macupdate.com]
16
+ search_url = %(#{excludes.map { |x| "-site:#{x}" }.join(' ')} #{search_terms} app)
17
+
18
+ url, title, link_text = SL.ddg(search_url, link_text)
19
+ link_text = title if link_text == '' && !SL.titleize
20
+
21
+ [url, title, link_text]
22
+ end
23
+ end
24
+
25
+ SL::Searches.register 'software', :search, self
26
+ end
27
+ end
@@ -0,0 +1,59 @@
1
+ module SL
2
+ # Spelling Search
3
+ class SpellSearch
4
+ class << self
5
+ def settings
6
+ {
7
+ trigger: 'sp(?:ell)?',
8
+ searches: [
9
+ %w[sp Spelling],
10
+ ['spell', nil]
11
+ ]
12
+ }
13
+ end
14
+
15
+ def search(_, search_terms, link_text)
16
+ title = SL.spell(search_terms)
17
+
18
+ [title, title, link_text]
19
+ end
20
+ end
21
+
22
+ SL::Searches.register 'spelling', :search, self
23
+ end
24
+
25
+ class << self
26
+ def spell(phrase)
27
+ aspell = if File.exist?('/usr/local/bin/aspell')
28
+ '/usr/local/bin/aspell'
29
+ elsif File.exist?('/opt/homebrew/bin/aspell')
30
+ '/opt/homebrew/bin/aspell'
31
+ else
32
+ `which aspell`.strip
33
+ end
34
+
35
+ if aspell.nil? || aspell.empty?
36
+ SL.add_error('Missing aspell', 'Install aspell in to allow spelling corrections')
37
+ return false
38
+ end
39
+
40
+ words = phrase.split(/\b/)
41
+ output = ''
42
+ words.each do |w|
43
+ if w =~ /[A-Za-z]+/
44
+ spell_res = `echo "#{w}" | #{aspell} --sug-mode=bad-spellers -C pipe | head -n 2 | tail -n 1`
45
+ if spell_res.strip == "\*"
46
+ output += w
47
+ else
48
+ spell_res.sub!(/.*?: /, '')
49
+ results = spell_res.split(/, /).delete_if { |word| phrase =~ /^[a-z]/ && word =~ /[A-Z]/ }
50
+ output += results[0]
51
+ end
52
+ else
53
+ output += w
54
+ end
55
+ end
56
+ output
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,28 @@
1
+ module SL
2
+ # Spotlight file search
3
+ class SpotlightSearch
4
+ class << self
5
+ def settings
6
+ {
7
+ trigger: 'file',
8
+ searches: [
9
+ ['file', 'Spotlight Search']
10
+ ]
11
+ }
12
+ end
13
+
14
+ def search(_, search_terms, link_text)
15
+ query = search_terms.gsub(/%22/, '"')
16
+ matches = `mdfind '#{query}' 2>/dev/null`.strip.split(/\n/)
17
+ res = matches.sort_by { |r| File.basename(r).length }.first
18
+ return [false, query, link_text] if res.strip.empty?
19
+
20
+ title = File.basename(res)
21
+ link_text = title if link_text.strip.empty? || link_text == search_terms
22
+ ["file://#{res.strip.gsub(/ /, '%20')}", title, link_text]
23
+ end
24
+ end
25
+
26
+ SL::Searches.register 'spotlight', :search, self
27
+ end
28
+ end
@@ -0,0 +1,31 @@
1
+ module SL
2
+ # Stack Overflow search
3
+ class StackOverflowSearch
4
+ class << self
5
+ def settings
6
+ {
7
+ trigger: 'soa?',
8
+ searches: [
9
+ ['so', 'StackOverflow Search'],
10
+ ['soa', 'StackOverflow Accepted Answer']
11
+ ]
12
+ }
13
+ end
14
+
15
+ def search(search_type, search_terms, link_text)
16
+ url, title, link_text = SL.ddg("site:stackoverflow.com #{search_terms}", link_text)
17
+ link_text = title if link_text == '' && !SL.titleize
18
+
19
+ if search_type =~ /a$/
20
+ body = `curl -SsL #{url}`.strip
21
+ m = body.match(/id="(?<id>answer-\d+)"[^>]+accepted-answer/)
22
+ url = "#{url}##{m['id']}" if m
23
+ end
24
+
25
+ [url, title, link_text]
26
+ end
27
+ end
28
+
29
+ SL::Searches.register 'stackoverflow', :search, self
30
+ end
31
+ end
@@ -0,0 +1,52 @@
1
+ module SL
2
+ # The Movie Database search
3
+ class TMDBSearch
4
+ class << self
5
+ def settings
6
+ {
7
+ trigger: 'tmdb[amt]?',
8
+ searches: [
9
+ ['tmdb', 'TMDB Multi Search'],
10
+ ['tmdba', 'TMDB Actor Search'],
11
+ ['tmdbm', 'TMDB Movie Search'],
12
+ ['tmdbt', 'TMDB TV Search']
13
+ ]
14
+ }
15
+ end
16
+
17
+ def search(search_type, terms, link_text)
18
+ type = case search_type
19
+ when /t$/
20
+ 'tv'
21
+ when /m$/
22
+ 'movie'
23
+ when /a$/
24
+ 'person'
25
+ else
26
+ 'multi'
27
+ end
28
+ body = `/usr/bin/curl -sSL 'https://api.themoviedb.org/3/search/#{type}?query=#{terms.url_encode}&api_key=2bd76548656d92517f14d64766e87a02'`
29
+ data = JSON.parse(body)
30
+ if data.key?('results') && data['results'].count.positive?
31
+ res = data['results'][0]
32
+ type = res['media_type'] if type == 'multi'
33
+ id = res['id']
34
+ url = "https://www.themoviedb.org/#{type}/#{id}"
35
+ title = res['name']
36
+ title ||= res['title']
37
+ title ||= terms
38
+ else
39
+ url, title, link_text = SL.ddg("site:imdb.com #{terms}", link_text)
40
+
41
+ return false unless url
42
+ end
43
+
44
+ link_text = title if link_text == '' && !SL.titleize
45
+
46
+ [url, title, link_text]
47
+ end
48
+ end
49
+
50
+ SL::Searches.register 'tmdb', :search, self
51
+ end
52
+ end
@@ -0,0 +1,46 @@
1
+ module SL
2
+ class TwitterSearch
3
+ class << self
4
+ def settings
5
+ {
6
+ trigger: 'te',
7
+ searches: [
8
+ ['te', 'Twitter Embed']
9
+ ]
10
+ }
11
+ end
12
+
13
+ def search(search_type, search_terms, link_text)
14
+ if SL::URL.url?(search_terms) && search_terms =~ %r{^https://twitter.com/}
15
+ url, title = twitter_embed(search_terms)
16
+ else
17
+ SL.add_error('Invalid Tweet URL', "#{search_terms} is not a valid link to a tweet or timeline")
18
+ url = false
19
+ title = false
20
+ end
21
+
22
+ [url, title, link_text]
23
+ end
24
+
25
+ def twitter_embed(tweet)
26
+ res = `curl -sSL 'https://publish.twitter.com/oembed?url=#{tweet.url_encode}'`.strip
27
+ if res
28
+ begin
29
+ json = JSON.parse(res)
30
+ url = 'embed'
31
+ title = json['html']
32
+ rescue StandardError
33
+ SL.add_error('Tweet Error', 'Error retrieving tweet')
34
+ url = false
35
+ title = tweet
36
+ end
37
+ else
38
+ return [false, 'Error retrieving tweet']
39
+ end
40
+ return [url, title]
41
+ end
42
+ end
43
+
44
+ SL::Searches.register 'twitter', :search, self
45
+ end
46
+ end
@@ -0,0 +1,33 @@
1
+ module SL
2
+ class WikipediaSearch
3
+ class << self
4
+ def settings
5
+ {
6
+ trigger: 'wiki',
7
+ searches: [
8
+ ['wiki', 'Wikipedia Search']
9
+ ]
10
+ }
11
+ end
12
+
13
+ def search(_, search_terms, link_text)
14
+ ## Hack to scrape wikipedia result
15
+ body = `/usr/bin/curl -sSL 'https://en.wikipedia.org/wiki/Special:Search?search=#{search_terms.url_encode}&go=Go'`
16
+ return false unless body
17
+
18
+ body = body.force_encoding('utf-8') if RUBY_VERSION.to_f > 1.9
19
+
20
+ begin
21
+ title = body.match(/"wgTitle":"(.*?)"/)[1]
22
+ url = body.match(/<link rel="canonical" href="(.*?)"/)[1]
23
+ rescue StandardError
24
+ return false
25
+ end
26
+
27
+ [url, title, link_text]
28
+ end
29
+ end
30
+
31
+ SL::Searches.register 'wikipedia', :search, self
32
+ end
33
+ end
@@ -0,0 +1,48 @@
1
+ module SL
2
+ # YouTube Search/Linking
3
+ class YouTubeSearch
4
+ YOUTUBE_RX = %r{(?:youtu\.be/|youtube\.com/watch\?v=)?(?<id>[a-z0-9_\-]+)$}i.freeze
5
+
6
+ class << self
7
+ def settings
8
+ {
9
+ trigger: 'yte?',
10
+ searches: [
11
+ ['yt', 'YouTube Search'],
12
+ ['yte', 'YouTube Embed']
13
+ ]
14
+ }
15
+ end
16
+
17
+ def search(search_type, search_terms, link_text)
18
+ if SL::URL.url?(search_terms) && search_terms =~ YOUTUBE_RX
19
+ url = search_terms
20
+ elsif search_terms =~ /^[a-z0-9_\-]+$/i
21
+ url = "https://youtube.com/watch?v=#{search_terms}"
22
+ else
23
+ url, title = SL.ddg("site:youtube.com #{search_terms}", link_text)
24
+ end
25
+
26
+ url, title = embed_for_url(url) if search_type =~ /e$/
27
+
28
+ [url, title]
29
+ end
30
+
31
+ def embed_for_url(url)
32
+ return unless url =~ YOUTUBE_RX
33
+
34
+ id = Regexp.last_match('id')
35
+ title = [
36
+ %(<iframe width="560" height="315" src="https://www.youtube.com/embed/#{id}"),
37
+ %(title="YouTube video player" frameborder="0"),
38
+ %(allow="accelerometer; autoplay; clipboard-write; encrypted-media;),
39
+ %(gyroscope; picture-in-picture; web-share"),
40
+ %(allowfullscreen></iframe>)
41
+ ].join(' ')
42
+ ['embed', title]
43
+ end
44
+ end
45
+
46
+ SL::Searches.register 'youtube', :search, self
47
+ end
48
+ end
@@ -0,0 +1,194 @@
1
+ module SL
2
+ module Searches
3
+ class << self
4
+ def plugins
5
+ @plugins ||= {}
6
+ end
7
+
8
+ def load_searches
9
+ Dir.glob(File.join(File.dirname(__FILE__), 'searches', '*.rb')).sort.each { |f| require f }
10
+ end
11
+
12
+ #
13
+ # Register a plugin with the plugin manager
14
+ #
15
+ # @param [String, Array] title title or array of titles
16
+ # @param [Symbol] type plugin type (:search)
17
+ # @param [Class] klass class that handles plugin actions. Search plugins
18
+ # must have a #settings and a #search method
19
+ #
20
+ def register(title, type, klass)
21
+ Array(title).each { |t| register_plugin(t, type, klass) }
22
+ end
23
+
24
+ def description_for_search(search_type)
25
+ description = "#{search_type} search"
26
+ plugins[:search].each do |_, plugin|
27
+ s = plugin[:searches].select { |s| s[0] == search_type }
28
+ unless s.empty?
29
+ description = s[0][1]
30
+ break
31
+ end
32
+ end
33
+ description
34
+ end
35
+
36
+ #
37
+ # Output an HTML table of available searches
38
+ #
39
+ # @return [String] Table HTML
40
+ #
41
+ def available_searches_html
42
+ searches = plugins[:search]
43
+ .flat_map { |_, plugin| plugin[:searches] }
44
+ .reject { |s| s[1].nil? }
45
+ .sort_by { |s| s[0] }
46
+ out = ['<table id="searches">',
47
+ '<thead><td>Shortcut</td><td>Search Type</td></thead>',
48
+ '<tbody>']
49
+ searches.each { |s| out << "<tr><td><code>!#{s[0]}</code></td><td>#{s[1]}</td></tr>" }
50
+ out.concat(['</tbody>', '</table>']).join("\n")
51
+ end
52
+
53
+ #
54
+ # Aligned list of available searches
55
+ #
56
+ # @return [String] Aligned list of searches
57
+ #
58
+ def available_searches
59
+ searches = []
60
+ plugins[:search].each { |_, plugin| searches.concat(plugin[:searches].delete_if { |s| s[1].nil? }) }
61
+ out = ''
62
+ searches.each { |s| out += "!#{s[0]}#{s[0].spacer}#{s[1]}\n" }
63
+ out
64
+ end
65
+
66
+ def best_search_match(term)
67
+ searches = all_possible_searches.dup
68
+ searches.select { |s| s.matches_score(term, separator: '', start_word: false) > 8 }
69
+ end
70
+
71
+ def all_possible_searches
72
+ searches = []
73
+ plugins[:search].each { |_, plugin| plugin[:searches].each { |s| searches.push(s[0]) } }
74
+ searches.concat(SL.config['custom_site_searches'].keys)
75
+ end
76
+
77
+ def did_you_mean(term)
78
+ matches = best_search_match(term)
79
+ matches.empty? ? '' : ", did you mean #{matches.map { |m| "!#{m}" }.join(', ')}?"
80
+ end
81
+
82
+ def valid_searches
83
+ searches = []
84
+ plugins[:search].each { |_, plugin| searches.push(plugin[:trigger]) }
85
+ searches
86
+ end
87
+
88
+ def valid_search?(term)
89
+ valid = false
90
+ valid = true if term =~ /^(#{valid_searches.join('|')})$/
91
+ valid = true if SL.config['custom_site_searches'].keys.include? term
92
+ # SL.notify("Invalid search#{did_you_mean(term)}", term) unless valid
93
+ valid
94
+ end
95
+
96
+ def register_plugin(title, type, klass)
97
+ raise StandardError, "Plugin #{title} has no settings method" unless klass.respond_to? :settings
98
+
99
+ settings = klass.settings
100
+
101
+ raise StandardError, "Plugin #{title} has no search method" unless klass.respond_to? :search
102
+
103
+ plugins[type] ||= {}
104
+ plugins[type][title] = {
105
+ trigger: settings.fetch(:trigger, title).normalize_trigger,
106
+ searches: settings[:searches],
107
+ class: klass
108
+ }
109
+ end
110
+
111
+ def load_custom
112
+ plugins_folder = File.expand_path('~/.local/searchlink/plugins')
113
+ return unless File.directory?(plugins_folder)
114
+
115
+ Dir.glob(File.join(plugins_folder, '**/*.rb')).sort.each do |plugin|
116
+ require plugin
117
+ end
118
+ end
119
+
120
+ def do_search(search_type, search_terms, link_text, timeout: SL.config['timeout'])
121
+ plugins[:search].each do |_title, plugin|
122
+ trigger = plugin[:trigger].gsub(/(^\^|\$$)/, '')
123
+ if search_type =~ /^#{trigger}$/
124
+ search = proc { plugin[:class].search(search_type, search_terms, link_text) }
125
+ return SL::Util.search_with_timeout(search, timeout)
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
132
+
133
+ # import
134
+ require_relative 'searches/applemusic'
135
+
136
+ # import
137
+ require_relative 'searches/itunes'
138
+
139
+ # import
140
+ require_relative 'searches/amazon'
141
+
142
+ # import
143
+ require_relative 'searches/bitly'
144
+
145
+ # import
146
+ require_relative 'searches/definition'
147
+
148
+ # import
149
+ require_relative 'searches/duckduckgo'
150
+
151
+ # import
152
+ require_relative 'searches/github'
153
+
154
+ # import
155
+ require_relative 'searches/google'
156
+
157
+ # import
158
+ require_relative 'searches/history'
159
+
160
+ # import
161
+ require_relative 'searches/hook'
162
+
163
+ # import
164
+ require_relative 'searches/lastfm'
165
+
166
+ # import
167
+ require_relative 'searches/pinboard'
168
+
169
+ # import
170
+ require_relative 'searches/social'
171
+
172
+ # import
173
+ require_relative 'searches/software'
174
+
175
+ # import
176
+ require_relative 'searches/spelling'
177
+
178
+ # import
179
+ require_relative 'searches/spotlight'
180
+
181
+ # import
182
+ require_relative 'searches/tmdb'
183
+
184
+ # import
185
+ require_relative 'searches/twitter'
186
+
187
+ # import
188
+ require_relative 'searches/wikipedia'
189
+
190
+ # import
191
+ require_relative 'searches/youtube'
192
+
193
+ # import
194
+ require_relative 'searches/stackoverflow'