File: mhc

package info (click to toggle)
mhc 1.2.4-2
  • links: PTS, VCS
  • area: main
  • in suites: bullseye
  • size: 2,424 kB
  • sloc: ruby: 12,718; lisp: 7,570; makefile: 70; sh: 68
file content (394 lines) | stat: -rwxr-xr-x 11,351 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
#!/usr/bin/env ruby

################################################################
# rbenv support:
# If this file is a symlink, and bound to a specific ruby
# version via rbenv (indicated by RBENV_VERSION),
# I want to resolve the symlink and re-exec
# the original executable respecting the .ruby_version
# which should indicate the right version.
#
if File.symlink?(__FILE__) and ENV["RBENV_VERSION"]
  ENV["RBENV_VERSION"] = nil
  shims_path = File.expand_path("shims", ENV["RBENV_ROOT"])
  ENV["PATH"] = shims_path + ":" + ENV["PATH"]
  exec(File.readlink(__FILE__), *ARGV)
end

#gemfile = File.expand_path("../../Gemfile", __FILE__)
#
#if File.exists?(gemfile + ".lock")
#  ENV["BUNDLE_GEMFILE"] = gemfile
#  require "bundler/setup"
#end

#require "rubygems"
require "thor"
require "mhc"

Encoding.default_external="UTF-8"

class MhcCLI < Thor
  ################################################################
  # constants

  DEFAULT_CONFIG_HOME = File.join((ENV["XDG_CONFIG_HOME"] || "~/.config"), "mhc")
  DEFAULT_CONFIG_FILE = "config.yml"
  DEFAULT_CONFIG_PATH = File.join(DEFAULT_CONFIG_HOME, DEFAULT_CONFIG_FILE)

  package_name 'MHC'

  ################################################################
  # class methods

  class << self
    attr_accessor :calendar
    attr_accessor :popular_options
  end

  def self.register_option(name, options)
    @popular_options ||= {}
    @popular_options[name] = options
  end

  def self.named_option(*names)
    names.each do |name|
      method_option name, @popular_options[name]
    end
  end

  ################################################################
  # global options

  class_option :debug,   :desc => "Set debug flag", :type => :boolean
  class_option :profile, :desc => "Set profiler flag", :type => :boolean
  class_option :config,  :desc => "Set config path (default: #{DEFAULT_CONFIG_PATH})", :banner => "FILE"

  check_unknown_options! :except => :completions

  ################################################################
  # frequently used options

  register_option :repository, :desc => "Set MHC top directory", :banner => "DIRECTORY"
  register_option :calendar,   :desc => "Set source CALENDAR"
  register_option :category,   :desc => "Pick items only in CATEGORY"
  register_option :format,     :desc => "Set printing format", :enum => %w(text mail orgtable emacs icalendar calfw howm json)
  register_option :search,     :desc => "Search items by complex expression"
  register_option :dry_run,    :desc => "Perform a trial run with no changes made", :type => :boolean

  ################################################################
  # command name mappings

  map ["--version", "-v"] => :version

  map ["--help", "-h"] => :help
  default_command :help

  ################################################################
  # Command: help
  ################################################################

  desc "help [COMMAND]", "Describe available commands or one specific command"
  def help(command = nil)
    super(command)
  end

  ################################################################
  # Command: version
  ################################################################
  desc "version", "Show version"

  def version
    puts Mhc::VERSION
  end

  ################################################################
  # Command: cache
  ################################################################
  desc "cache", "Dump cache file"

  named_option :repository

  def cache
    Mhc::Command::Cache.new(builder.datastore)
  end

  # Command: todo
  desc "todo", "List Todo entries in MHC calendar"

  named_option :repository
  method_option :show_all, :desc => "Include all finished tasks."

  def todo
    todos = []
    calendar.tasks.each do |task|
      if task.recurring?
        # Yearly: today - 90days .. today + 365d - 90days ?
        # Weekly: today - 7days .. today + 7days
        search_range = Mhc::PropertyValue::Date.parse_range("today+365d")
        # search_range = nil
      else
        search_range = nil
      end
      next if task.in_category?("done") && !options[:show_all]
      todos << task.occurrences(range: search_range).first
    end
    todos.each.sort{|a, b| a.dtstart <=> b.dtstart}.each do |t|
      deadline = t.dtstart
      deadline_string = ""
      remaining = (deadline - Mhc::PropertyValue::Date.today).to_i
      if remaining == 0
        deadline_string = " (due this date)"
      elsif remaining > 0
        deadline_string = format(" (%d days to go)", remaining)
      else
        deadline_string = format(" (%d days overdue)", -remaining)
      end
      location_string = " [#{t.location}]" if !t.location.empty?
      puts format("%s %-11s %s%s%s",
                  deadline.strftime("%Y/%m/%d %a"),
                  t.time_range.to_mhc_string,
                  t.subject, location_string, deadline_string)
    end
  end # todo

  ################################################################
  # Command: completions
  ################################################################
  desc "completions [COMMAND]", "List available commands or options for COMMAND", :hide => true

  long_desc <<-LONGDESC
    List available commands or options for COMMAND
    This is supposed to be a zsh compsys helper"
  LONGDESC

  def completions(*command)
    help = self.class.commands
    global_options = self.class.class_options
    Mhc::Command::Completions.new(help, global_options, command, config)
  end

  ################################################################
  # Command: config
  ################################################################
  desc "configuration", "Show current configuration in various formats."

  named_option :format

  def configuration(name = nil)
    puts Mhc::Converter::Emacs.new.to_emacs(config.get_value(name))
  end

  ################################################################
  # Command: init
  ################################################################
  desc "init DIRECTORY", "Initialize MHC repository and configuration template"

  def init(top_dir)
    Mhc::Command::Init.new(top_dir, options[:config] || DEFAULT_CONFIG_PATH, ENV["MHC_TZID"])
  end

  ################################################################
  # Command: scan
  ################################################################
  desc "scan RANGE", "Scan events in date RANGE"

  long_desc <<-LONGDESC
    scan events in date RANGE.

    RANGE is one of:
    \x5 + START-YYYYMMDD
    \x5 + START[+LENGTH]

    START is one of:
    \x5 + today, tomorrow, sun ... sat, yyyymmdd
    \x5 + thismonth, nextmonth, yyyymm

    LENGTH is a number followed by a SUFFIX. SUFFIX is one of:
    \x5 + d (days)
    \x5 + w (weeks)
    \x5 + m (months)

    If LENGTH is omitted, it is treated as '1d' or '1m' depending on
    which type of START is set.

    Examples:
    \x5 mhc scan 20140101-20141231
    \x5 mhc scan 2140101+3d
    \x5 mhc scan today --category 'Business'
    \x5 mhc scan thismonth --search 'category:Business & !subject:"Trip"'
  LONGDESC

  named_option :calendar, :category, :format, :repository, :search

  def scan(range)
    begin
      Mhc::Command::Scan.new(calendar, range, **symbolize_keys(options))
    rescue Mhc::PropertyValue::ParseError, Mhc::Formatter::NameError, Mhc::Query::ParseError => e
      STDERR.print "Error: " + e.message + "\n"
    end
    return self
  end

  ################################################################
  # Command: server
  ################################################################
  desc "server", "Invoked as server (backend of emacs)"

  named_option :repository

  def server
    require "shellwords"
    while line = STDIN.gets # STDIN.noecho(&:gets)
      argv = line.chomp.shellsplit
      self.class.start(argv)
      STDOUT.flush
    end
  end

  ################################################################
  # Command: show
  ################################################################
  desc "show MESSAGE_ID", "Show article found by MESSAGE_ID"

  named_option :calendar, :repository

  def show(message_id)
    event = exit_on_error do
      calendar.find(uid: message_id)
    end
    print event.dump if event
  end

  ################################################################
  # Command: sync
  ################################################################
  desc "sync SYNC_CHANNEL", "Synchronize DBs via SYNC_CHANNEL"

  named_option :dry_run

  def sync(channel_name)
    driver = exit_on_error do
      builder.sync_driver(channel_name)
    end
    driver.sync_all(options[:dry_run])
    return self
  end

  ################################################################
  # Command: validate
  ################################################################
  desc "validate FILE", "Validate event FILE"

  named_option :format

  def validate(file)
    full_path = File.expand_path(file)

    unless File.exist?(full_path)
      puts Mhc::Converter::Emacs.new.to_emacs("No such file #{file}.")
      return 1
    end

    errors = Mhc::Event.validate(File.open(full_path) {|f| f.read})

    string = ""
    exit_on_error do
      errors.each do |err, key|
        string += "#{err.to_s.capitalize}"
        string += " in X-SC-#{key.capitalize}" if key
        string += ".\n"
      end
    end
    if errors.empty?
      puts Mhc::Converter::Emacs.new.to_emacs("OK")
      return 0
    end

    puts Mhc::Converter::Emacs.new.to_emacs(string)
    return 1
  end

  ################################################################
  # add some hooks to Thor

  no_commands do
    def invoke_command(command, *args)
      setup_global_options unless command.name == "init"
      result = super
      teardown
      result
    end
  end

  ################################################################
  # private

  private

  def exit_on_error(&block)
    begin
      yield if block_given?
    rescue Mhc::ConfigurationError => e
      STDERR.print "ERROR: #{e.message}.\n"
      exit 1
    end
  end

  attr_reader :builder, :config, :calendar

  def setup_global_options
    exit_on_error do
      @config = Mhc::Config.create_from_file(options[:config] || DEFAULT_CONFIG_PATH)
      @builder ||= Mhc::Builder.new(@config)
      if @config.general.tzid
        Mhc.default_tzid = @config.general.tzid
      end

      calname  = options[:calendar] || @config.calendars.first.name
      @config.general.repository = options[:repository] if options[:repository]

      self.class.calendar ||= builder.calendar(calname)
      @calendar = self.class.calendar
    end

    load_plugins

    if options[:profile]
      require 'profiler'
      Profiler__.start_profile
    end
    if options[:debug]
      require "pp"
      $MHC_DEBUG = true
      $MHC_DEBUG_FOR_DEVELOPER = true if ENV["MHC_DEBUG_FOR_DEVELOPER"]
    end
  end

  def load_plugins
    config_path = options[:config] || DEFAULT_CONFIG_PATH
    plugin_dir  = File.dirname(config_path)

    Dir.glob(File.expand_path("plugins/*.rb", plugin_dir)) do |rb|
      require rb
    end
  end

  def teardown
    if options[:profile]
      Profiler__.print_profile($stdout)
    end
  end

  def symbolize_keys(hash)
    Hash[hash.map {|k,v| [k.to_sym, v]}]
  end
end

result = MhcCLI.start(ARGV)

if result.is_a?(Numeric)
  exit result
else
  exit 0
end