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
|