mhc 1.2.4 → 1.2.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +14 -0
- data/README.org +37 -0
- data/Rakefile +1 -1
- data/bin/mhc +199 -3
- data/emacs/Cask +1 -1
- data/emacs/mhc-db.el +42 -7
- data/emacs/mhc-summary.el +3 -1
- data/emacs/mhc-vars.el +1 -1
- data/emacs/mhc.el +30 -6
- data/lib/mhc/calendar.rb +20 -0
- data/lib/mhc/command/init.rb +4 -4
- data/lib/mhc/config.rb +6 -2
- data/lib/mhc/converter.rb +74 -3
- data/lib/mhc/datastore.rb +9 -5
- data/lib/mhc/etag.rb +1 -1
- data/lib/mhc/formatter/html.rb +32 -0
- data/lib/mhc/formatter.rb +3 -0
- data/lib/mhc/templates/full-calendar.html.erb +417 -0
- data/lib/mhc/version.rb +1 -1
- data/mhc.gemspec +2 -1
- data/samples/japanese-holidays.mhcc +6 -6
- metadata +22 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f1770cced4e43154f7af7e9f9827811c082ba36215fc1a5f0553306a2b7ffec1
|
4
|
+
data.tar.gz: 68f9c66861b05ee65769ac9cc2bd3d011311a3407d39fcf1d07370fc57565c11
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f24b75dc4012c86e17578dcdcb3aa25cb689e2a448d75c520e37bed7951db33b5d6303598a7e3e923390a37f85a42150e8ecdc4edc5c4064997e0c19273f1f57
|
7
|
+
data.tar.gz: 7f56ec202242dfda4bb52b4fb9cb8820b5f45b2f433468c60671d3c44fae2c7724fb2b803cd787e5d58a76eb7b80c4c5809e7af618a3bb9aca6863975314a8b7
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
Style/RedundantReturn:
|
2
|
+
Enabled: false
|
3
|
+
Style/CommentedKeyword:
|
4
|
+
Enabled: false
|
5
|
+
Style/Documentation:
|
6
|
+
Enabled: false
|
7
|
+
Style/RedundantSelf:
|
8
|
+
Enabled: false
|
9
|
+
Style/StringLiterals:
|
10
|
+
Enabled: false
|
11
|
+
Metrics/MethodLength:
|
12
|
+
Enabled: false
|
13
|
+
Metrics/ClassLength:
|
14
|
+
Enabled: false
|
data/README.org
CHANGED
@@ -127,6 +127,7 @@
|
|
127
127
|
| =M= | Open pointed article to edit |
|
128
128
|
| =D= | Delete pointed article |
|
129
129
|
| =C= | Copy article temporally as a reusable template |
|
130
|
+
| =R= | Copy article like =C= using completing-read |
|
130
131
|
| =Y= | Same as =E= but use the template stored by =C= |
|
131
132
|
|
132
133
|
3) Draft Buffer
|
@@ -149,6 +150,7 @@
|
|
149
150
|
X-SC-Time: 18:00-21:00
|
150
151
|
X-SC-Category: Private Party
|
151
152
|
X-SC-Cond:
|
153
|
+
X-SC-Recurrence-Tag: HomeParty
|
152
154
|
X-SC-Duration:
|
153
155
|
X-SC-Record-Id: C34D89F5-27FA-4243-AC6C-168D8D243D9A
|
154
156
|
X-SC-Sequence: 0
|
@@ -241,6 +243,40 @@
|
|
241
243
|
of 2015-07-15 (Wed) and inclusion of 2015-08-01 (Sat).
|
242
244
|
#+END_EXAMPLE
|
243
245
|
|
246
|
+
** Headers for Grouping
|
247
|
+
*** X-SC-Category
|
248
|
+
=X-SC-Category:= is a space separated list of category
|
249
|
+
It is useful for selective display both in CLI:
|
250
|
+
: mhc scan today --category=Private
|
251
|
+
and Emacs UI:
|
252
|
+
: M-x mhc-set-default-category
|
253
|
+
: Default Category: Private && !Party
|
254
|
+
|
255
|
+
Also useful sync with Google Calendar. See [[https://github.com/yoshinari-nomura/mhc/blob/master/samples/DOT.mhc-config.yml][mhc-config.yml]] for
|
256
|
+
details.
|
257
|
+
|
258
|
+
*** X-SC-Recurrence-Tag
|
259
|
+
=X-SC-Recurrence-Tag:= is a tag for bundling multiple event articles as one recurring group.
|
260
|
+
|
261
|
+
MHC allows flexible description of repeating events using
|
262
|
+
=X-SC-Cond:=, =X-SC-Duration:=, or =X-SC-Day:=. However, we know
|
263
|
+
this is not sufficient.
|
264
|
+
|
265
|
+
For example, in my experience, some monthly meetings do not have
|
266
|
+
distinct recurring patterns such like "Third Wednesday of each
|
267
|
+
month." Instead, the next date is fixed by coordination during the meeting.
|
268
|
+
|
269
|
+
In such cases, it is difficult to mark these events as a series of related events.
|
270
|
+
That's why we need =X-SC-Recurrence-Tag:=
|
271
|
+
|
272
|
+
Using =X-SC-Recurrence-Tag:=, MHC suggests what event should be arranged.
|
273
|
+
: mhc stuck_recurrences
|
274
|
+
will tell you to make the next appointment.
|
275
|
+
|
276
|
+
For example, if you have "X-SC-Recurrence-Tag: Dentist" in your
|
277
|
+
articles of past dentist events, mhc will point out that you
|
278
|
+
forgot to make your next dentist appointment.
|
279
|
+
|
244
280
|
* INFORMATION FOR DEVELOPERS
|
245
281
|
** INSTALL for developers:
|
246
282
|
1) Install rbenv + ruby-build
|
@@ -326,6 +362,7 @@
|
|
326
362
|
(setq load-path
|
327
363
|
(cons "~/src/mhc/emacs" load-path))
|
328
364
|
(autoload 'mhc "mhc" "Message Harmonized Calendar system." t)
|
365
|
+
(autoload 'mhc-import "mhc" "Import a schedule." t)
|
329
366
|
|
330
367
|
;; M-x mhc
|
331
368
|
#+END_SRC
|
data/Rakefile
CHANGED
@@ -20,7 +20,7 @@ task :build => :check_version
|
|
20
20
|
task :check_version do
|
21
21
|
for file in %w(mhc.el mhc-vars.el Cask)
|
22
22
|
path = File.expand_path("../emacs/#{file}", __FILE__)
|
23
|
-
raise "File not found #{path}" unless File.
|
23
|
+
raise "File not found #{path}" unless File.exist?(path)
|
24
24
|
|
25
25
|
content = File.open(path).read
|
26
26
|
unless (/^;; Version: (\d+\.\d+\.\d+)$/ =~ content ||
|
data/bin/mhc
CHANGED
@@ -17,7 +17,7 @@ end
|
|
17
17
|
|
18
18
|
gemfile = File.expand_path("../../Gemfile", __FILE__)
|
19
19
|
|
20
|
-
if File.
|
20
|
+
if File.exist?(gemfile + ".lock")
|
21
21
|
ENV["BUNDLE_GEMFILE"] = gemfile
|
22
22
|
require "bundler/setup"
|
23
23
|
end
|
@@ -72,9 +72,10 @@ class MhcCLI < Thor
|
|
72
72
|
register_option :repository, :desc => "Set MHC top directory", :banner => "DIRECTORY"
|
73
73
|
register_option :calendar, :desc => "Set source CALENDAR"
|
74
74
|
register_option :category, :desc => "Pick items only in CATEGORY"
|
75
|
-
register_option :format, :desc => "Set printing format", :enum => %w(text mail orgtable emacs icalendar calfw howm json)
|
75
|
+
register_option :format, :desc => "Set printing format", :enum => %w(text mail orgtable emacs icalendar calfw howm json html)
|
76
76
|
register_option :search, :desc => "Search items by complex expression"
|
77
77
|
register_option :dry_run, :desc => "Perform a trial run with no changes made", :type => :boolean
|
78
|
+
register_option :verbose, :desc => "Show verbose message", :type => :boolean
|
78
79
|
|
79
80
|
################################################################
|
80
81
|
# command name mappings
|
@@ -113,7 +114,9 @@ class MhcCLI < Thor
|
|
113
114
|
Mhc::Command::Cache.new(builder.datastore)
|
114
115
|
end
|
115
116
|
|
117
|
+
################################################################
|
116
118
|
# Command: todo
|
119
|
+
################################################################
|
117
120
|
desc "todo", "List Todo entries in MHC calendar"
|
118
121
|
|
119
122
|
named_option :repository
|
@@ -131,7 +134,8 @@ class MhcCLI < Thor
|
|
131
134
|
search_range = nil
|
132
135
|
end
|
133
136
|
next if task.in_category?("done") && !options[:show_all]
|
134
|
-
|
137
|
+
task_first = task.occurrences(range: search_range).first
|
138
|
+
todos << task_first if task_first
|
135
139
|
end
|
136
140
|
todos.each.sort{|a, b| a.dtstart <=> b.dtstart}.each do |t|
|
137
141
|
deadline = t.dtstart
|
@@ -168,6 +172,38 @@ class MhcCLI < Thor
|
|
168
172
|
Mhc::Command::Completions.new(help, global_options, command, config)
|
169
173
|
end
|
170
174
|
|
175
|
+
################################################################
|
176
|
+
# Command: compose
|
177
|
+
################################################################
|
178
|
+
desc "compose", "Create new draft and open it in your editor"
|
179
|
+
|
180
|
+
long_desc <<-LONGDESC
|
181
|
+
Create a new draft and open it in your editor.
|
182
|
+
To select your favorite editor,
|
183
|
+
set environment variable MHC_EDITOR or EDITOR.
|
184
|
+
LONGDESC
|
185
|
+
|
186
|
+
def compose
|
187
|
+
require 'securerandom'
|
188
|
+
ev = Mhc::Event.new
|
189
|
+
ev.record_id = SecureRandom.uuid.upcase
|
190
|
+
path = builder.datastore.create(ev, true)
|
191
|
+
puts "Create draft #{path}"
|
192
|
+
editor = ENV["MHC_EDITOR"] || ENV["EDITOR"] || "vi"
|
193
|
+
system(editor, path)
|
194
|
+
|
195
|
+
while add(path) != 0
|
196
|
+
if ask("Retry?") =~ /^[Yy]/
|
197
|
+
system(editor, path)
|
198
|
+
else
|
199
|
+
puts "Abort..."
|
200
|
+
exit(1)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
puts "Successfully registered."
|
204
|
+
exit(0)
|
205
|
+
end
|
206
|
+
|
171
207
|
################################################################
|
172
208
|
# Command: config
|
173
209
|
################################################################
|
@@ -179,6 +215,40 @@ class MhcCLI < Thor
|
|
179
215
|
puts Mhc::Converter::Emacs.new.to_emacs(config.get_value(name))
|
180
216
|
end
|
181
217
|
|
218
|
+
################################################################
|
219
|
+
# Command: add
|
220
|
+
################################################################
|
221
|
+
desc "add FILE", "Add event FILE to repository"
|
222
|
+
|
223
|
+
named_option :repository
|
224
|
+
|
225
|
+
def add(file)
|
226
|
+
full_path = File.expand_path(file)
|
227
|
+
|
228
|
+
unless File.exist?(full_path)
|
229
|
+
puts Mhc::Converter::Emacs.new.to_emacs("No such file #{file}.")
|
230
|
+
return 1
|
231
|
+
end
|
232
|
+
|
233
|
+
event_string = File.open(full_path).read
|
234
|
+
errors = Mhc::Event.validate(event_string)
|
235
|
+
|
236
|
+
if errors.empty?
|
237
|
+
ev = Mhc::Event.parse(event_string)
|
238
|
+
calendar.add(ev)
|
239
|
+
return 0
|
240
|
+
else
|
241
|
+
string = ""
|
242
|
+
errors.each do |err, key|
|
243
|
+
string += "#{err.to_s.capitalize}"
|
244
|
+
string += " in X-SC-#{key.capitalize}" if key
|
245
|
+
string += ".\n"
|
246
|
+
end
|
247
|
+
puts string
|
248
|
+
return 1
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
182
252
|
################################################################
|
183
253
|
# Command: init
|
184
254
|
################################################################
|
@@ -188,6 +258,55 @@ class MhcCLI < Thor
|
|
188
258
|
Mhc::Command::Init.new(top_dir, options[:config] || DEFAULT_CONFIG_PATH, ENV["MHC_TZID"])
|
189
259
|
end
|
190
260
|
|
261
|
+
################################################################
|
262
|
+
# Command: stuck_recurrences
|
263
|
+
################################################################
|
264
|
+
desc "stuck_recurrences", "List stuck recurrences in MHC calendar"
|
265
|
+
|
266
|
+
named_option :repository
|
267
|
+
named_option :verbose
|
268
|
+
named_option :format
|
269
|
+
|
270
|
+
# * stuck_recurrences
|
271
|
+
#
|
272
|
+
# For all "living recurrences", if no next event has been set in
|
273
|
+
# the "future", show the information on the last time.
|
274
|
+
#
|
275
|
+
# + "living recurrences" ::
|
276
|
+
# + There is a last-time evnet within a year and a half.
|
277
|
+
# + "future" :: ten years from now.
|
278
|
+
#
|
279
|
+
def stuck_recurrences(format = options[:format])
|
280
|
+
today = Mhc::PropertyValue::Date.today
|
281
|
+
|
282
|
+
# Seek -1.5y to +10y
|
283
|
+
search_range = (today - ((365 * 5).to_i)) ..
|
284
|
+
(today + ((365 * 10).to_i))
|
285
|
+
|
286
|
+
recurrences = {}
|
287
|
+
|
288
|
+
# Get all recurrences in search_range
|
289
|
+
calendar.occurrences(search_range).each do |oc|
|
290
|
+
rtag = oc.recurrence_tag.to_s
|
291
|
+
next if rtag.empty?
|
292
|
+
recurrences[rtag] ||= []
|
293
|
+
recurrences[rtag] << oc
|
294
|
+
end
|
295
|
+
|
296
|
+
formatter = Mhc::Formatter.build(formatter: format || :text, date_range: search_range)
|
297
|
+
|
298
|
+
recurrences.each do |rtag, recurrence|
|
299
|
+
if options[:verbose]
|
300
|
+
rec_put_debug(recurrence)
|
301
|
+
else
|
302
|
+
formatter << recurrence.last if rec_stuck?(recurrence)
|
303
|
+
end
|
304
|
+
end
|
305
|
+
str = formatter.to_s
|
306
|
+
return str if format == :json || options[:verbose]
|
307
|
+
puts str
|
308
|
+
end # stuck_recurrences
|
309
|
+
|
191
310
|
################################################################
|
192
311
|
# Command: scan
|
193
312
|
################################################################
|
@@ -222,6 +341,9 @@ class MhcCLI < Thor
|
|
222
341
|
named_option :calendar, :category, :format, :repository, :search
|
223
342
|
|
224
343
|
def scan(range)
|
344
|
+
if options[:format].to_s == 'html'
|
345
|
+
$stuck_recurrences_json = stuck_recurrences(:json)
|
346
|
+
end
|
225
347
|
begin
|
226
348
|
Mhc::Command::Scan.new(calendar, range, **symbolize_keys(options))
|
227
349
|
rescue Mhc::PropertyValue::ParseError, Mhc::Formatter::NameError, Mhc::Query::ParseError => e
|
@@ -326,6 +448,80 @@ class MhcCLI < Thor
|
|
326
448
|
|
327
449
|
private
|
328
450
|
|
451
|
+
def rec_put_debug(recurrence)
|
452
|
+
ol = recurrence.last
|
453
|
+
|
454
|
+
today = Mhc::PropertyValue::Date.today
|
455
|
+
last_occurrence = recurrence.last
|
456
|
+
last_occurrence_date = last_occurrence.dtstart.to_date
|
457
|
+
# Ignore living recurrences
|
458
|
+
return false if last_occurrence_date > today
|
459
|
+
|
460
|
+
status = if rec_stuck?(recurrence) then " TODO" else "" end
|
461
|
+
print "*#{status} #{ol.recurrence_tag}#{status} "
|
462
|
+
|
463
|
+
print format("%s %s",
|
464
|
+
ol.dtstart.strftime("%Y-%m-%d %a"),
|
465
|
+
ol.subject)
|
466
|
+
|
467
|
+
print format(" (length: %d, max: %d, min: %d, avr: %d)\n",
|
468
|
+
recurrence.length,
|
469
|
+
*rec_stat(recurrence))
|
470
|
+
|
471
|
+
pr = recurrence.first
|
472
|
+
recurrence.each do |oc|
|
473
|
+
puts format(" %s %s (%d)",
|
474
|
+
oc.dtstart.strftime("%Y-%m-%d %a"),
|
475
|
+
oc.subject,
|
476
|
+
(oc.dtstart.to_date - pr.dtstart.to_date).to_i
|
477
|
+
)
|
478
|
+
pr = oc
|
479
|
+
end
|
480
|
+
end
|
481
|
+
|
482
|
+
# Judge if a recurrence is stuck or not. recurrence is a set of occurrences.
|
483
|
+
def rec_stuck?(recurrence, today = Mhc::PropertyValue::Date.today)
|
484
|
+
last_occurrence = recurrence.last
|
485
|
+
last_occurrence_date = last_occurrence.dtstart.to_date
|
486
|
+
max_interval, _min_interval, avr_interval = rec_stat(recurrence)
|
487
|
+
|
488
|
+
# Not stuck if scheduled in the future dates
|
489
|
+
return false if last_occurrence_date > today
|
490
|
+
|
491
|
+
# Stuck if last event occurred within 90 days, but average
|
492
|
+
# interval is less than 60 days.
|
493
|
+
if (today - last_occurrence_date).to_i < 90 && avr_interval < 60
|
494
|
+
return true
|
495
|
+
end
|
496
|
+
|
497
|
+
# Not stuck if the number of days between the last event and today
|
498
|
+
# does not reach 80% of the maximum interval.
|
499
|
+
return false if (today - last_occurrence_date).to_i < max_interval * 4 / 5
|
500
|
+
|
501
|
+
# Not stuck if the number of days between the last event and today
|
502
|
+
# is more than 30 days or 1.5 years beyond the maximum interval.
|
503
|
+
# It would be no more recurring.
|
504
|
+
return false if (today - last_occurrence_date).to_i > [max_interval + 30, 365*1.5].min
|
505
|
+
|
506
|
+
# Otherwise, stuck
|
507
|
+
return true
|
508
|
+
end
|
509
|
+
|
510
|
+
# Return statistics on recurrences: max/min/average interval.
|
511
|
+
def rec_stat(recurrence)
|
512
|
+
# Calcurate all intervals of the recurrence
|
513
|
+
intervals = recurrence.each_cons(2).map{|ocs|
|
514
|
+
(ocs[1].dtstart.to_date - ocs[0].dtstart.to_date).to_i
|
515
|
+
}.reject{|interval| interval < 1}
|
516
|
+
|
517
|
+
# Assume annual event if the recurrence is one-shot.
|
518
|
+
intervals = [365] if intervals.empty?
|
519
|
+
|
520
|
+
return [intervals.max,
|
521
|
+
intervals.min,
|
522
|
+
intervals.reduce(0, :+) / intervals.length]
|
523
|
+
end
|
524
|
+
|
329
525
|
def exit_on_error(&block)
|
330
526
|
begin
|
331
527
|
yield if block_given?
|
data/emacs/Cask
CHANGED
data/emacs/mhc-db.el
CHANGED
@@ -34,6 +34,12 @@ If optional SEARCH is non-nil returned value is clipped by search string."
|
|
34
34
|
(if category (format " --category=%s" category) "")
|
35
35
|
(if search (format " --search='%s'" search) ""))))
|
36
36
|
|
37
|
+
(defun mhc-db-stuck-recurrences ()
|
38
|
+
"List stuck recurrences in MHC calendar."
|
39
|
+
(mhc-db-flatten
|
40
|
+
(mhc-process-send-command
|
41
|
+
(format "stuck_recurrences --format=emacs"))))
|
42
|
+
|
37
43
|
(defun mhc-db-scan-flat (begin-date end-date &optional nosort category search)
|
38
44
|
"Scan MHC database from BEGIN-DATE to END-DATE.
|
39
45
|
Unlike `mhc-db-scan`, returned value is not grouped by date.
|
@@ -42,19 +48,48 @@ For example:
|
|
42
48
|
If optional NOSORT is non-nil, returned value is not sort.
|
43
49
|
If optional CATEGORY is non-nil, returned value is clipped by category.
|
44
50
|
If optional SEARCH is non-nil returned value is clipped by search string."
|
45
|
-
(
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
51
|
+
(mhc-db-flatten (mhc-db-scan begin-date end-date nosort category search)))
|
52
|
+
|
53
|
+
(defun mhc-db-flatten (dayinfo-list)
|
54
|
+
"Flatten DAYINFO-LIST scanned from `mhc-db-scan`.
|
55
|
+
Unlike `mhc-db-scan`, returned value is not grouped by date."
|
56
|
+
(apply 'append
|
57
|
+
(mapcar (lambda (dayinfo)
|
58
|
+
(let ((date (mhc-day-date dayinfo))
|
59
|
+
(schedules (mhc-day-schedules dayinfo)))
|
60
|
+
(mapcar (lambda (sch) (cons date sch)) schedules)))
|
61
|
+
dayinfo-list)))
|
52
62
|
|
53
63
|
(defun mhc-db-search (&rest query)
|
54
64
|
(let ((b (mhc-date-new 1970 1 1))
|
55
65
|
(e (mhc-date-yy+ (mhc-date-now) 10)))
|
56
66
|
(mhc-db-scan b e nil nil (mhc-db/query-to-search-string query))))
|
57
67
|
|
68
|
+
(defun mhc-db-past-occurrences (&optional days)
|
69
|
+
(let ((now (mhc-date-now)))
|
70
|
+
(nreverse
|
71
|
+
(mhc-db-scan-flat (mhc-date- now (or days 732)) now))))
|
72
|
+
|
73
|
+
(defun mhc-db-make-completion-list (occurrences)
|
74
|
+
(delq nil
|
75
|
+
(mapcar
|
76
|
+
(lambda (occurrence)
|
77
|
+
(let ((sch (cdr occurrence)))
|
78
|
+
(if (not (string= (mhc-record-name (mhc-schedule-record sch)) ""))
|
79
|
+
(cons (mhc-db-real-to-display occurrence) occurrence))))
|
80
|
+
occurrences)))
|
81
|
+
|
82
|
+
(defun mhc-db-real-to-display (occurrence)
|
83
|
+
(let* ((date (car occurrence))
|
84
|
+
(sch (cdr occurrence))
|
85
|
+
(location (mhc-schedule-location sch)))
|
86
|
+
(concat
|
87
|
+
(mhc-date-format date "%04d/%02d/%02d" yy mm dd)
|
88
|
+
(format " %11s " (mhc-schedule-time-as-string sch))
|
89
|
+
(mhc-schedule-subject sch)
|
90
|
+
(if (and location (not (string= location "")))
|
91
|
+
(format " [%s]" location)))))
|
92
|
+
|
58
93
|
(defun mhc-db/quote-string (string)
|
59
94
|
(format "\"%s\"" string))
|
60
95
|
|
data/emacs/mhc-summary.el
CHANGED
@@ -294,9 +294,10 @@ If optional argument FOR-DRAFT is non-nil, Hilight message as draft message."
|
|
294
294
|
(set-visited-file-name nil)
|
295
295
|
;; (rename-buffer (file-name-nondirectory file) 'unique)
|
296
296
|
;; (run-hooks 'mhc-calendar-view-file-hook)
|
297
|
+
(let ((buffer-read-only nil))
|
298
|
+
(mhc-message-mode))
|
297
299
|
(set-buffer-modified-p nil)
|
298
300
|
(setq buffer-read-only t)
|
299
|
-
(mhc-message-mode)
|
300
301
|
(mhc-message-set-file-name file)
|
301
302
|
)))
|
302
303
|
|
@@ -633,6 +634,7 @@ If BANNER is set, it is printed on the horizontal line."
|
|
633
634
|
(define-key mhc-summary-mode-map "E" 'mhc-edit)
|
634
635
|
(define-key mhc-summary-mode-map "M" 'mhc-modify)
|
635
636
|
(define-key mhc-summary-mode-map "C" 'mhc-reuse-copy)
|
637
|
+
(define-key mhc-summary-mode-map "R" 'mhc-reuse-past-event)
|
636
638
|
(define-key mhc-summary-mode-map "Y" 'mhc-reuse-create)
|
637
639
|
|
638
640
|
(define-key mhc-summary-mode-map "n" 'mhc-summary-display-next)
|
data/emacs/mhc-vars.el
CHANGED
data/emacs/mhc.el
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
;; Description: Message Harmonized Calendaring system.
|
4
4
|
;; Author: Yoshinari Nomura <nom@quickhack.net>
|
5
5
|
;; Created: 1994-07-04
|
6
|
-
;; Version: 1.2.
|
6
|
+
;; Version: 1.2.6
|
7
7
|
;; Keywords: calendar
|
8
8
|
;; URL: http://www.quickhack.net/mhc
|
9
9
|
;; Package-Requires: ((calfw "20150703"))
|
@@ -19,7 +19,8 @@
|
|
19
19
|
;;
|
20
20
|
;; (setq load-path
|
21
21
|
;; (cons "~/src/mhc/emacs" load-path))
|
22
|
-
;; (autoload 'mhc "mhc")
|
22
|
+
;; (autoload 'mhc "mhc" nil t)
|
23
|
+
;; (autoload 'mhc-import "mhc" nil t)
|
23
24
|
;;
|
24
25
|
;; and M-x mhc
|
25
26
|
|
@@ -325,10 +326,11 @@
|
|
325
326
|
ret))))
|
326
327
|
|
327
328
|
(defun mhc-expr-compile (string)
|
328
|
-
(
|
329
|
-
|
330
|
-
|
331
|
-
|
329
|
+
(let ((lexical-binding nil))
|
330
|
+
(byte-compile
|
331
|
+
`(lambda (schedule)
|
332
|
+
,(mhc-expr-parse string)
|
333
|
+
))))
|
332
334
|
|
333
335
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
334
336
|
;;
|
@@ -796,6 +798,7 @@ Returns t if the importation was succeeded."
|
|
796
798
|
:group 'mhc
|
797
799
|
:type 'boolean)
|
798
800
|
|
801
|
+
;;;###autoload
|
799
802
|
(defun mhc-import (&optional get-original)
|
800
803
|
"Import a schedule from the current article.
|
801
804
|
The default action of this command is to import a schedule from the
|
@@ -912,6 +915,27 @@ the default action of this command is changed to the latter."
|
|
912
915
|
(message "%s is copied." (mhc-record-subject-as-string record)))
|
913
916
|
(message "No file here."))))
|
914
917
|
|
918
|
+
(defun mhc-reuse-past-event ()
|
919
|
+
"Select past schedule and copy to template."
|
920
|
+
(interactive)
|
921
|
+
(let* ((completion-ignore-case t)
|
922
|
+
(read-file-name-completion-ignore-case t)
|
923
|
+
(read-buffer-completion-ignore-case t)
|
924
|
+
(case-fold-search t)
|
925
|
+
(collection
|
926
|
+
(mhc-db-make-completion-list
|
927
|
+
(mhc-db-past-occurrences)))
|
928
|
+
(completion-table
|
929
|
+
(lambda (string pred action)
|
930
|
+
(if (eq action 'metadata)
|
931
|
+
'(metadata (display-sort-function . identity)
|
932
|
+
(cycle-sort-function . identity))
|
933
|
+
(complete-with-action action collection string pred))))
|
934
|
+
(key (completing-read "Select occurrence: " completion-table nil t))
|
935
|
+
(sch (cdr (cdr (assoc key collection)))))
|
936
|
+
(mhc-reuse-copy
|
937
|
+
(mhc-record-name (mhc-schedule-record sch)))))
|
938
|
+
|
915
939
|
(defun mhc-modify ()
|
916
940
|
"Modify the current schedule."
|
917
941
|
(interactive)
|
data/lib/mhc/calendar.rb
CHANGED
@@ -23,6 +23,22 @@ module Mhc
|
|
23
23
|
@datastore.entries(category: "todo")
|
24
24
|
end
|
25
25
|
|
26
|
+
def recurrences(rec, &scope_block)
|
27
|
+
@datastore.entries(recurrence: rec)
|
28
|
+
end
|
29
|
+
|
30
|
+
def recurrence_tags
|
31
|
+
hash = {}
|
32
|
+
@datastore.entries.each do |ev|
|
33
|
+
tag = ev.recurrence_tag.to_s
|
34
|
+
next if tag == ""
|
35
|
+
next if hash[tag]
|
36
|
+
|
37
|
+
yield tag
|
38
|
+
hash[tag] = ev
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
26
42
|
def occurrences(date_range, &scope_block)
|
27
43
|
ocs = []
|
28
44
|
@datastore.entries(range: date_range).each do |event|
|
@@ -34,6 +50,10 @@ module Mhc
|
|
34
50
|
return ocs.sort
|
35
51
|
end
|
36
52
|
|
53
|
+
def add(event)
|
54
|
+
@datastore.update(event)
|
55
|
+
end
|
56
|
+
|
37
57
|
################################################################
|
38
58
|
## for sync manager
|
39
59
|
|
data/lib/mhc/command/init.rb
CHANGED
@@ -62,7 +62,7 @@ module Mhc
|
|
62
62
|
require "erb"
|
63
63
|
template = ERB.new(File.open(template_path).read, nil, "-")
|
64
64
|
|
65
|
-
if File.
|
65
|
+
if File.exist?(dest_path)
|
66
66
|
say_status "exist", "Ignore #{dest_path}", :yellow
|
67
67
|
return
|
68
68
|
end
|
@@ -98,7 +98,7 @@ module Mhc
|
|
98
98
|
require "digest/md5"
|
99
99
|
|
100
100
|
# Debian
|
101
|
-
if File.
|
101
|
+
if File.exist?("/etc/timezone")
|
102
102
|
return File.open("/etc/timezone").read.chomp
|
103
103
|
end
|
104
104
|
|
@@ -109,13 +109,13 @@ module Mhc
|
|
109
109
|
end
|
110
110
|
|
111
111
|
# Red Had / CentOS
|
112
|
-
if File.
|
112
|
+
if File.exist?("/etc/sysconfig/clock") &&
|
113
113
|
/ZONE=["']?([^"']+)/ =~ File.open("/etc/sysconfig/clock").read.chomp
|
114
114
|
return $1
|
115
115
|
end
|
116
116
|
|
117
117
|
# generic including FreeBSD
|
118
|
-
if File.
|
118
|
+
if File.exist?("/etc/localtime")
|
119
119
|
localtime = Digest::MD5.file("/etc/localtime")
|
120
120
|
candidates = Dir.chdir("/usr/share/zoneinfo") do
|
121
121
|
Dir.glob("**/*").select do |fn|
|
data/lib/mhc/config.rb
CHANGED
@@ -52,7 +52,11 @@ module Mhc
|
|
52
52
|
end
|
53
53
|
|
54
54
|
def self.create_from_yaml_string(yaml_string, filename = nil)
|
55
|
-
hash = YAML.
|
55
|
+
hash = if YAML.respond_to?(:unsafe_load)
|
56
|
+
YAML.unsafe_load(yaml_string, filename: filename)
|
57
|
+
else
|
58
|
+
YAML.load(yaml_string, filename)
|
59
|
+
end || {}
|
56
60
|
return new(hash)
|
57
61
|
end
|
58
62
|
|
@@ -207,7 +211,7 @@ module Mhc
|
|
207
211
|
end # class Top
|
208
212
|
|
209
213
|
def self.create_from_file(file_name)
|
210
|
-
unless File.
|
214
|
+
unless File.exist?(File.expand_path(file_name))
|
211
215
|
raise Mhc::ConfigurationError, "config file '#{file_name}' not found"
|
212
216
|
end
|
213
217
|
begin
|