···11+Copyright (c) 2010, 2014, 2017 joshua stein <jcs@jcs.org>
22+33+Redistribution and use in source and binary forms, with or without
44+modification, are permitted provided that the following conditions
55+are met:
66+77+1. Redistributions of source code must retain the above copyright
88+ notice, this list of conditions and the following disclaimer.
99+2. Redistributions in binary form must reproduce the above copyright
1010+ notice, this list of conditions and the following disclaimer in the
1111+ documentation and/or other materials provided with the distribution.
1212+3. The name of the author may not be used to endorse or promote products
1313+ derived from this software without specific prior written permission.
1414+1515+THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR
1616+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
1717+OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
1818+IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
1919+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
2020+NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
2121+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
2222+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
2323+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
2424+THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+19
README.md
···11+## `ical_forecast`
22+33+Print a forecast of upcoming events from a list of iCal files.
44+55+### Installation
66+77+`gem install ri_cal tzinfo`
88+99+### Usage
1010+1111+ jcs@humble:~> ruby ical_forecast.rb ~/.cals/*.cal
1212+ some far out event in 3 weeks, 6 days (wed 1 sep)
1313+ something here in 1 day (fri 6 aug to sun 8 aug)
1414+ some timed event today at 08:45 (thu 5 aug)
1515+1616+### Related
1717+1818+[remind_forecast](https://github.com/jcs/remind_forecast) which
1919+does the same thing from `remind` or `iCalBuddy`.
+141
ical_forecast.rb
···11+#!/usr/bin/env ruby
22+#
33+# ical_forecast.rb
44+# print upcoming events from iCal files
55+#
66+# Copyright (c) 2010, 2014, 2017 joshua stein <jcs@jcs.org>
77+#
88+# Redistribution and use in source and binary forms, with or without
99+# modification, are permitted provided that the following conditions
1010+# are met:
1111+#
1212+# 1. Redistributions of source code must retain the above copyright
1313+# notice, this list of conditions and the following disclaimer.
1414+# 2. Redistributions in binary form must reproduce the above copyright
1515+# notice, this list of conditions and the following disclaimer in the
1616+# documentation and/or other materials provided with the distribution.
1717+# 3. The name of the author may not be used to endorse or promote products
1818+# derived from this software without specific prior written permission.
1919+#
2020+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR
2121+# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
2222+# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
2323+# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
2424+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
2525+# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
2626+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
2727+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
2828+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
2929+# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
3030+#
3131+3232+require "ri_cal"
3333+require "tzinfo"
3434+require "date"
3535+3636+FORECAST_SPAN = (60 * 60 * 24 * 7)
3737+3838+BOLD = "\e[1;1m"
3939+UNBOLD = "\e[0;0m"
4040+GRAY = "\e[38;5;239m"
4141+LIGHTGRAY = "\e[38;5;242m"
4242+RESET = "\e[0;0m"
4343+4444+upcoming = []
4545+4646+if !ARGV.any? || ARGV[0] == "-h"
4747+ puts "usage: #{$0} <ical file...>"
4848+ exit 1
4949+end
5050+5151+ARGV.each do |file|
5252+ RiCal.parse(File.open(file)).first.events.each do |ev|
5353+ ev.occurrences(:count => 10).each do |oc|
5454+ begin
5555+ start = oc.dtstart.to_time.getlocal
5656+ finish = oc.dtend.to_time.getlocal
5757+ rescue RiCal::InvalidTimezoneIdentifier
5858+ next
5959+ end
6060+6161+ if finish < Time.now || start > (Time.now + FORECAST_SPAN)
6262+ next
6363+ end
6464+6565+ upcoming.push oc
6666+ end
6767+ end
6868+end
6969+7070+upcoming.sort_by{|u| u.dtstart.to_time.getlocal }.reverse.each do |ev|
7171+ start = ev.dtstart.to_time.getlocal
7272+ finish = ev.dtend.to_time.getlocal
7373+7474+ weeks = 0
7575+ days = ((start - Time.now) / (60 * 60 * 24)).ceil
7676+ if days > 7
7777+ weeks = (days.to_f / 7.0).floor
7878+ days = days - (weeks * 7)
7979+ end
8080+8181+ out = ""
8282+ if weeks == 0 && days == 0
8383+ out << BOLD
8484+ elsif weeks == 1
8585+ out << GRAY
8686+ elsif weeks > 1
8787+ out << LIGHTGRAY
8888+ end
8989+9090+ out << ev.summary << " "
9191+9292+ if weeks > 0
9393+ out << "in #{weeks} week#{weeks == 1 ? "" : "s"}"
9494+9595+ if days > 0
9696+ out << ", #{days} day#{days == 1 ? "" : "s"}"
9797+ end
9898+ else
9999+ if days < -1
100100+ out << "#{days.abs} day#{days == -1 ? "" : "s"} ago"
101101+ elsif days == -1
102102+ out << "yesterday"
103103+ elsif days == 0
104104+ out << "today"
105105+ elsif days == 1
106106+ out << "tomorrow"
107107+ else
108108+ out << "in #{days} day#{days == 1 ? "" : "s"}"
109109+ end
110110+ end
111111+112112+ all_day = (start != finish && start.hour == 0 && finish.hour == 0 &&
113113+ (finish.to_i - start.to_i == (60 * 60 * 24)))
114114+115115+ if !all_day
116116+ out << " at #{start.strftime("%H:%M")}"
117117+118118+ if start.to_date == Time.now.to_date
119119+ mins = ((Time.now - start) / 60).floor
120120+ if start < Time.now
121121+ out << " (#{mins} minute#{mins == 1 ? "" : "s"} ago)"
122122+ else
123123+ out << " (in #{mins} minute#{mins == 1 ? "" : "s"})"
124124+ end
125125+ end
126126+ end
127127+128128+ if weeks == 0 && days == 0
129129+ out << UNBOLD
130130+ end
131131+132132+ out << " (" << start.strftime("%a #{start.day} %b").downcase
133133+ if !all_day && (start.to_date != finish.to_date)
134134+ out << " to " << finish.strftime("%a #{finish.day} %b").downcase
135135+ end
136136+ out << ")"
137137+138138+ out << RESET
139139+140140+ puts out
141141+end