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