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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4522630272ecdc9d44c9b994b2b31f8e36278abb0004708ec0e39d63e5d16792
4
- data.tar.gz: cbcd079db569d336eec4eb87f21e5598cd3735b04f0fef6ffbbc8b7fdb583067
3
+ metadata.gz: f1770cced4e43154f7af7e9f9827811c082ba36215fc1a5f0553306a2b7ffec1
4
+ data.tar.gz: 68f9c66861b05ee65769ac9cc2bd3d011311a3407d39fcf1d07370fc57565c11
5
5
  SHA512:
6
- metadata.gz: 1f052c78047e59e8638248ef56928f2ee3e85ae544b959d294c6900afb06868cb305a4a23b1ae85a35d963b870dbb0ec950c4c1d5cd9ea25d9ffe07334be5cd8
7
- data.tar.gz: 874b007618cfd8be8a49ba0fab497ba38f6a36edfedbedf0ec72bb983c6c5ceff67a52c2b6138a62ced23fb0586e5f644eca018e2d9b963a8d79666100b94b9a
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.exists?(path)
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.exists?(gemfile + ".lock")
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
- todos << task.occurrences(range: search_range).first
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
@@ -5,7 +5,7 @@
5
5
  (source org)
6
6
  (source melpa)
7
7
 
8
- (package "mhc" "1.2.4" "Message Harmonized Calendaring system") ;; MHC_VERSION
8
+ (package "mhc" "1.2.6" "Message Harmonized Calendaring system") ;; MHC_VERSION
9
9
 
10
10
  (files "mhc.el" "mhc-*.el")
11
11
 
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
- (let ((dayinfo-list (mhc-db-scan begin-date end-date nosort category search)))
46
- (apply 'append
47
- (mapcar (lambda (dayinfo)
48
- (let ((date (mhc-day-date dayinfo))
49
- (schedules (mhc-day-schedules dayinfo)))
50
- (mapcar (lambda (sch) (cons date sch)) schedules)))
51
- dayinfo-list))))
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
@@ -18,7 +18,7 @@
18
18
 
19
19
 
20
20
  ;;; Constants:
21
- (defconst mhc-version "mhc 1.2.4") ;; MHC_VERSION
21
+ (defconst mhc-version "mhc 1.2.6") ;; MHC_VERSION
22
22
 
23
23
 
24
24
  ;;; Configration Variables:
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.4
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
- (byte-compile
329
- `(lambda (schedule)
330
- ,(mhc-expr-parse string)
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
 
@@ -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.exists?(dest_path)
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.exists?("/etc/timezone")
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.exists?("/etc/sysconfig/clock") &&
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.exists?("/etc/localtime")
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.load(yaml_string, filename) || {}
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.exists?(File.expand_path(file_name))
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