pincerna 1.1.3

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 (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,40 @@
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 Chrome bookmarks.
9
+ class ChromeBookmark < Bookmark
10
+ # The icon to show for each feedback item.
11
+ ICON = Pincerna::Base::ROOT + "/images/chrome.png"
12
+
13
+ # The location of the bookmarks data
14
+ BOOKMARKS_DATA = File.expand_path("~/Library/Application Support/Google/Chrome/Default/Bookmarks")
15
+
16
+ # Reads the list of Chrome Bookmarks.
17
+ def read_bookmarks
18
+ data = File.read(BOOKMARKS_DATA) rescue nil
19
+
20
+ if data then
21
+ Oj.load(data)["roots"].each do |_, root|
22
+ scan_folder(root, "") if root.is_a?(Hash)
23
+ end
24
+ end
25
+ end
26
+
27
+ private
28
+ # Scans a folder of bookmarks.
29
+ #
30
+ # @param node [Hash] The directory to visit.
31
+ # @param path [String] The path of this node.
32
+ def scan_folder(node, path)
33
+ path += " #{SEPARATOR} #{node["name"]}"
34
+
35
+ node["children"].each do |children|
36
+ children["type"] == "url" ? add_bookmark(children["name"], children["url"], path) : scan_folder(children, path)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,134 @@
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
+ # Converts a value from a currency to another.
9
+ class CurrencyConversion < Base
10
+ # The expression to match.
11
+ MATCHER = /^
12
+ (?<value>([+-]?)(\d+)([.,]\d+)?)
13
+ \s
14
+ (?<from>\S{1,3})
15
+ \s+
16
+ (to\s+)?
17
+ (?<to>\S{1,3})
18
+ (?<rate>\swith\srate)?
19
+ $/mix
20
+
21
+ # Relevant groups in the match.
22
+ RELEVANT_MATCHES = {
23
+ "value" => ->(context, value){ context.round_float(value.gsub(",", ".").to_f) },
24
+ "from" => ->(_, value) { value.upcase },
25
+ "to" => ->(_, value) { value.upcase },
26
+ "rate" => ->(_, value) { !value.nil? } # If show conversion rate
27
+ }
28
+
29
+ # The icon to show for each feedback item.
30
+ ICON = Pincerna::Base::ROOT + "/images/currency.png"
31
+
32
+ # The URL of the webservice.
33
+ URL = "http://rate-exchange.appspot.com/currency"
34
+
35
+ # A list of symbols and their associated ISO codes
36
+ SYMBOLS = {
37
+ "Lek" => "ALL",
38
+ "؋" => "AFN",
39
+ "$" => "USD",
40
+ "ƒ" => "ANG",
41
+ "ман" => "AZN",
42
+ "p." => "BYR",
43
+ "BZ$" => "BZD",
44
+ "$b" => "BOB",
45
+ "KM" => "BAM",
46
+ "P" => "BWP",
47
+ "лв" => "UZS",
48
+ "R$" => "BRL",
49
+ "៛" => "KHR",
50
+ "¥" => "JPY",
51
+ "₡" => "CRC",
52
+ "kn" => "HRK",
53
+ "₱" => "PHP",
54
+ "Kč" => "CZK",
55
+ "kr" => "SEK",
56
+ "RD$" => "DOP",
57
+ "£" => "GBP",
58
+ "€" => "EUR",
59
+ "¢" => "GHC",
60
+ "Q" => "GTQ",
61
+ "L" => "HNL",
62
+ "Ft" => "HUF",
63
+ "" => "TRY",
64
+ "Rp" => "IDR",
65
+ "﷼" => "YER",
66
+ "₪" => "ILS",
67
+ "J$" => "JMD",
68
+ "₩" => "KRW",
69
+ "₭" => "LAK",
70
+ "Ls" => "LVL",
71
+ "Lt" => "LTL",
72
+ "ден" => "MKD",
73
+ "RM" => "MYR",
74
+ "₨" => "LKR",
75
+ "₮" => "MNT",
76
+ "MT" => "MZN",
77
+ "C$" => "NIO",
78
+ "₦" => "NGN",
79
+ "B/." => "PAB",
80
+ "Gs" => "PYG",
81
+ "S/." => "PEN",
82
+ "zł" => "PLN",
83
+ "lei" => "RON",
84
+ "руб" => "RUB",
85
+ "Дин." => "RSD",
86
+ "S" => "SOS",
87
+ "R" => "ZAR",
88
+ "CHF" => "CHF",
89
+ "NT$" => "TWD",
90
+ "฿" => "THB",
91
+ "TT$" => "TTD",
92
+ "₤" => "TRL",
93
+ "₴" => "UAH",
94
+ "$U" => "UYU",
95
+ "Bs" => "VEF",
96
+ "₫" => "VND",
97
+ "Z$" => "ZWD"
98
+ }
99
+
100
+ # Converts a value from a currency to another.
101
+ #
102
+ # @param value [Float] The value to convert.
103
+ # @param from [String] The origin currency.
104
+ # @param to [String] The target currency.
105
+ # @param with_rate [Boolean] If to return the conversion rate in the results.
106
+ # @return [Hash|NilClass] The converted data or `nil` if the conversion failed.
107
+ def perform_filtering(value, from, to, with_rate)
108
+ from = replace_symbol(from)
109
+ to = replace_symbol(to)
110
+ response = fetch_remote_resource(URL, {q: value, from: from, to: to})
111
+ {value: value, from: from, to: to, result: round_float(response["v"]), rate: round_float(response["rate"]), with_rate: with_rate}
112
+ end
113
+
114
+ # Processes items to obtain feedback items.
115
+ #
116
+ # @param results [Hash] The item to process.
117
+ # @return [Array] The feedback items.
118
+ def process_results(results)
119
+ title = "%s %s = %s %s" % [format_float(results[:value]), results[:from], format_float(results[:result]), results[:to]]
120
+ title << " (1 %s = %s %s)" % [results[:from], format_float(results[:rate]), results[:to]] if results[:with_rate]
121
+
122
+ [{title: title, arg: format_float(results[:value]), subtitle: "Action this item to copy the converted amount on the clipboard.", icon: self.class::ICON}]
123
+ end
124
+
125
+ private
126
+ # Replaces a currency symbol with its corresponding ISO code.
127
+ #
128
+ # @param symbol [String] The symbol to replace.
129
+ # @return [String] The corresponding code. If none is found, the original symbol is returned.
130
+ def replace_symbol(symbol)
131
+ SYMBOLS.fetch(symbol, symbol)
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,92 @@
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 Firefox bookmarks.
9
+ class FirefoxBookmark < Bookmark
10
+ # The icon to show for each feedback item.
11
+ ICON = Pincerna::Base::ROOT + "/images/firefox.png"
12
+
13
+ # A wildcard to searc the default profile
14
+ PROFILES_SEARCH = File.expand_path("~/Library/Application Support/Firefox/Profiles/*.default")
15
+
16
+ # The queries to obtain the bookmarks
17
+ QUERIES = [
18
+ "SELECT b.title, p.url, b.parent FROM moz_bookmarks b, moz_places p WHERE b.type=1 AND b.fk=p.id",
19
+ "SELECT b.title, b.id, b.parent FROM moz_bookmarks b WHERE b.type=2"
20
+ ]
21
+
22
+ # Reads the list of Firefox Bookmarks.
23
+ def read_bookmarks
24
+ path = Dir.glob(PROFILES_SEARCH).first
25
+ data = execute_command("/usr/bin/sqlite3", "-echo", "#{path}/places.sqlite", QUERIES.join("; "))
26
+
27
+ if data && !data.empty? then
28
+ @folders = {}
29
+ parse_bookmarks_data(data)
30
+ build_paths
31
+ end
32
+ end
33
+
34
+ private
35
+ # Parses bookmarks data.
36
+ #
37
+ # @param data [String] The data to parse.
38
+ def parse_bookmarks_data(data)
39
+ data = StringScanner.new(data)
40
+ data.skip_until(/\n/) # Discard the first line
41
+
42
+ # While we're still in the first query, look for bookmarks
43
+ while data.exist?(/#{QUERIES.last}/) do
44
+ line = data.scan_until(/\n/).strip
45
+ break if line == QUERIES.last
46
+ add_bookmark(*restrict_array(line.split("|"), 3))
47
+ end
48
+
49
+ # Now look for folder
50
+ while !data.eos? do
51
+ line = data.scan_until(/\n/).strip
52
+ add_folder(*restrict_array(line.split("|"), 3))
53
+ end
54
+ end
55
+
56
+ # Builds the paths of the bookmarks.
57
+ def build_paths
58
+ @bookmarks.map! do |bookmark|
59
+ bookmark[:path] = " #{SEPARATOR} #{build_path(bookmark[:path].to_i).reverse.join(" #{SEPARATOR} ")}"
60
+ bookmark
61
+ end
62
+ end
63
+
64
+ # Builds the full path of a folder.
65
+ #
66
+ # @param id [Fixnum] The id of the folder.
67
+ # @return [String] The path of the folder.
68
+ def build_path(id)
69
+ folder = @folders[id]
70
+ folder ? [folder[0]] + build_path(folder[1]).compact : []
71
+ end
72
+
73
+ # Adds a folder to the list.
74
+ #
75
+ # @param title [String] The name of the folder.
76
+ # @param id [String] The id of the folder.
77
+ # @param parent [String] The id of the parent folder.
78
+ def add_folder(title, id, parent)
79
+ @folders[id.to_i] = [title, parent.to_i] if !title.empty?
80
+ end
81
+
82
+ # Restrict array making sure it does not exceed a length.
83
+ #
84
+ # @param array [Array] The array to restrict.
85
+ # @param len [Fixnum] The maximum allowed length.
86
+ # @return [Array] The restricted array.
87
+ def restrict_array(array, len)
88
+ array[0] = "#{array.shift}|#{array[0]}" while array.length > len
89
+ array
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,135 @@
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
+ # Shows the IP addresses of all network interfaces.
9
+ class Ip < 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/network.png"
15
+
16
+ # The URL of the webservice.
17
+ URL = "http://api.externalip.net/ip"
18
+
19
+ # Shows the IP addresses of all network interfaces.
20
+ #
21
+ # @param query [Array] A query to match against interfaces names.
22
+ # @return [Array] A list of items to process.
23
+ def perform_filtering(query)
24
+ @interface_filter ||= query.empty? ? /.*/ : /#{query}/i
25
+
26
+ # Get local addresses
27
+ rv = get_local_addresses
28
+
29
+ # Sort interfaces and address, IPv4 first then IPv6
30
+ rv.sort! {|left, right|
31
+ cmp = left[:interface] <=> right[:interface] # Interface name first
32
+ cmp = compare_ip_classes(left[:address], right[:address]) if cmp == 0 # Now IPv4 first then IPv6
33
+ cmp = compare_ip_addresses(left[:address], right[:address]) if cmp == 0 # Finally addresses
34
+ cmp
35
+ }
36
+
37
+ # Add public address
38
+ rv = rv.insert(0, get_public_address) if @interface_filter.match("public")
39
+
40
+ rv
41
+ end
42
+
43
+ # Processes items to obtain feedback items.
44
+ #
45
+ # @param results [Array] The items to process.
46
+ # @return [Array] The feedback items.
47
+ def process_results(results)
48
+ results.map do |result|
49
+ title = "#{result[:interface] ? result[:interface] : "Public"} IP: #{result[:address]}"
50
+ {title: title, arg: result[:address], subtitle: "Action this item to copy the IP on the clipboard.", icon: ICON}
51
+ end
52
+ end
53
+
54
+ # Gets a list of local IP addresses.
55
+ #
56
+ # @return [Array] A list of IPs data.
57
+ def get_local_addresses
58
+ rv = []
59
+ names = get_interfaces_names
60
+
61
+ # Split by interfaces
62
+ interfaces = execute_command("/sbin/ifconfig").split(/(^\S+:\s+)/)
63
+ interfaces.shift # Discard first whitespace
64
+
65
+ # For each interface
66
+ interfaces.each_slice(2) do |interface, configuration|
67
+ # See if matches the query and then replace public name
68
+ interface = interface.gsub(/\s*(.+):\s*/, "\\1")
69
+ name = names[interface] ? "#{names[interface]} (#{interface})" : interface
70
+ next if !@interface_filter.match(name)
71
+
72
+ # Get addresses
73
+ addresses = StringScanner.new(configuration)
74
+ while addresses.scan_until(/inet(6?)\s/) do
75
+ rv << {interface: name, address: addresses.scan(/\S+/)}
76
+ end
77
+ end
78
+
79
+ rv
80
+ end
81
+
82
+ # Gets the public IP address for this machine.
83
+ #
84
+ # @return [Hash] The public IP address data.
85
+ def get_public_address
86
+ {interface: nil, address: fetch_remote_resource(URL, {}, false)}
87
+ end
88
+
89
+ # Compares two IP classes, giving higher priority to IPv4.
90
+ #
91
+ # @param left [String] The first IP to compare.
92
+ # @param right [String] The second IP to compare.
93
+ # @return [Fixnum] The result of the comparison.
94
+ def compare_ip_classes(left, right)
95
+ (left.index(":") ? 1 : 0) <=> (right.index(":") ? 1 : 0)
96
+ end
97
+
98
+ # Compares to IP addresses, giving higher priority to local address such as 127.0.0.1.
99
+ #
100
+ # @param left [String] The first IP to compare.
101
+ # @param right [String] The second IP to compare.
102
+ # @return [Fixnum] The result of the comparison.
103
+ def compare_ip_addresses(left, right)
104
+ higher_priority = ["::1", "127.0.0.1", "10.0.0.1"]
105
+ cmp = (higher_priority.include?(left) ? 0 : 1) <=> (higher_priority.include?(right) ? 0 : 1)
106
+ cmp = left <=> right if cmp == 0
107
+ cmp
108
+ end
109
+
110
+ # Gets a hash with pair of interfaces and their human names.
111
+ #
112
+ # @return [Hash] The hash with interfaces' name.
113
+ def get_interfaces_names
114
+ rv = {"lo0" => "Loopback"}
115
+
116
+ names = execute_command("/usr/sbin/networksetup", "-listallhardwareports").split(/\n\n/)
117
+ names.shift # Discard first whitespace
118
+
119
+ names.each do |port|
120
+ port = StringScanner.new(port)
121
+ name = nil
122
+ interface = nil
123
+
124
+ if port.scan_until(/Hardware Port: /) then
125
+ name = port.scan_until(/\n/).strip
126
+ interface = port.scan_until(/\n/).strip if port.scan_until(/Device: /)
127
+ end
128
+
129
+ rv[interface] = name if name && interface
130
+ end
131
+
132
+ rv
133
+ end
134
+ end
135
+ end
@@ -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
+ module Pincerna
8
+ # Shows addresses or coordinates on Google Maps.
9
+ class Map < Base
10
+ # The icon to show for each feedback item.
11
+ ICON = Pincerna::Base::ROOT + "/images/map.png"
12
+
13
+ # Filters a query.
14
+ #
15
+ # @param query [String] An address to show on Google Maps.
16
+ # @return [Array] A list of items to process.
17
+ def perform_filtering(query)
18
+ {query: query}
19
+ end
20
+
21
+ # Processes items to obtain feedback items.
22
+ #
23
+ # @param results [Array] The items to process.
24
+ # @return [Array] The feedback items.
25
+ def process_results(results)
26
+ type = results[:query] =~ /((-?)\d+(\.\d+)?)\s*,\s*((-?)\d+(\.\d+)?)/ ? "coordinates" : "location"
27
+ [{title: "View #{type} on Google Maps", arg: CGI.escape(results[:query]), subtitle: "Action this item to open Google Maps in the browser.", icon: ICON}]
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,40 @@
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 SafariBookmark < Bookmark
10
+ # The icon to show for each feedback item.
11
+ ICON = Pincerna::Base::ROOT + "/images/safari.png"
12
+
13
+ # The file with bookmarks data.
14
+ BOOKMARKS_DATA = File.expand_path("~/Library/Safari/Bookmarks.plist")
15
+
16
+ # Reads the list of Safari Bookmarks.
17
+ def read_bookmarks
18
+ data = execute_command("/usr/bin/plutil", "-convert", "xml1", "-o", "-", BOOKMARKS_DATA)
19
+
20
+ if data && !data.empty? then
21
+ Plist.parse_xml(data)["Children"].each do |children|
22
+ scan_folder(children, "")
23
+ end
24
+ end
25
+ end
26
+
27
+ private
28
+ # Scans a folder of bookmarks.
29
+ #
30
+ # @param node [Hash] The directory to visit.
31
+ # @param path [String] The path of this node.
32
+ def scan_folder(node, path)
33
+ path += " #{SEPARATOR} #{node["Title"]}"
34
+
35
+ (node["Children"] || []).each do |children|
36
+ children["WebBookmarkType"] == "WebBookmarkTypeLeaf" ? add_bookmark(children["URIDictionary"]["title"], children["URLString"], path) : scan_folder(children, path)
37
+ end
38
+ end
39
+ end
40
+ end