searchlink 2.3.59

Sign up to get free protection for your applications and to get access to all the features.
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'