After Width: | Height: | Size: 5.9 KiB |
@ -0,0 +1,28 @@
|
||||
## 0.0.5 - 2014.11.22
|
||||
### Changed
|
||||
- Use `forecast-config` for managing API keys.
|
||||
- Fixed bug when precipitation intensity/probability was all 0's.
|
||||
|
||||
## 0.0.4 - 2014.11.21
|
||||
### Added
|
||||
- Sparklines for precipitation intensity and probability for the next hour
|
||||
(where applicable) and day.
|
||||
|
||||
### Changed
|
||||
- Bugfix for when `DEFAULT_LAT_LONG` is set and `DEFAULT_LOCATION` is not.
|
||||
|
||||
## 0.0.3 - 2014.11.19
|
||||
### Added
|
||||
- Forecast now uses units appropriate to the location.
|
||||
|
||||
### Changed
|
||||
- Fix `DEFAULT_LAT_LONG`.
|
||||
|
||||
## 0.0.2 - 2014.11.19
|
||||
### Changed
|
||||
- Remove minutely result for non-US locations since Forecast doesn't have this
|
||||
data.
|
||||
|
||||
## 0.0.1 - 2014.11.18
|
||||
### Added
|
||||
- Initial release
|
@ -0,0 +1,42 @@
|
||||
# Forecast Workflow for Alfred
|
||||
|
||||
![screenshot][screenshot]
|
||||
|
||||
[screenshot]: http://i.imgur.com/mxGnovo.png
|
||||
|
||||
# Requirements
|
||||
|
||||
- [Alfred](http://www.alfredapp.com/)
|
||||
- [Alfred Powerpack](http://www.alfredapp.com/powerpack/)
|
||||
- OS X Mavericks
|
||||
|
||||
# Installation
|
||||
|
||||
Download and install the [workflow][download].
|
||||
|
||||
[download]: https://github.com/kejadlen/forecast.alfredworkflow/releases/download/0.0.5/Forecast.alfredworkflow
|
||||
|
||||
Run `forecast-config VALUE` to set API keys and the default location:
|
||||
|
||||
- `FORECAST_API_KEY`: Get an API key [here][forecast-api-key].
|
||||
- `GOOGLE_API_KEY`: Get an API key [here][google-api-key]. (Used for geocoding.
|
||||
If you never need to search for a location, this can be omitted by using
|
||||
`DEFAULT_LAT_LONG`.)
|
||||
- `DEFAULT_LOCATION`: Ex. "Seattle, WA".
|
||||
- `DEFAULT_LAT_LONG`: Only required if `GOOGLE_API_KEY` is unavailable, since
|
||||
`DEFAULT_LOCATION` can't be geocoded. Format: `lat,long`.
|
||||
|
||||
[forecast-api-key]: https://developer.forecast.io/register
|
||||
[google-api-key]: https://developers.google.com/maps/documentation/geocoding/#api_key
|
||||
|
||||
# TODO
|
||||
|
||||
- Handle errors gracefully
|
||||
- Caching? (Probably unnecessary...)
|
||||
- Use `Accept-Encoding: gzip` for Forecast calls
|
||||
|
||||
# Attributions
|
||||
|
||||
- [Climacons](http://adamwhitcroft.com/climacons/)
|
||||
- [Forecast API](https://developer.forecast.io/docs/v2)
|
||||
- [Google Geocoding API](https://developers.google.com/maps/documentation/geocoding/)
|
@ -0,0 +1,89 @@
|
||||
require 'delegate'
|
||||
require 'erb'
|
||||
require 'yaml'
|
||||
|
||||
class Items < DelegateClass(Array)
|
||||
attr_reader :items
|
||||
|
||||
def initialize
|
||||
@items = []
|
||||
super(@items)
|
||||
end
|
||||
|
||||
def to_s
|
||||
ERB.new(<<-XML).result(binding)
|
||||
<?xml version="1.0"?>
|
||||
<items>
|
||||
<%= items.map {|item| item.to_s.split("\n").map {|line| ' ' << line }}.join("\n").strip %>
|
||||
</items>
|
||||
XML
|
||||
end
|
||||
end
|
||||
|
||||
class Item
|
||||
attr_reader *%i[ uid arg valid
|
||||
title subtitle icon ]
|
||||
def initialize(**kwargs)
|
||||
@uid = kwargs.fetch(:uid).to_s.encode(xml: :attr)
|
||||
@arg = kwargs[:arg].to_s.encode(xml: :attr)
|
||||
@valid = kwargs.fetch(:valid, false) ? 'yes' : 'no'
|
||||
@title = kwargs.fetch(:title).encode(xml: :text)
|
||||
@subtitle = kwargs[:subtitle] && kwargs[:subtitle].encode(xml: :text)
|
||||
@icon = kwargs[:icon] && kwargs[:icon].encode(xml: :text)
|
||||
end
|
||||
|
||||
def to_s
|
||||
ERB.new(<<-XML, nil, '%>').result(binding)
|
||||
<item arg=<%= arg %> uid=<%= uid %> valid="<%= valid %>">
|
||||
<title><%= title %></title>
|
||||
% if subtitle
|
||||
<subtitle><%= subtitle %></subtitle>
|
||||
% end
|
||||
% if icon
|
||||
<icon><%= icon %></icon>
|
||||
% end
|
||||
</item>
|
||||
XML
|
||||
end
|
||||
end
|
||||
|
||||
module Alfred
|
||||
class Config
|
||||
def self.[](key)
|
||||
config[key]
|
||||
end
|
||||
|
||||
def self.[]=(key, value)
|
||||
config[key] = value
|
||||
end
|
||||
|
||||
def self.config
|
||||
return @config if defined?(@config)
|
||||
|
||||
bundle_id = `/usr/libexec/PlistBuddy info.plist -c 'print :bundleid'`.strip
|
||||
@config = self.new(bundle_id)
|
||||
end
|
||||
|
||||
WORKFLOW_DATA = '~/Library/Application Support/Alfred 2/Workflow Data/'
|
||||
|
||||
attr_reader :path
|
||||
attr_accessor :config
|
||||
|
||||
def initialize(bundle_id)
|
||||
dir = File.expand_path(File.join(WORKFLOW_DATA, bundle_id))
|
||||
Dir.mkdir(dir) unless Dir.exist?(dir)
|
||||
|
||||
@path = File.join(dir, 'config.yml')
|
||||
@config = File.exist?(@path) ? YAML.load_file(@path) : {}
|
||||
end
|
||||
|
||||
def [](key)
|
||||
config.fetch(key) { '' }
|
||||
end
|
||||
|
||||
def []=(key, value)
|
||||
config[key] = value
|
||||
File.write(path, YAML.dump(config))
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,26 @@
|
||||
require_relative 'alfred'
|
||||
|
||||
OPTIONS = %w[ FORECAST_API_KEY
|
||||
GOOGLE_API_KEY
|
||||
DEFAULT_LOCATION
|
||||
DEFAULT_LAT_LONG ]
|
||||
|
||||
input = ARGV.shift || ''
|
||||
|
||||
items = Items.new
|
||||
OPTIONS.each do |option|
|
||||
title = if input.empty?
|
||||
"Unset #{option}"
|
||||
else
|
||||
"Set #{option} to #{input}"
|
||||
end
|
||||
items << Item.new(
|
||||
uid: option,
|
||||
arg: "Alfred::Config['#{option}'] = '#{input}'",
|
||||
valid: true,
|
||||
title: title,
|
||||
subtitle: Alfred::Config[option],
|
||||
)
|
||||
end
|
||||
|
||||
puts items.to_s
|
@ -0,0 +1,138 @@
|
||||
require 'delegate'
|
||||
require 'erb'
|
||||
require 'yaml'
|
||||
|
||||
require_relative 'alfred'
|
||||
require_relative 'forecaster'
|
||||
require_relative 'location'
|
||||
require_relative 'spark'
|
||||
|
||||
ICONS = {
|
||||
'clear-day' => 'Sun',
|
||||
'clear-night' => 'Moon',
|
||||
'rain' => 'Cloud-Rain',
|
||||
'snow' => 'Cloud-Snow',
|
||||
'sleet' => 'Cloud-Snow-Alt',
|
||||
'wind' => 'Wind',
|
||||
'fog' => 'Cloud-Fog',
|
||||
'cloudy' => 'Cloud',
|
||||
'partly-cloudy-day' => 'Cloud-Sun',
|
||||
'partly-cloudy-night' => 'Cloud-Moon',
|
||||
}
|
||||
|
||||
Precipitation = Struct.new(:intensity, :probability) do
|
||||
def self.from_forecast(forecast)
|
||||
self.new(*forecast.values_at('precipIntensity', 'precipProbability'))
|
||||
end
|
||||
|
||||
def human_intensity
|
||||
case intensity
|
||||
when 0...0.002
|
||||
'no'
|
||||
when 0.002...0.017
|
||||
'very light'
|
||||
when 0.017...0.1
|
||||
'light'
|
||||
when 0.1...0.4
|
||||
'moderate'
|
||||
else
|
||||
'heavy'
|
||||
end
|
||||
end
|
||||
|
||||
def to_s
|
||||
"#{(probability*100).to_i}% chance of #{human_intensity} rain."
|
||||
end
|
||||
end
|
||||
|
||||
query = ARGV.shift || ''
|
||||
location = if query.empty?
|
||||
lat, long = Alfred::Config['DEFAULT_LAT_LONG'].split(?,).map(&:to_f)
|
||||
Location.new(Alfred::Config['DEFAULT_LOCATION'], lat, long)
|
||||
else
|
||||
Location.new(query)
|
||||
end
|
||||
forecast = Forecaster.forecast(location)
|
||||
|
||||
items = Items.new
|
||||
|
||||
items << Item.new(
|
||||
uid: :location,
|
||||
arg: "#{location.lat.round(4)},#{location.long.round(4)}",
|
||||
valid: true,
|
||||
title: location.name,
|
||||
icon: 'icons/forecast.ico',
|
||||
)
|
||||
|
||||
currently = forecast['currently']
|
||||
precip = Precipitation.from_forecast(currently)
|
||||
subtitle = [ "#{currently['temperature'].round}°" ]
|
||||
subtitle << "Feels like #{currently['apparentTemperature'].round}°"
|
||||
subtitle << precip.to_s if precip.probability > 0
|
||||
items << Item.new(
|
||||
uid: :currently,
|
||||
title: currently['summary'],
|
||||
subtitle: subtitle.join(' · '),
|
||||
icon: "icons/#{ICONS[currently['icon']]}.png",
|
||||
)
|
||||
|
||||
minutely = forecast['minutely']
|
||||
if minutely
|
||||
intensity = minutely['data'].map {|m| 1000 * m['precipIntensity'] }
|
||||
intensity = intensity.select.with_index {|_,i| i % 5 == 0 }
|
||||
min, max = intensity.minmax
|
||||
|
||||
subtitle = ["#{min.round}\" #{Spark.new(intensity)} #{max.round}\""]
|
||||
|
||||
probability = minutely['data'].map {|m| (100 * m['precipProbability']).round }
|
||||
probability = probability.select.with_index {|_,i| i % 5 == 0 }
|
||||
min, max = probability.minmax
|
||||
|
||||
subtitle << "#{min}% #{Spark.new(probability, max: 100)} #{max}%"
|
||||
|
||||
items << Item.new(
|
||||
uid: :minutely,
|
||||
title: minutely['summary'],
|
||||
subtitle: subtitle.join(' · '),
|
||||
icon: "icons/#{ICONS[minutely['icon']]}.png",
|
||||
)
|
||||
end
|
||||
|
||||
hourly = forecast['hourly']
|
||||
|
||||
intensity = hourly['data'].map {|m| 1000 * m['precipIntensity'] }
|
||||
intensity = intensity.select.with_index {|_,i| i % 4 == 0 }
|
||||
min, max = intensity.minmax
|
||||
|
||||
subtitle = ["#{min.round}\" #{Spark.new(intensity)} #{max.round}\""]
|
||||
|
||||
probability = hourly['data'].map {|m| (100 * m['precipProbability']).round }
|
||||
probability = probability.select.with_index {|_,i| i % 4 == 0 }
|
||||
min, max = probability.minmax
|
||||
|
||||
subtitle << "#{min}% #{Spark.new(probability, max: 100)} #{max}%"
|
||||
|
||||
items << Item.new(
|
||||
uid: :hourly,
|
||||
title: hourly['summary'],
|
||||
subtitle: subtitle.join(' · '),
|
||||
icon: "icons/#{ICONS[hourly['icon']]}.png",
|
||||
)
|
||||
|
||||
forecast['daily']['data'][1..6].each do |data|
|
||||
wday = Time.at(data['time']).strftime('%A')
|
||||
precip = Precipitation.from_forecast(data)
|
||||
|
||||
subtitle = [ "Low: #{data['apparentTemperatureMin'].round}°",
|
||||
"High: #{data['apparentTemperatureMax'].round}°" ]
|
||||
subtitle << precip.to_s if precip.probability > 0
|
||||
|
||||
items << Item.new(
|
||||
uid: wday,
|
||||
title: "#{wday} - #{data['summary']}",
|
||||
subtitle: subtitle.join(' · '),
|
||||
icon: "icons/#{ICONS[data['icon']]}.png",
|
||||
)
|
||||
end
|
||||
|
||||
puts items.to_s
|
@ -0,0 +1,23 @@
|
||||
require 'json'
|
||||
require 'open-uri'
|
||||
|
||||
require_relative 'alfred'
|
||||
|
||||
Forecaster = Struct.new(:api_key) do
|
||||
def self.forecast(location)
|
||||
forecaster.forecast(location)
|
||||
end
|
||||
|
||||
def self.forecaster
|
||||
return @forecaster if defined?(@forecaster)
|
||||
|
||||
@forecaster = self.new(Alfred::Config['FORECAST_API_KEY'])
|
||||
end
|
||||
|
||||
def forecast(location)
|
||||
lat, long = location.lat, location.long
|
||||
url = "https://api.forecast.io/forecast/#{api_key}/#{lat},#{long}?units=auto"
|
||||
response = JSON.load(open(url))
|
||||
end
|
||||
end
|
||||
|
@ -0,0 +1,30 @@
|
||||
require 'json'
|
||||
require 'open-uri'
|
||||
require 'uri'
|
||||
|
||||
require_relative 'alfred'
|
||||
|
||||
Geocoder = Struct.new(:api_key) do
|
||||
def self.geocode(location)
|
||||
geocoder.geocode(location)
|
||||
end
|
||||
|
||||
def self.geocoder
|
||||
return @geocoder if defined?(@geocoder)
|
||||
|
||||
@geocoder = self.new(Alfred::Config['GOOGLE_API_KEY'])
|
||||
end
|
||||
|
||||
def geocode(location)
|
||||
url = 'https://maps.googleapis.com/maps/api/geocode/json'
|
||||
query = URI.encode_www_form(address: location, api_key: api_key)
|
||||
response = JSON.load(open("#{url}?#{query}"))
|
||||
result = response['results'][0]
|
||||
|
||||
name = result['formatted_address']
|
||||
location = result['geometry']['location']
|
||||
lat, long = location.values_at('lat', 'lng')
|
||||
|
||||
[name, lat, long]
|
||||
end
|
||||
end
|
After Width: | Height: | Size: 5.9 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 976 B |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 609 B |
After Width: | Height: | Size: 818 B |
After Width: | Height: | Size: 879 B |
After Width: | Height: | Size: 968 B |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 695 B |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1008 B |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 979 B |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 951 B |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 963 B |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 879 B |
After Width: | Height: | Size: 835 B |
After Width: | Height: | Size: 783 B |
After Width: | Height: | Size: 846 B |
After Width: | Height: | Size: 849 B |
After Width: | Height: | Size: 785 B |
After Width: | Height: | Size: 655 B |
After Width: | Height: | Size: 581 B |
After Width: | Height: | Size: 591 B |
After Width: | Height: | Size: 635 B |
After Width: | Height: | Size: 598 B |
After Width: | Height: | Size: 518 B |
After Width: | Height: | Size: 640 B |
After Width: | Height: | Size: 635 B |
After Width: | Height: | Size: 630 B |
After Width: | Height: | Size: 631 B |
After Width: | Height: | Size: 626 B |
After Width: | Height: | Size: 446 B |
After Width: | Height: | Size: 589 B |
After Width: | Height: | Size: 738 B |
After Width: | Height: | Size: 637 B |
After Width: | Height: | Size: 878 B |
After Width: | Height: | Size: 796 B |
After Width: | Height: | Size: 800 B |
After Width: | Height: | Size: 597 B |
After Width: | Height: | Size: 608 B |
After Width: | Height: | Size: 616 B |
After Width: | Height: | Size: 613 B |
After Width: | Height: | Size: 576 B |
After Width: | Height: | Size: 552 B |
After Width: | Height: | Size: 522 B |
After Width: | Height: | Size: 715 B |
After Width: | Height: | Size: 560 B |
After Width: | Height: | Size: 8.2 KiB |
@ -0,0 +1,159 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>bundleid</key>
|
||||
<string>com.kejadlen.forecast</string>
|
||||
<key>category</key>
|
||||
<string>Internet</string>
|
||||
<key>connections</key>
|
||||
<dict>
|
||||
<key>2A5C0A87-204E-49EA-94A7-8E62BB4EFD8A</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>destinationuid</key>
|
||||
<string>5E2D96BA-31B2-4800-9A2B-B999285680A0</string>
|
||||
<key>modifiers</key>
|
||||
<integer>0</integer>
|
||||
<key>modifiersubtext</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>450E8285-E286-4D33-AADF-1ACF99F41031</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>destinationuid</key>
|
||||
<string>373F7A50-59E2-4225-B665-E63FBBBAF7E5</string>
|
||||
<key>modifiers</key>
|
||||
<integer>0</integer>
|
||||
<key>modifiersubtext</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<key>createdby</key>
|
||||
<string>Alpha Chen</string>
|
||||
<key>description</key>
|
||||
<string></string>
|
||||
<key>disabled</key>
|
||||
<false/>
|
||||
<key>name</key>
|
||||
<string>Forecast</string>
|
||||
<key>objects</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>config</key>
|
||||
<dict>
|
||||
<key>plusspaces</key>
|
||||
<false/>
|
||||
<key>url</key>
|
||||
<string>http://forecast.io/#/f/{query}</string>
|
||||
<key>utf8</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>type</key>
|
||||
<string>alfred.workflow.action.openurl</string>
|
||||
<key>uid</key>
|
||||
<string>5E2D96BA-31B2-4800-9A2B-B999285680A0</string>
|
||||
<key>version</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>config</key>
|
||||
<dict>
|
||||
<key>argumenttype</key>
|
||||
<integer>1</integer>
|
||||
<key>escaping</key>
|
||||
<integer>127</integer>
|
||||
<key>keyword</key>
|
||||
<string>forecast</string>
|
||||
<key>runningsubtext</key>
|
||||
<string>Retriving location/weather...</string>
|
||||
<key>script</key>
|
||||
<string>ruby forecast.rb {query}</string>
|
||||
<key>title</key>
|
||||
<string>Forecast</string>
|
||||
<key>type</key>
|
||||
<integer>0</integer>
|
||||
<key>withspace</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>type</key>
|
||||
<string>alfred.workflow.input.scriptfilter</string>
|
||||
<key>uid</key>
|
||||
<string>2A5C0A87-204E-49EA-94A7-8E62BB4EFD8A</string>
|
||||
<key>version</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>config</key>
|
||||
<dict>
|
||||
<key>escaping</key>
|
||||
<integer>0</integer>
|
||||
<key>script</key>
|
||||
<string>ruby -r./alfred -e "{query}"</string>
|
||||
<key>type</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
<key>type</key>
|
||||
<string>alfred.workflow.action.script</string>
|
||||
<key>uid</key>
|
||||
<string>373F7A50-59E2-4225-B665-E63FBBBAF7E5</string>
|
||||
<key>version</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>config</key>
|
||||
<dict>
|
||||
<key>argumenttype</key>
|
||||
<integer>1</integer>
|
||||
<key>escaping</key>
|
||||
<integer>127</integer>
|
||||
<key>keyword</key>
|
||||
<string>forecast-config</string>
|
||||
<key>script</key>
|
||||
<string>ruby forecast-config.rb {query}</string>
|
||||
<key>title</key>
|
||||
<string>Configure the Forecast workflow</string>
|
||||
<key>type</key>
|
||||
<integer>0</integer>
|
||||
<key>withspace</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>type</key>
|
||||
<string>alfred.workflow.input.scriptfilter</string>
|
||||
<key>uid</key>
|
||||
<string>450E8285-E286-4D33-AADF-1ACF99F41031</string>
|
||||
<key>version</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
</array>
|
||||
<key>readme</key>
|
||||
<string></string>
|
||||
<key>uidata</key>
|
||||
<dict>
|
||||
<key>2A5C0A87-204E-49EA-94A7-8E62BB4EFD8A</key>
|
||||
<dict>
|
||||
<key>ypos</key>
|
||||
<real>10</real>
|
||||
</dict>
|
||||
<key>373F7A50-59E2-4225-B665-E63FBBBAF7E5</key>
|
||||
<dict>
|
||||
<key>ypos</key>
|
||||
<real>130</real>
|
||||
</dict>
|
||||
<key>450E8285-E286-4D33-AADF-1ACF99F41031</key>
|
||||
<dict>
|
||||
<key>ypos</key>
|
||||
<real>130</real>
|
||||
</dict>
|
||||
<key>5E2D96BA-31B2-4800-9A2B-B999285680A0</key>
|
||||
<dict>
|
||||
<key>ypos</key>
|
||||
<real>10</real>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>webaddress</key>
|
||||
<string>http://github.com/kejadlen/forecast.alfredworkflow</string>
|
||||
</dict>
|
||||
</plist>
|
@ -0,0 +1,16 @@
|
||||
require_relative 'geocoder'
|
||||
|
||||
class Location
|
||||
attr_accessor :name, :lat, :long, :geocoder
|
||||
|
||||
def initialize(name, lat=nil, long=nil, geocoder=Geocoder)
|
||||
@name, @lat, @long, @geocoder = name, lat, long, geocoder
|
||||
|
||||
geocode! unless lat && long
|
||||
end
|
||||
|
||||
def geocode!
|
||||
self.name, self.lat, self.long = geocoder.geocode(name)
|
||||
end
|
||||
end
|
||||
|
@ -0,0 +1,17 @@
|
||||
class Spark
|
||||
# TICKS = %w[▁ ▂ ▃ ▄ ▅ ▆ ▇ █] # Alfred doesn't render the last bar correctly
|
||||
# for some reason...
|
||||
TICKS = %w[▁ ▂ ▃ ▄ ▅ ▆ ▇]
|
||||
|
||||
attr_reader :data, :min, :max
|
||||
|
||||
def initialize(data, **kwargs)
|
||||
@data = data.map(&:round)
|
||||
@min = kwargs.fetch(:min) { 0 }
|
||||
@max = [(kwargs.fetch(:max) { data.max }).to_f, 1.0].max
|
||||
end
|
||||
|
||||
def to_s
|
||||
data.map {|i| TICKS[(TICKS.size - 1) * (i - min) / max] }.join
|
||||
end
|
||||
end
|
@ -0,0 +1,22 @@
|
||||
require 'minitest/autorun'
|
||||
|
||||
require_relative 'alfred'
|
||||
|
||||
class TestConfig < Minitest::Test
|
||||
def setup
|
||||
@config = Alfred::Config.new('com.kejadlen.test')
|
||||
end
|
||||
|
||||
def teardown
|
||||
File.delete(@config.path) if File.exist?(@config.path)
|
||||
end
|
||||
|
||||
def test_config
|
||||
assert_nil @config[:foo]
|
||||
|
||||
@config[:foo] = 123
|
||||
assert_equal 123, @config[:foo]
|
||||
assert File.exist?(@config.path)
|
||||
assert_equal '{:foo=>123}', File.read(@config.path)
|
||||
end
|
||||
end
|
@ -0,0 +1,10 @@
|
||||
require 'minitest/autorun'
|
||||
|
||||
require_relative 'spark'
|
||||
|
||||
class TestSpark < Minitest::Test
|
||||
def test_div_by_zero
|
||||
spark = Spark.new([0, 0, 0])
|
||||
assert_equal '▁▁▁', spark.to_s
|
||||
end
|
||||
end
|