mhc 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +27 -0
- data/.rspec +2 -0
- data/.travis.yml +3 -0
- data/COPYRIGHT +28 -0
- data/Gemfile +8 -0
- data/README.org +209 -0
- data/Rakefile +13 -0
- data/bin/mhc +312 -0
- data/emacs/Cask +25 -0
- data/emacs/Makefile +58 -0
- data/emacs/mhc-calendar.el +1723 -0
- data/emacs/mhc-calfw.el +135 -0
- data/emacs/mhc-compat.el +90 -0
- data/emacs/mhc-date.el +642 -0
- data/emacs/mhc-day.el +149 -0
- data/emacs/mhc-db.el +158 -0
- data/emacs/mhc-draft.el +211 -0
- data/emacs/mhc-e21.el +167 -0
- data/emacs/mhc-face.el +236 -0
- data/emacs/mhc-file.el +224 -0
- data/emacs/mhc-guess.el +648 -0
- data/emacs/mhc-header.el +176 -0
- data/emacs/mhc-logic.el +563 -0
- data/emacs/mhc-message.el +130 -0
- data/emacs/mhc-minibuf.el +466 -0
- data/emacs/mhc-misc.el +248 -0
- data/emacs/mhc-mua.el +260 -0
- data/emacs/mhc-parse.el +286 -0
- data/emacs/mhc-process.el +35 -0
- data/emacs/mhc-ps.el +1174 -0
- data/emacs/mhc-record.el +201 -0
- data/emacs/mhc-schedule.el +202 -0
- data/emacs/mhc-summary.el +763 -0
- data/emacs/mhc-sync.el +158 -0
- data/emacs/mhc-vars.el +149 -0
- data/emacs/mhc.el +1114 -0
- data/icons/Anniversary.xbm +6 -0
- data/icons/Anniversary.xpm +27 -0
- data/icons/Birthday.xbm +6 -0
- data/icons/Birthday.xpm +25 -0
- data/icons/Business.xbm +6 -0
- data/icons/Business.xpm +24 -0
- data/icons/CheckBox.xbm +6 -0
- data/icons/CheckBox.xpm +24 -0
- data/icons/CheckedBox.xbm +6 -0
- data/icons/CheckedBox.xpm +25 -0
- data/icons/Conflict.xbm +6 -0
- data/icons/Conflict.xpm +22 -0
- data/icons/Date.xbm +6 -0
- data/icons/Date.xpm +29 -0
- data/icons/Holiday.xbm +6 -0
- data/icons/Holiday.xpm +25 -0
- data/icons/Link.xbm +6 -0
- data/icons/Link.xpm +25 -0
- data/icons/Other.xbm +6 -0
- data/icons/Other.xpm +28 -0
- data/icons/Party.xbm +6 -0
- data/icons/Party.xpm +23 -0
- data/icons/Private.xbm +6 -0
- data/icons/Private.xpm +26 -0
- data/icons/Recurrence.xbm +6 -0
- data/icons/Recurrence.xpm +98 -0
- data/icons/Vacation.xbm +6 -0
- data/icons/Vacation.xpm +26 -0
- data/lib/mhc.rb +45 -0
- data/lib/mhc/builder.rb +64 -0
- data/lib/mhc/caldav.rb +304 -0
- data/lib/mhc/calendar.rb +106 -0
- data/lib/mhc/command.rb +13 -0
- data/lib/mhc/command/cache.rb +14 -0
- data/lib/mhc/command/completions.rb +108 -0
- data/lib/mhc/command/init.rb +133 -0
- data/lib/mhc/command/scan.rb +33 -0
- data/lib/mhc/command/sync.rb +22 -0
- data/lib/mhc/config.rb +229 -0
- data/lib/mhc/converter.rb +330 -0
- data/lib/mhc/datastore.rb +164 -0
- data/lib/mhc/date_enumerator.rb +274 -0
- data/lib/mhc/date_frame.rb +124 -0
- data/lib/mhc/date_helper.rb +49 -0
- data/lib/mhc/etag.rb +68 -0
- data/lib/mhc/event.rb +396 -0
- data/lib/mhc/formatter.rb +312 -0
- data/lib/mhc/logger.rb +94 -0
- data/lib/mhc/modifier.rb +149 -0
- data/lib/mhc/occurrence.rb +94 -0
- data/lib/mhc/occurrence_enumerator.rb +113 -0
- data/lib/mhc/property_value.rb +33 -0
- data/lib/mhc/property_value/date.rb +190 -0
- data/lib/mhc/property_value/integer.rb +15 -0
- data/lib/mhc/property_value/list.rb +41 -0
- data/lib/mhc/property_value/period.rb +49 -0
- data/lib/mhc/property_value/range.rb +100 -0
- data/lib/mhc/property_value/recurrence_condition.rb +272 -0
- data/lib/mhc/property_value/text.rb +11 -0
- data/lib/mhc/property_value/time.rb +45 -0
- data/lib/mhc/query.rb +210 -0
- data/lib/mhc/sync.rb +46 -0
- data/lib/mhc/sync/driver.rb +108 -0
- data/lib/mhc/sync/status.rb +70 -0
- data/lib/mhc/sync/status_manager.rb +142 -0
- data/lib/mhc/sync/strategy.rb +233 -0
- data/lib/mhc/sync/syncinfo.rb +98 -0
- data/lib/mhc/templates/config.yml.erb +142 -0
- data/lib/mhc/version.rb +4 -0
- data/lib/mhc/webdav.rb +319 -0
- data/mhc.gemspec +24 -0
- data/samples/DOT.mhc-config.yml +116 -0
- data/samples/japanese-holidays.mhcc +153 -0
- data/samples/mhc-completions.zsh +11 -0
- data/spec/mhc_spec.rb +682 -0
- data/spec/spec_helper.rb +9 -0
- data/xpm/close.xpm +18 -0
- data/xpm/delete.xpm +19 -0
- data/xpm/exit.xpm +18 -0
- data/xpm/month.xpm +18 -0
- data/xpm/next.xpm +18 -0
- data/xpm/next2.xpm +18 -0
- data/xpm/next_year.xpm +18 -0
- data/xpm/open.xpm +19 -0
- data/xpm/prev.xpm +18 -0
- data/xpm/prev2.xpm +18 -0
- data/xpm/prev_year.xpm +18 -0
- data/xpm/save.xpm +19 -0
- data/xpm/today.xpm +18 -0
- metadata +214 -0
data/lib/mhc/calendar.rb
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
module Mhc
|
3
|
+
class Calendar
|
4
|
+
|
5
|
+
def initialize(datastore, modifiers = [], &default_scope)
|
6
|
+
@datastore = datastore
|
7
|
+
@modifiers = modifiers || []
|
8
|
+
@logger = @datastore.logger
|
9
|
+
@default_scope = default_scope
|
10
|
+
end
|
11
|
+
|
12
|
+
def find(uid:)
|
13
|
+
if event = @datastore.find_by_uid(uid)
|
14
|
+
decorate_event(event)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def events(date_range = nil, &scope_block)
|
19
|
+
occurrences(date_range, &scope_block).map(&:event).uniq
|
20
|
+
end
|
21
|
+
|
22
|
+
def occurrences(date_range, &scope_block)
|
23
|
+
ocs = []
|
24
|
+
@datastore.entries(date_range).each do |event|
|
25
|
+
event = decorate_event(event)
|
26
|
+
event.occurrences(range:date_range).each do |oc|
|
27
|
+
ocs << oc if in_scope?(oc, &scope_block)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
return ocs.sort
|
31
|
+
end
|
32
|
+
|
33
|
+
################################################################
|
34
|
+
## for sync manager
|
35
|
+
|
36
|
+
def report_etags(uid = nil)
|
37
|
+
return find(uid) if uid
|
38
|
+
date_range = (Mhc::PropertyValue::Date.today - 90)..
|
39
|
+
(Mhc::PropertyValue::Date.today + 90)
|
40
|
+
events(date_range)
|
41
|
+
end
|
42
|
+
|
43
|
+
def get_with_etag(uid)
|
44
|
+
find(uid: uid)
|
45
|
+
end
|
46
|
+
|
47
|
+
def put_if_match(uid, ics_string, expected_etag)
|
48
|
+
STDERR.print "Mhc::Calendar#put_if_match(uid:#{uid}, expected_etag:#{expected_etag})..."
|
49
|
+
if ev = find(uid: uid) and ev.etag != expected_etag
|
50
|
+
STDERR.print "failed: etag not match #{ev.etag} != #{expected_etag}\n"
|
51
|
+
return nil
|
52
|
+
end
|
53
|
+
if expected_etag and (not ev)
|
54
|
+
STDERR.print "failed: etag not match #{expected_etag} != nil\n"
|
55
|
+
end
|
56
|
+
begin
|
57
|
+
ev = Mhc::Event.new_from_ics(ics_string)
|
58
|
+
@datastore.update(ev)
|
59
|
+
STDERR.print "succeeded #{ev.etag}\n"
|
60
|
+
return true
|
61
|
+
rescue Exception => e
|
62
|
+
STDERR.print "failed: #{e.to_s}\n"
|
63
|
+
STDERR.print "#{e.backtrace.first}\n" if $MHC_DEBUG
|
64
|
+
STDERR.print "#{ics_string}\n" if $MHC_DEBUG
|
65
|
+
return nil
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def delete_if_match(uid, expected_etag)
|
70
|
+
STDERR.print "Mhc::Calendar#delete_if_match(uid:#{uid}, expected_etag:#{expected_etag})..."
|
71
|
+
unless ev = find(uid: uid)
|
72
|
+
STDERR.print "failed: uid #{uid} not found\n"
|
73
|
+
return nil
|
74
|
+
end
|
75
|
+
if expected_etag && ev.etag != expected_etag
|
76
|
+
STDERR.print "failed: etag not match #{ev.etag} != #{expected_etag}\n"
|
77
|
+
return nil
|
78
|
+
end
|
79
|
+
begin
|
80
|
+
@datastore.delete(ev)
|
81
|
+
STDERR.print "succeeded: #{ev.etag}\n"
|
82
|
+
return ev
|
83
|
+
rescue Exception => e
|
84
|
+
STDERR.print "failed: #{e.to_s}\n"
|
85
|
+
return nil
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
################################################################
|
90
|
+
private
|
91
|
+
################################################################
|
92
|
+
|
93
|
+
def decorate_event(event)
|
94
|
+
@modifiers.each do |deco|
|
95
|
+
event = deco.decorate(event)
|
96
|
+
end
|
97
|
+
return event
|
98
|
+
end
|
99
|
+
|
100
|
+
def in_scope?(oc, &scope_block)
|
101
|
+
(!@default_scope || @default_scope.call(oc)) &&
|
102
|
+
(!scope_block || scope_block.call(oc))
|
103
|
+
end
|
104
|
+
|
105
|
+
end # class Calendar
|
106
|
+
end # module Mhc
|
data/lib/mhc/command.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
module Mhc
|
2
|
+
module Command
|
3
|
+
|
4
|
+
dir = File.dirname(__FILE__) + "/command"
|
5
|
+
|
6
|
+
autoload :Completions, "#{dir}/completions.rb"
|
7
|
+
autoload :Cache, "#{dir}/cache.rb"
|
8
|
+
autoload :Init, "#{dir}/init.rb"
|
9
|
+
autoload :Scan, "#{dir}/scan.rb"
|
10
|
+
autoload :Sync, "#{dir}/sync.rb"
|
11
|
+
|
12
|
+
end # module Command
|
13
|
+
end # module Mhc
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Mhc
|
2
|
+
module Command
|
3
|
+
class Cache
|
4
|
+
|
5
|
+
def initialize(calendar)
|
6
|
+
calendar.events.each do |event|
|
7
|
+
range = event.range
|
8
|
+
puts "#{range.min.to_mhc_string},#{range.max.to_mhc_string},#{event.uid},#{event.subject}"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
end # class Cache
|
13
|
+
end # module Command
|
14
|
+
end # module Mhc
|
@@ -0,0 +1,108 @@
|
|
1
|
+
module Mhc
|
2
|
+
module Command
|
3
|
+
class Completions
|
4
|
+
|
5
|
+
def initialize(help, global_options, arguments, config = nil)
|
6
|
+
@help, @global_options, @arguments, @config = help, global_options, arguments, config
|
7
|
+
command_name = arguments.first
|
8
|
+
|
9
|
+
if command_name and help[command_name]
|
10
|
+
option_arguments(help, global_options, command_name)
|
11
|
+
else
|
12
|
+
command_arguments
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def command_arguments
|
19
|
+
print "_arguments\n"
|
20
|
+
print "1:Possible commands\\::"
|
21
|
+
print possible_commands + "\n"
|
22
|
+
end
|
23
|
+
|
24
|
+
def option_arguments(help, global_options, command_name)
|
25
|
+
command_arguments
|
26
|
+
print arguments(help, command_name)
|
27
|
+
print options(help[command_name], global_options)
|
28
|
+
end
|
29
|
+
|
30
|
+
# make normal argument completion setting from usage string such as: "scan REPOSITORY"
|
31
|
+
def arguments(help, command_name, position = 2)
|
32
|
+
str = ""
|
33
|
+
help[command_name].usage.split(/\s+/)[1..-1].each do |arg|
|
34
|
+
pos = position
|
35
|
+
|
36
|
+
if /^\[(.*)\]/ =~ arg
|
37
|
+
arg = $1
|
38
|
+
end
|
39
|
+
|
40
|
+
multi = ""
|
41
|
+
if /(.*)\.\.\.$/ =~ arg
|
42
|
+
arg = $1
|
43
|
+
pos = "*"
|
44
|
+
multi = ":"
|
45
|
+
end
|
46
|
+
|
47
|
+
str << "#{pos}:#{arg}\\::#{possible_values(arg)}#{multi}\n"
|
48
|
+
position += 1
|
49
|
+
end
|
50
|
+
return str
|
51
|
+
end
|
52
|
+
|
53
|
+
# make option argument completion setting from usage options help
|
54
|
+
def options(command_help, global_options, position = 2)
|
55
|
+
str = ""
|
56
|
+
options = command_help.options.merge(global_options)
|
57
|
+
|
58
|
+
options.each do |name, opt|
|
59
|
+
name = name.to_s.gsub("_", "-")
|
60
|
+
|
61
|
+
if opt.type == :boolean
|
62
|
+
str << "(--#{name})--#{name}[#{opt.description}]\n"
|
63
|
+
else
|
64
|
+
str << "(--#{name})--#{name}=-[#{opt.description}]:#{opt.banner}:#{possible_values_for_opt(opt)}\n"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
return str
|
68
|
+
end
|
69
|
+
|
70
|
+
def possible_commands
|
71
|
+
str = "(("
|
72
|
+
@help.each_value do |cmd|
|
73
|
+
next if cmd.name == "completions"
|
74
|
+
str << " #{cmd.name}\\:"
|
75
|
+
str << cmd.description.gsub(/([()\s"';&|#\\])/, '\\\\\1')
|
76
|
+
end
|
77
|
+
str << "))"
|
78
|
+
end
|
79
|
+
|
80
|
+
def possible_values_for_opt(option)
|
81
|
+
return "(" + option.enum.join(" ") + ")" if option.enum
|
82
|
+
return possible_values(option.banner)
|
83
|
+
end
|
84
|
+
|
85
|
+
def possible_values(banner)
|
86
|
+
case banner
|
87
|
+
when /^CALENDAR/
|
88
|
+
"(" + @config.calendars.select{|cal| cal.type == "mhc"}.map(&:name).join(' ') + ")"
|
89
|
+
when /^SYNC_CHANNEL/
|
90
|
+
"(" + @config.sync_channels.map(&:name).join(' ') + ")"
|
91
|
+
when /^(FILE|CONF)/
|
92
|
+
"_files"
|
93
|
+
when /^DIR/
|
94
|
+
"_files -/"
|
95
|
+
when "COMMAND"
|
96
|
+
possible_commands
|
97
|
+
when "RANGE"
|
98
|
+
"(today tomorrow thismonth nextmonth)"
|
99
|
+
when /^NUM/
|
100
|
+
"_guard '[0-9]#' 'Number'"
|
101
|
+
else
|
102
|
+
""
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
end # class Completions
|
107
|
+
end # module Command
|
108
|
+
end # module Mhc
|
@@ -0,0 +1,133 @@
|
|
1
|
+
module Mhc
|
2
|
+
module Command
|
3
|
+
class Init
|
4
|
+
SUB_DIRS = %w(draft inbox presets spool trash
|
5
|
+
status/cache status/log status/sync_channels)
|
6
|
+
|
7
|
+
TEMPLATE_DIR = File.expand_path("../../templates", __FILE__)
|
8
|
+
|
9
|
+
def initialize(top_dir, config_path, tzid = nil, template_dir = nil)
|
10
|
+
@shell = Thor.new
|
11
|
+
@status = {green: 0, yellow: 0, red: 0}
|
12
|
+
@config = {}
|
13
|
+
|
14
|
+
# guess teimzone
|
15
|
+
say "Guessing current local timezone ..."
|
16
|
+
|
17
|
+
if @config[:tzid] = find_current_tzid
|
18
|
+
say_status "ok", "guess timezone ... #{@config[:tzid]}", :green
|
19
|
+
tzid = find_current_tzid
|
20
|
+
else
|
21
|
+
say_status "failed", "guess timezone... Unknown", :red
|
22
|
+
end
|
23
|
+
|
24
|
+
# mkdir
|
25
|
+
say "Making directries under #{top_dir} ..."
|
26
|
+
SUB_DIRS.each do |ent|
|
27
|
+
mkdir_p(File.expand_path(ent, top_dir))
|
28
|
+
end
|
29
|
+
|
30
|
+
# make config file from tamplate
|
31
|
+
say "Copying config file(s) into #{config_path} ..."
|
32
|
+
src = File.expand_path("config.yml.erb", TEMPLATE_DIR)
|
33
|
+
dst = File.expand_path(config_path)
|
34
|
+
@config[:topdir] = top_dir
|
35
|
+
expand_template(src, dst)
|
36
|
+
|
37
|
+
say_status_report
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def say(message, color = nil)
|
43
|
+
@shell.say(message, color)
|
44
|
+
end
|
45
|
+
|
46
|
+
def say_status(status, message, log_status = nil)
|
47
|
+
@status[log_status] += 1
|
48
|
+
@shell.say_status(status, message, log_status)
|
49
|
+
end
|
50
|
+
|
51
|
+
def say_status_report
|
52
|
+
if (errors = @status[:red]) > 0
|
53
|
+
say "#{errors} error(s) were occurred.", :red
|
54
|
+
else
|
55
|
+
say "Done."
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def expand_template(template_path, dest_path)
|
60
|
+
require "erb"
|
61
|
+
template = ERB.new(File.open(template_path).read, nil, "-")
|
62
|
+
|
63
|
+
if File.exists?(dest_path)
|
64
|
+
say_status "exist", "Ignore #{dest_path}", :yellow
|
65
|
+
return
|
66
|
+
end
|
67
|
+
|
68
|
+
begin
|
69
|
+
mkdir_p(File.expand_path("..", dest_path))
|
70
|
+
File.open(dest_path, "w", 0600) do |file|
|
71
|
+
file.write(template.result(binding))
|
72
|
+
end
|
73
|
+
say_status "ok", "copy #{dest_path}", :green
|
74
|
+
rescue StandardError => e
|
75
|
+
say_status "failed", "#{e.message.split(' @').first} #{dest_path}", :red
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def mkdir_p(path)
|
80
|
+
path = File.expand_path(path)
|
81
|
+
|
82
|
+
if File.directory?(path)
|
83
|
+
say_status "exist", "Ignore #{path}", :yellow
|
84
|
+
return
|
85
|
+
end
|
86
|
+
|
87
|
+
begin
|
88
|
+
FileUtils.mkdir_p(path)
|
89
|
+
say_status "create", "#{path}", :green
|
90
|
+
rescue StandardError => e
|
91
|
+
say_status "failed", "#{e.message.split(' @').first} #{path}", :red
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def find_current_tzid
|
96
|
+
require "digest/md5"
|
97
|
+
|
98
|
+
# Debian
|
99
|
+
if File.exists?("/etc/timezone")
|
100
|
+
return File.open("/etc/timezone").read.chomp
|
101
|
+
end
|
102
|
+
|
103
|
+
# Mac
|
104
|
+
if File.symlink?("/etc/localtime") &&
|
105
|
+
/([^\/]+\/[^\/]+)$/ =~ File.readlink("/etc/localtime")
|
106
|
+
return $1
|
107
|
+
end
|
108
|
+
|
109
|
+
# Red Had / CentOS
|
110
|
+
if File.exists?("/etc/sysconfig/clock") &&
|
111
|
+
/ZONE=["']?([^"']+)/ =~ File.open("/etc/sysconfig/clock").read.chomp
|
112
|
+
return $1
|
113
|
+
end
|
114
|
+
|
115
|
+
# generic including FreeBSD
|
116
|
+
if File.exists?("/etc/localtime")
|
117
|
+
localtime = Digest::MD5.file("/etc/localtime")
|
118
|
+
candidates = Dir.chdir("/usr/share/zoneinfo") do
|
119
|
+
Dir.glob("**/*").select do |fn|
|
120
|
+
File.file?(fn) && Digest::MD5.file(fn) == localtime
|
121
|
+
end
|
122
|
+
end
|
123
|
+
unless candidates.empty?
|
124
|
+
# take the most descriptive (has long name) one
|
125
|
+
return candidates.sort {|a,b| b.length <=> a.length}.first
|
126
|
+
end
|
127
|
+
end
|
128
|
+
return "Unknown"
|
129
|
+
end
|
130
|
+
|
131
|
+
end # class Init
|
132
|
+
end # module Command
|
133
|
+
end # module Mhc
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Mhc
|
2
|
+
module Command
|
3
|
+
class Scan
|
4
|
+
Encoding.default_external = "UTF-8"
|
5
|
+
|
6
|
+
def initialize(calendar, range_string, format: :text, search: nil, **options)
|
7
|
+
date_range = Mhc::PropertyValue::Date.parse_range(range_string)
|
8
|
+
formatter = Mhc::Formatter.build(formatter: format, date_range: date_range, **options)
|
9
|
+
format_range(calendar, formatter, date_range, search: search, **options)
|
10
|
+
end
|
11
|
+
|
12
|
+
def format_range(calendar, formatter, date_range, search: nil, **options)
|
13
|
+
if options[:category] and not search
|
14
|
+
search = "category:\"#{options[:category]}\""
|
15
|
+
end
|
16
|
+
|
17
|
+
if search
|
18
|
+
begin
|
19
|
+
search_proc = Mhc::Query.new(search).to_proc
|
20
|
+
rescue Mhc::Query::ParseError => e
|
21
|
+
STDERR.print "Error: " + e.message.capitalize + " in search string\n"
|
22
|
+
exit 1
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
calendar.occurrences(date_range, &search_proc).each do |oc|
|
27
|
+
formatter << oc
|
28
|
+
end
|
29
|
+
print formatter.to_s
|
30
|
+
end
|
31
|
+
end # class Scan
|
32
|
+
end # module Command
|
33
|
+
end # module Mhc
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Mhc
|
2
|
+
module Command
|
3
|
+
class Sync
|
4
|
+
Encoding.default_external = "UTF-8"
|
5
|
+
|
6
|
+
def initialize(channel_name, config)
|
7
|
+
channel = config.sync_channels[channel_name]
|
8
|
+
|
9
|
+
unless channel
|
10
|
+
STDERR.print "Error: Not found '#{channel_name}' in ~/.mhc/config.yml\n"
|
11
|
+
exit 1
|
12
|
+
end
|
13
|
+
|
14
|
+
builder = Mhc::Builder.new(config)
|
15
|
+
|
16
|
+
driver = builder.sync_driver(channel.name, channel.strategy)
|
17
|
+
driver.sync_all
|
18
|
+
end
|
19
|
+
|
20
|
+
end # class Sync
|
21
|
+
end # module Command
|
22
|
+
end # module Mhc
|
data/lib/mhc/config.rb
ADDED
@@ -0,0 +1,229 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'pp'
|
3
|
+
|
4
|
+
module Mhc
|
5
|
+
class Config
|
6
|
+
# Syntax table manipulation
|
7
|
+
class Syntax
|
8
|
+
def initialize(syntax_config)
|
9
|
+
@syntax_config = syntax_config
|
10
|
+
end
|
11
|
+
|
12
|
+
def keyword_symbols
|
13
|
+
@syntax_config.keys
|
14
|
+
end
|
15
|
+
|
16
|
+
def keywords
|
17
|
+
keyword_symbols.map {|sym| sym.to_s.upcase }
|
18
|
+
end
|
19
|
+
|
20
|
+
def keyword?(word)
|
21
|
+
if word.is_a?(Symbol)
|
22
|
+
keyword_symbols.member?(word)
|
23
|
+
else
|
24
|
+
# String
|
25
|
+
keywords.member?(word)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def instance_variable_name(word)
|
30
|
+
return nil unless keyword?(word)
|
31
|
+
return '@' + as_symbol(word).to_s
|
32
|
+
end
|
33
|
+
|
34
|
+
def item_class(word)
|
35
|
+
return nil unless keyword?(word)
|
36
|
+
@syntax_config[as_symbol(word)]
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
def as_symbol(word)
|
41
|
+
word.to_s.downcase.sub(/^@+/, "").to_sym
|
42
|
+
end
|
43
|
+
end # class Syntax
|
44
|
+
|
45
|
+
# Parse Key-Value object in YAML
|
46
|
+
class Base
|
47
|
+
# attr_accessor :name
|
48
|
+
|
49
|
+
def self.create_from_yaml_file(yaml_file)
|
50
|
+
yaml_string = File.open(File.expand_path(yaml_file)).read
|
51
|
+
return create_from_yaml_string(yaml_string, yaml_file)
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.create_from_yaml_string(yaml_string, filename = nil)
|
55
|
+
hash = YAML.load(yaml_string, filename) || {}
|
56
|
+
return new(hash)
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.define_syntax(config)
|
60
|
+
@syntax = Syntax.new(config)
|
61
|
+
@syntax.keyword_symbols.each do |sym|
|
62
|
+
attr_accessor sym # XXX: attr_reader is enough?
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.syntax
|
67
|
+
return @syntax
|
68
|
+
end
|
69
|
+
|
70
|
+
def initialize(hash = {})
|
71
|
+
@original_hash = hash
|
72
|
+
(hash || {}).each do |key, val|
|
73
|
+
raise Mhc::ConfigurationError, "config syntax error (#{key})" unless syntax.keyword?(key)
|
74
|
+
var = syntax.instance_variable_name(key)
|
75
|
+
obj = create_subnode(key, val)
|
76
|
+
instance_variable_set(var, obj)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
attr_reader :original_hash
|
81
|
+
|
82
|
+
def get_value(dot_separated_string = nil)
|
83
|
+
if dot_separated_string.to_s == ""
|
84
|
+
return original_hash
|
85
|
+
end
|
86
|
+
|
87
|
+
key, subkey = dot_separated_string.to_s.upcase.split(".", 2)
|
88
|
+
subnode = get_subnode(key)
|
89
|
+
|
90
|
+
if subnode.respond_to?(:get_value)
|
91
|
+
return subnode.get_value(subkey)
|
92
|
+
else
|
93
|
+
return subnode.to_s
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def to_yaml
|
98
|
+
return self.to_hash.to_yaml
|
99
|
+
end
|
100
|
+
|
101
|
+
def to_hash
|
102
|
+
hash = {}
|
103
|
+
syntax.keywords.each do |key|
|
104
|
+
var = syntax.instance_variable_name(key)
|
105
|
+
obj = instance_variable_get(var)
|
106
|
+
obj = obj.respond_to?(:to_hash) ? obj.to_hash : obj.to_s
|
107
|
+
hash[key] = obj
|
108
|
+
end
|
109
|
+
return hash
|
110
|
+
end
|
111
|
+
|
112
|
+
private
|
113
|
+
def syntax
|
114
|
+
self.class.syntax
|
115
|
+
end
|
116
|
+
|
117
|
+
def get_subnode(key)
|
118
|
+
raise Mhc::ConfigurationError, "Invalid key: #{key}" unless syntax.keyword?(key)
|
119
|
+
return instance_variable_get(syntax.instance_variable_name(key))
|
120
|
+
end
|
121
|
+
|
122
|
+
def create_subnode(keyword, value)
|
123
|
+
item_class = syntax.item_class(keyword)
|
124
|
+
if item_class.is_a?(Array)
|
125
|
+
return List.new(item_class.first, value)
|
126
|
+
elsif item_class == String
|
127
|
+
return value.to_s
|
128
|
+
else
|
129
|
+
return item_class.new(value)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
end # class Base
|
134
|
+
|
135
|
+
# Parse Array object in YAML
|
136
|
+
class List < Base
|
137
|
+
include Enumerable
|
138
|
+
|
139
|
+
def initialize(item_class, array = [])
|
140
|
+
@original_hash = array
|
141
|
+
@configs = []
|
142
|
+
(array || []).each do |value|
|
143
|
+
item = item_class.new(value)
|
144
|
+
@configs << item
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def [](key)
|
149
|
+
@configs.find {|c| c.name == key}
|
150
|
+
end
|
151
|
+
|
152
|
+
alias_method :get_subnode, :[]
|
153
|
+
|
154
|
+
def <<(conf)
|
155
|
+
@configs << conf
|
156
|
+
end
|
157
|
+
|
158
|
+
def to_hash # XXX: actually, it returns a Array
|
159
|
+
return @configs.map {|c| c.respond_to?(:to_hash) ? c.to_hash : c.to_s}
|
160
|
+
end
|
161
|
+
|
162
|
+
def each
|
163
|
+
@configs.each do |conf|
|
164
|
+
yield conf
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end # List
|
168
|
+
|
169
|
+
## concrete config classes
|
170
|
+
|
171
|
+
class General < Base
|
172
|
+
define_syntax :tzid => String,
|
173
|
+
:repository => String
|
174
|
+
end # class General
|
175
|
+
|
176
|
+
class SyncChannel < Base
|
177
|
+
define_syntax :name => String,
|
178
|
+
:calendar1 => String,
|
179
|
+
:calendar2 => String,
|
180
|
+
:strategy => String
|
181
|
+
end # class SyncChannel
|
182
|
+
|
183
|
+
class Calendar < Base
|
184
|
+
define_syntax :name => String,
|
185
|
+
:type => String,
|
186
|
+
:user => String,
|
187
|
+
:password => String,
|
188
|
+
:url => String,
|
189
|
+
:filter => Mhc::Query,
|
190
|
+
:modifiers => [Mhc::Modifier]
|
191
|
+
end # class Calendar
|
192
|
+
|
193
|
+
# Top-Level Config
|
194
|
+
class Top < Base
|
195
|
+
define_syntax :general => General,
|
196
|
+
:sync_channels => [SyncChannel],
|
197
|
+
:calendars => [Calendar]
|
198
|
+
|
199
|
+
def embed_values
|
200
|
+
super hash
|
201
|
+
self.sync_channels.each do |ch|
|
202
|
+
# String -> Calendar
|
203
|
+
ch.calendar1 = calendars[ch.calendar1] if calendars[ch.calendar1]
|
204
|
+
ch.calendar2 = calendars[ch.calendar2] if calendars[ch.calendar2]
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end # class Top
|
208
|
+
|
209
|
+
def self.create_from_file(file_name)
|
210
|
+
unless File.exists?(File.expand_path(file_name))
|
211
|
+
raise Mhc::ConfigurationError, "config file '#{file_name}' not found"
|
212
|
+
end
|
213
|
+
begin
|
214
|
+
return Top.create_from_yaml_file(file_name)
|
215
|
+
rescue Psych::SyntaxError, Mhc::Query::ParseError, Mhc::Modifier::ParseError => e
|
216
|
+
raise Mhc::ConfigurationError, e.message
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
def self.create_from_string(string)
|
221
|
+
begin
|
222
|
+
return Top.create_from_yaml_string(string)
|
223
|
+
rescue Psych::SyntaxError, Mhc::Query::ParseError, Mhc::Modifier::ParseError => e
|
224
|
+
raise Mhc::ConfigurationError, e.message
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
end # class Config
|
229
|
+
end # module Mhc
|