pincerna 1.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (102) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +7 -0
  3. data/.rubocop.yml +77 -0
  4. data/.travis-gemfile +17 -0
  5. data/.travis.yml +7 -0
  6. data/.yardopts +1 -0
  7. data/CHANGELOG.md +3 -0
  8. data/Gemfile +23 -0
  9. data/LICENSE.md +21 -0
  10. data/README.md +211 -0
  11. data/Rakefile +45 -0
  12. data/bin/pincernad +56 -0
  13. data/docs/Pincerna.html +130 -0
  14. data/docs/Pincerna/Base.html +3051 -0
  15. data/docs/Pincerna/Bookmark.html +523 -0
  16. data/docs/Pincerna/Cache.html +767 -0
  17. data/docs/Pincerna/ChromeBookmark.html +308 -0
  18. data/docs/Pincerna/CurrencyConversion.html +589 -0
  19. data/docs/Pincerna/FirefoxBookmark.html +328 -0
  20. data/docs/Pincerna/Ip.html +1017 -0
  21. data/docs/Pincerna/Map.html +399 -0
  22. data/docs/Pincerna/SafariBookmark.html +308 -0
  23. data/docs/Pincerna/Server.html +673 -0
  24. data/docs/Pincerna/Translation.html +517 -0
  25. data/docs/Pincerna/UnitConversion.html +1042 -0
  26. data/docs/Pincerna/Version.html +189 -0
  27. data/docs/Pincerna/Vpn.html +561 -0
  28. data/docs/Pincerna/Weather.html +837 -0
  29. data/docs/_index.html +298 -0
  30. data/docs/class_list.html +54 -0
  31. data/docs/css/common.css +1 -0
  32. data/docs/css/full_list.css +57 -0
  33. data/docs/css/style.css +338 -0
  34. data/docs/file.README.html +327 -0
  35. data/docs/file_list.html +56 -0
  36. data/docs/frames.html +28 -0
  37. data/docs/index.html +327 -0
  38. data/docs/js/app.js +214 -0
  39. data/docs/js/full_list.js +178 -0
  40. data/docs/js/jquery.js +4 -0
  41. data/docs/method_list.html +389 -0
  42. data/docs/top-level-namespace.html +112 -0
  43. data/icon.png +0 -0
  44. data/images/chrome.png +0 -0
  45. data/images/currency.png +0 -0
  46. data/images/firefox.png +0 -0
  47. data/images/map.png +0 -0
  48. data/images/network.png +0 -0
  49. data/images/safari.png +0 -0
  50. data/images/translate.png +0 -0
  51. data/images/unit.png +0 -0
  52. data/images/vpn.png +0 -0
  53. data/images/weather.png +0 -0
  54. data/info.plist +961 -0
  55. data/it.cowtech.pincernad.plist +19 -0
  56. data/lib/pincerna.rb +30 -0
  57. data/lib/pincerna/base.rb +258 -0
  58. data/lib/pincerna/bookmark.rb +80 -0
  59. data/lib/pincerna/cache.rb +61 -0
  60. data/lib/pincerna/chrome_bookmark.rb +40 -0
  61. data/lib/pincerna/currency_conversion.rb +134 -0
  62. data/lib/pincerna/firefox_bookmark.rb +92 -0
  63. data/lib/pincerna/ip.rb +135 -0
  64. data/lib/pincerna/map.rb +30 -0
  65. data/lib/pincerna/safari_bookmark.rb +40 -0
  66. data/lib/pincerna/server.rb +85 -0
  67. data/lib/pincerna/translation.rb +67 -0
  68. data/lib/pincerna/unit_conversion.rb +120 -0
  69. data/lib/pincerna/version.rb +24 -0
  70. data/lib/pincerna/vpn.rb +74 -0
  71. data/lib/pincerna/weather.rb +188 -0
  72. data/pincerna.alfredworkflow +0 -0
  73. data/pincerna.gemspec +36 -0
  74. data/pincerna.sh +9 -0
  75. data/spec/cassettes/Pincerna_CurrencyConversion/_perform_filtering/should_return_valid_values.yml +38 -0
  76. data/spec/cassettes/Pincerna_Ip/_get_local_addresses/should_return_a_list_of_addresses.yml +47 -0
  77. data/spec/cassettes/Pincerna_Ip/_get_public_address/should_return_public_IP_address.yml +44 -0
  78. data/spec/cassettes/Pincerna_Translation/_perform_filtering/should_default_from_English_to_the_given_language_when_only_one_is_present.yml +124 -0
  79. data/spec/cassettes/Pincerna_Translation/_perform_filtering/should_query_Google_Translate_for_sentences_returning_no_alternatives.yml +51 -0
  80. data/spec/cassettes/Pincerna_Translation/_perform_filtering/should_query_Google_Translate_for_single_words.yml +71 -0
  81. data/spec/cassettes/Pincerna_Weather/_get_forecast/should_append_name.yml +177 -0
  82. data/spec/cassettes/Pincerna_Weather/_get_forecast/should_get_correct_forecasts.yml +339 -0
  83. data/spec/cassettes/Pincerna_Weather/_lookup_places/should_return_an_existing_WOEID_without_making_any_request.yml +56 -0
  84. data/spec/cassettes/Pincerna_Weather/_lookup_places/should_search_for_places.yml +189 -0
  85. data/spec/cassettes/Pincerna_Weather/_perform_filtering/should_get_forecast.yml +56 -0
  86. data/spec/coverage_helper.rb +44 -0
  87. data/spec/pincerna/base_spec.rb +166 -0
  88. data/spec/pincerna/bookmark_spec.rb +65 -0
  89. data/spec/pincerna/cache_spec.rb +88 -0
  90. data/spec/pincerna/chrome_bookmark_spec.rb +114 -0
  91. data/spec/pincerna/currency_conversion_spec.rb +46 -0
  92. data/spec/pincerna/firefox_bookmark_spec.rb +46 -0
  93. data/spec/pincerna/ip_spec.rb +194 -0
  94. data/spec/pincerna/map_spec.rb +24 -0
  95. data/spec/pincerna/safari_bookmark_spec.rb +237 -0
  96. data/spec/pincerna/server_spec.rb +108 -0
  97. data/spec/pincerna/translation_spec.rb +55 -0
  98. data/spec/pincerna/unit_conversion_spec.rb +98 -0
  99. data/spec/pincerna/vpn_spec.rb +68 -0
  100. data/spec/pincerna/weather_spec.rb +131 -0
  101. data/spec/spec_helper.rb +48 -0
  102. metadata +283 -0
@@ -0,0 +1,19 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>Label</key>
6
+ <string>it.cowtech.pincernad</string>
7
+ <key>KeepAlive</key>
8
+ <true/>
9
+ <key>RunAtLoad</key>
10
+ <true/>
11
+ <key>EnableGlobbing</key>
12
+ <true/>
13
+ <key>ProgramArguments</key>
14
+ <array>
15
+ <string>~/.rvm/bin/pincernad</string>
16
+ <string>-e production</string>
17
+ </array>
18
+ </dict>
19
+ </plist>
@@ -0,0 +1,30 @@
1
+ # encoding: utf-8
2
+ #
3
+ # This file is part of the pincerna gem. Copyright (C) 2013 and above Shogun <shogun@cowtech.it>.
4
+ # Licensed under the MIT license, which can be found at https://choosealicense.com/licenses/mit.
5
+ #
6
+
7
+ require "open-uri"
8
+ require "strscan"
9
+ require "cgi"
10
+ require "fileutils"
11
+ require "nokogiri"
12
+ require "oj"
13
+ require "yahoo_weatherman"
14
+ require "ruby-units"
15
+ require "plist"
16
+ require "daybreak"
17
+ require "em-synchrony"
18
+ require "em-synchrony/em-http"
19
+
20
+ require "pincerna/version" if !defined?(Pincerna::Version)
21
+ require "pincerna/base"
22
+ require "pincerna/cache"
23
+ require "pincerna/currency_conversion"
24
+ require "pincerna/ip"
25
+ require "pincerna/map"
26
+ require "pincerna/translation"
27
+ require "pincerna/unit_conversion"
28
+ require "pincerna/vpn"
29
+ require "pincerna/weather"
30
+ require "pincerna/bookmark"
@@ -0,0 +1,258 @@
1
+ # encoding: utf-8
2
+ #
3
+ # This file is part of the pincerna gem. Copyright (C) 2013 and above Shogun <shogun@cowtech.it>.
4
+ # Licensed under the MIT license, which can be found at https://choosealicense.com/licenses/mit.
5
+ #
6
+
7
+ # A bunch of useful Alfred 2 workflows.
8
+ module Pincerna
9
+ # Base class for all filter.
10
+ #
11
+ # @attribute [r] output
12
+ # @return [String] The output of filtering.
13
+ # @attribute [r] format
14
+ # @return [Symbol] The format of output. Can be `:xml` (default) or `:yml`.
15
+ # @attribute [r] format_content_type
16
+ # @return [String] The content type of the format. Can be `text/xml` (default) or `text/x-yaml`.
17
+ class Base
18
+ # The expression to match.
19
+ MATCHER = /^(?<all>.*)$/i
20
+
21
+ # Relevant groups in the match.
22
+ RELEVANT_MATCHES = {
23
+ "all" => ->(_, value) { value }
24
+ }
25
+
26
+ # Recognized types of filtering
27
+ TYPES = {
28
+ "unit_conversion" => /^(convert|unit|c)$/,
29
+ "currency_conversion" => /^(currency|cc)$/,
30
+ "translation" => /^(translate|t)$/,
31
+ "map" => /^(map|m)$/,
32
+ "weather" => /^(forecast|weather)$/,
33
+ "ip" => /^ip$/,
34
+ "vpn" => /^vpn$/,
35
+ "chrome_bookmark" => /^(chrome-bookmark|bc)$/,
36
+ "safari_bookmark" => /^(safari-bookmark|bs)$/,
37
+ "firefox_bookmark" => /^(firefox-bookmark|bf)$/
38
+ }
39
+
40
+ # The full name of the gem
41
+ FULL_NAME = "it.cowtech.pincerna"
42
+
43
+ # The root of the pincerna gem
44
+ ROOT = File.expand_path(File.dirname(__FILE__) + "/../../")
45
+
46
+ # The root of the cache
47
+ CACHE_ROOT = File.expand_path("~/Library/Caches/com.runningwithcrayons.Alfred-2/Workflow Data/#{FULL_NAME}")
48
+
49
+ # The root of alfred workflows
50
+ WORKFLOW_ROOT = File.expand_path("~/Library/Application Support/Alfred 2/Alfred.alfredpreferences/workflows/#{FULL_NAME}")
51
+
52
+ attr_reader :output, :format, :format_content_type
53
+
54
+ # Executes a filtering query.
55
+ #
56
+ # @param type [Symbol] The type of the query.
57
+ # @param query [String] The argument of the query.
58
+ # @param format [String] The format to use. Valid values are `:xml` (default) and `:yml`.
59
+ # @param debug [String] The debug mode.
60
+ # @return [String] The result of the query.
61
+ def self.execute!(type, query, format = :xml, debug = nil)
62
+ instance = nil
63
+
64
+ type = catch(:type) do
65
+ TYPES.each do |file, matcher|
66
+ throw(:type, file) if type =~ matcher
67
+ end
68
+
69
+ nil
70
+ end
71
+
72
+ if type
73
+ instance = find_class(type).new(query, format, debug)
74
+ instance.filter
75
+ end
76
+
77
+ instance
78
+ end
79
+
80
+ # Creates a new query.
81
+ #
82
+ # @param query [String] The argument of the query.
83
+ # @param requested_format [String] The format to use. Valid values are `xml` (default), `yaml` or `yml`.
84
+ # @param debug [String] The debug mode.
85
+ def initialize(query, requested_format = "xml", debug = nil)
86
+ @query = query.strip.gsub("\\ ", " ")
87
+ @cache_dir = CACHE_ROOT
88
+
89
+ if requested_format =~ /^y(a?)ml$/i then
90
+ @format = :yml
91
+ @format_content_type = "text/x-yaml"
92
+ else
93
+ @format = :xml
94
+ @format_content_type = "text/xml"
95
+ end
96
+
97
+ @debug = debug
98
+ @feedback_items = []
99
+ end
100
+
101
+ # Filters a query.
102
+ #
103
+ # @return [String] The feedback items of the query, formatted as XML.
104
+ def filter
105
+ # Match the query
106
+ matches = self.class::MATCHER.match(@query)
107
+
108
+ if matches then
109
+ # Execute the filtering
110
+ results = execute_filtering(matches)
111
+
112
+ # Show results if appropriate
113
+ process_results(results).each {|r| add_feedback_item(r) } if !results.empty?
114
+ end
115
+
116
+ output_feedback
117
+ end
118
+
119
+ # Filters a query.
120
+ #
121
+ # @param args [Array] The arguments of the query.
122
+ # @return [Array] A list of items to process.
123
+ def perform_filtering(*args)
124
+ raise ArgumentError.new("Must be overriden by subclasses.")
125
+ end
126
+
127
+ # Processes items to obtain feedback items.
128
+ #
129
+ # @param results [Array] The items to process.
130
+ # @return [Array] The feedback items.
131
+ def process_results(results)
132
+ raise ArgumentError.new("Must be overriden by subclasses.")
133
+ end
134
+
135
+ # Adds a feedback items.
136
+ #
137
+ # @param item [Array] The items to add.
138
+ def add_feedback_item(item)
139
+ @feedback_items << item
140
+ end
141
+
142
+ # Prepares the feedback for output.
143
+ def output_feedback
144
+ if format == :xml then
145
+ @output = Nokogiri::XML::Builder.new { |xml|
146
+ xml.items do
147
+ @feedback_items.each do |item|
148
+ childs, attributes = split_output_item(item)
149
+
150
+ xml.item(attributes) do
151
+ childs.each { |name, value| xml.send(name, value) }
152
+ end
153
+ end
154
+ end
155
+ }.to_xml
156
+ else
157
+ @output = @feedback_items.to_yaml
158
+ end
159
+ end
160
+
161
+ # Rounds a float to a certain precision.
162
+ #
163
+ # @param value [Float] The value to convert.
164
+ # @param precision [Fixnum] The precision to use.
165
+ # @return [Float] The rounded value.
166
+ def round_float(value, precision = 3)
167
+ factor = 10**precision
168
+ (value * factor).round.to_f / factor
169
+ end
170
+
171
+ # Rounds a float to a certain precision and prints it as a string. Unneeded leading zero are remove.
172
+ #
173
+ # @param value [Float] The value to print.
174
+ # @param precision [Fixnum] The precision to use.
175
+ # @return [String] The formatted value.
176
+ def format_float(value, precision = 3)
177
+ value = round_float(value, precision)
178
+ value = "%0.#{precision}f" % value
179
+ value.gsub(/\.?0+$/, "")
180
+ end
181
+
182
+ protected
183
+ # Instantiates the new class.
184
+ #
185
+ # @param file [String] The file name.
186
+ def self.find_class(file)
187
+ Pincerna.const_get(file.capitalize.gsub(/_(.)/) { $1.upcase})
188
+ end
189
+
190
+ # Executes filtering on matched data.
191
+ #
192
+ # @param matches [MatchData] The matched data.
193
+ # @return [Array] The results of filtering.
194
+ def execute_filtering(matches)
195
+ # Get relevant matches and arguments.
196
+ relevant = self.class::RELEVANT_MATCHES
197
+ args = relevant.map {|key, value| value.call(self, matches[key]) }
198
+
199
+ # Now perform the operation
200
+ begin
201
+ perform_filtering(*args)
202
+ rescue => e
203
+ raise e if debug_mode == :error
204
+ []
205
+ end
206
+ end
207
+
208
+ # Converts an array of key-value pairs to an hash.
209
+ #
210
+ # @param array [Array] The array to convert.
211
+ # @return [Hash] The converted hash.
212
+ def array_to_hash(array)
213
+ array.reduce({}){ |rv, entry|
214
+ rv[entry[0]] = entry[1]
215
+ rv
216
+ }
217
+ end
218
+
219
+ # Gets attributes and children for output.
220
+ #
221
+ # @param item [Hash] The output item.
222
+ # @return [Array] An array with children and attributes.
223
+ def split_output_item(item)
224
+ item.partition {|k, _| [:title, :subtitle, :icon].include?(k) }.map {|a| array_to_hash(a) }
225
+ end
226
+
227
+ # Fetches remote JSON resource.
228
+ #
229
+ # @param url [String] The URL to get.
230
+ # @param params [Hash] The parameters to pass to the server.
231
+ # @param json [Boolean] If the response is a JSON object.
232
+ # @return [Hash] The server's response.
233
+ def fetch_remote_resource(url, params, json = true)
234
+ args = {query: params}
235
+ args[:head] = {"accept" => "application/json"} if json
236
+ response = EM::HttpRequest.new(url, {connect_timeout: 5}).get(args).response
237
+ json ? Oj.load(response) : response
238
+ end
239
+
240
+ # Executes a command and returns its output.
241
+ #
242
+ # @param args [Array] The command to execute, with its arguments.
243
+ # @return [String] The output of the command
244
+ def execute_command(*args)
245
+ rv = ""
246
+ IO.popen(args) {|f| rv = f.read }
247
+ rv
248
+ end
249
+
250
+ # Returns the current debug mode.
251
+ #
252
+ # @return [Boolean|NilClass] The current debug mode.
253
+ def debug_mode
254
+ mode = ENV["PINCERNA_DEBUG"] || @debug
255
+ !mode.nil? ? mode.to_sym : nil
256
+ end
257
+ end
258
+ end
@@ -0,0 +1,80 @@
1
+ # encoding: utf-8
2
+ #
3
+ # This file is part of the pincerna gem. Copyright (C) 2013 and above Shogun <shogun@cowtech.it>.
4
+ # Licensed under the MIT license, which can be found at https://choosealicense.com/licenses/mit.
5
+ #
6
+
7
+ module Pincerna
8
+ # Show the list of Safari bookmarks.
9
+ class Bookmark < Base
10
+ # The expression to match.
11
+ MATCHER = /^(?<all>.*)$/i
12
+
13
+ # The icon to show for each feedback item.
14
+ ICON = Pincerna::Base::ROOT + "/images/safari.png"
15
+
16
+ # The separator for paths.
17
+ SEPARATOR = "\u2192"
18
+
19
+ # Reads the list of Chrome Bookmarks.
20
+ #
21
+ # @param query [Array] A query to match against bookmarks names.
22
+ # @return [Array] A list of boomarks.
23
+ def perform_filtering(query)
24
+ Pincerna::Cache.instance.use("bookmarks:#{self.class.to_s}", Pincerna::Cache::EXPIRATIONS[:short]) do
25
+ # Get bookmarks and then only keep valid ones
26
+ @bookmarks = []
27
+ read_bookmarks
28
+ filter_and_sort(@bookmarks, query)
29
+ end
30
+ end
31
+
32
+ # Processes items to obtain feedback items.
33
+ #
34
+ # @param results [Array] The items to process.
35
+ # @return [Array] The feedback items.
36
+ def process_results(results)
37
+ results.map do |result|
38
+ subtitle = result[:path] ? result[:path].gsub(/^\s\u2192 /, "") : "Action this item to open the URL in the browser ..."
39
+ {title: result[:name], arg: result[:url], subtitle: subtitle, icon: ICON}
40
+ end
41
+ end
42
+
43
+ # Reads the list of bookmarks.
44
+ def read_bookmarks
45
+ raise ArgumentError.new("Must be overriden by subclasses.")
46
+ end
47
+
48
+ private
49
+ # Filters and sorts bookmarks.
50
+ #
51
+ # @param bookmarks [Array] The bookmarks list.
52
+ # @param query [String] The query to filter.
53
+ def filter_and_sort(bookmarks, query)
54
+ # Filtering
55
+ matcher = !query.empty? ? /^#{Regexp.escape(query.downcase)}/i : nil
56
+ bookmarks = bookmarks.select {|bookmark| bookmark[:name] =~ matcher } if matcher
57
+
58
+ # Sorting
59
+ bookmarks.sort {|first, second|
60
+ cmp = first[:name] <=> second[:name]
61
+ cmp = first[:path] <=> second[:path] if cmp == 0
62
+ cmp
63
+ }
64
+ end
65
+
66
+ protected
67
+ # Adds a bookmarks to the list.
68
+ #
69
+ # @param title [String] The title of the bookmark.
70
+ # @param url [String] The URL of the bookmark.
71
+ # @param path [String] The id of the parent folder.
72
+ def add_bookmark(title, url, path)
73
+ @bookmarks << {name: title, url: url, path: path} if title && !title.empty? && url && !url.empty? && url !~ /^javascript:/
74
+ end
75
+ end
76
+ end
77
+
78
+ require "pincerna/safari_bookmark"
79
+ require "pincerna/firefox_bookmark"
80
+ require "pincerna/chrome_bookmark"
@@ -0,0 +1,61 @@
1
+ # encoding: utf-8
2
+ #
3
+ # This file is part of the pincerna gem. Copyright (C) 2013 and above Shogun <shogun@cowtech.it>.
4
+ # Licensed under the MIT license, which can be found at https://choosealicense.com/licenses/mit.
5
+ #
6
+
7
+ module Pincerna
8
+ # A utility class to handle caching.
9
+ #
10
+ # @attribute [r] data
11
+ # @return [Daybreak::DB] The cache store.
12
+ class Cache
13
+ # Expiration of keys.
14
+ EXPIRATIONS = {short: 1800, long: 2592000} # 30 min, 1 month
15
+
16
+ # Location of the cache file
17
+ FILE = Pincerna::Base::CACHE_ROOT + "/cache.db"
18
+
19
+ attr_reader :data
20
+
21
+ # Returns the instance of the cache.
22
+ def self.instance
23
+ @instance ||= Pincerna::Cache.new
24
+ end
25
+
26
+ # Creates a new cache object.
27
+ def initialize
28
+ FileUtils.mkdir_p(File.dirname(FILE))
29
+ @data = Daybreak::DB.new(FILE)
30
+ @flusher = EM.add_periodic_timer(5) { Pincerna::Cache.instance.flush }
31
+ end
32
+
33
+ # Closes the cache data.
34
+ def destroy
35
+ @flusher.cancel
36
+ @data.close rescue nil
37
+ end
38
+
39
+ # Flush data into disk.
40
+ def flush
41
+ @data.flush
42
+ @data.compact
43
+ end
44
+
45
+ # Use data from cache or fetches new data.
46
+ #
47
+ # @param key [String] The key of the data.
48
+ # @param expiration [Fixnum] Expiration of new data, in seconds.
49
+ def use(key, expiration)
50
+ value = @data[key]
51
+
52
+ if !value || Time.now.to_f > value[:expiration] then
53
+ data = yield
54
+ @data[key] = {data: data, expiration: Time.now.to_f + expiration}
55
+ data
56
+ else
57
+ value[:data]
58
+ end
59
+ end
60
+ end
61
+ end