Script: cron.py

Time-based scheduler, like cron and at.
Author: FlashCode — Version: 0.6 — License: GPL-3.0-or-later
For WeeChat ≥ 0.3.0.
Tags: cron, time, py2, py3
Added: 2010-07-26 — Updated: 2021-11-07

Download GitHub Repository

  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
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
# -*- coding: utf-8 -*-
#
# Copyright (C) 2010-2021 Sébastien Helleu <flashcode@flashtux.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#

#
# Time-based scheduler, like cron and at.
# (this script requires WeeChat 0.3.0 or newer)
#
# History:
#
# 2021-11-07, Sébastien Helleu <flashcode@flashtux.org>:
#     version 0.6: replace calls to function hook_completion_list_add by
#                  completion_list_add
# 2021-05-02, Sébastien Helleu <flashcode@flashtux.org>:
#     version 0.5: add compatibility with WeeChat >= 3.2 (XDG directories)
# 2012-01-03, Sébastien Helleu <flashcode@flashtux.org>:
#     version 0.4: make script compatible with Python 3.x
# 2011-02-13, Sébastien Helleu <flashcode@flashtux.org>:
#     version 0.3: use new help format for command arguments
# 2010-07-31, Sébastien Helleu <flashcode@flashtux.org>:
#     version 0.2: add keyword "commands" to run many commands
# 2010-07-26, Sébastien Helleu <flashcode@flashtux.org>:
#     version 0.1: initial release
# 2010-07-20, Sébastien Helleu <flashcode@flashtux.org>:
#     script creation
#

SCRIPT_NAME    = "cron"
SCRIPT_AUTHOR  = "Sébastien Helleu <flashcode@flashtux.org>"
SCRIPT_VERSION = "0.6"
SCRIPT_LICENSE = "GPL3"
SCRIPT_DESC    = "Time-based scheduler, like cron and at"

import_ok = True

try:
    import weechat
except:
    print("This script must be run under WeeChat.")
    print("Get WeeChat now at: https://weechat.org/")
    import_ok = False

try:
    import os, stat, time
    from datetime import datetime, timedelta
except ImportError as message:
    print("Missing package(s) for %s: %s" % (SCRIPT_NAME, message))
    import_ok = False

# script options
cron_settings = {
    "auto_reload" : "on",          # auto reload cron file each minute if file has changed
                                   # if set to off, then command "/cron reload" must be executed manually
    "auto_save"   : "on",          # auto save jobs to file (when adding/removing job, and when unloading script)
    "filename"    : "%h/cron.txt", # cron filename
    "quiet_load"  : "off",         # silently load crontab file (no message displayed, except errors)
    "quiet_save"  : "on" ,         # silently save crontab file (no message displayed, except errors)
    "quiet_exec"  : "off",         # silently exec commands (no error displayed)
}

crontab = []
cron_last_read_time = 0
cron_commands = {
    "print"     : "print a message on buffer",
    "print_hl"  : "print a message on buffer with \"highlight\" notify on line",
    "print_msg" : "print a message on buffer with \"message\" notify on line",
    "command"   : "execute a command (starting with \"/\") or send text to buffer (like input)",
    "commands"  : "execute many commands separated by \";\"",
    "python"    : "evaluate python code",
}

# ================================[ cron jobs ]===============================

class AlwaysMatch(set):
    def __contains__(self, item):
        return True
    def __str__(self):
        return "*"

alwaysMatch = AlwaysMatch()

def cron_str2int(str):
    """ Convert day name to digit. """
    days = [ "sun", "mon", "tue", "wed", "thu", "fri", "sat" ]
    try:
        pos = days.index(str)
        if pos != ValueError:
            return pos
    except:
        try:
            value = int(str)
            return value
        except:
            return 0

def cron_str2set(str, min_value, max_value):
    """ Convert string with range to a set. """
    if str == "*":
        return alwaysMatch
    ret = set([])
    try:
        items = str.split(",")
        for item in items:
            values = item
            skip = 1
            pos = values.find("/")
            if pos > 0:
                try:
                    skip = int(values[pos+1:])
                except:
                    skip = 1
                values = values[0:pos]
            pos = values.find("-")
            if pos > 0:
                value1 = cron_str2int(values[0:pos])
                value2 = cron_str2int(values[pos+1:])
            else:
                if values == "*":
                    value1 = min_value
                    value2 = max_value
                else:
                    value1 = cron_str2int(values)
                    value2 = cron_str2int(values)
            ret = set.union(ret, set(range(value1, value2 + 1, skip)))
    except:
        weechat.prnt("", "%scron: error with time string \"%s\""
                     % (weechat.prefix("error"), str))
        return alwaysMatch
    return ret

def cron_set2str(obj):
    """ Convert a set to a string, for display (sort set). """
    if isinstance(obj, AlwaysMatch):
        return "*"
    l = list(obj)
    l.sort()
    return "%s" % l

class CronJob(object):
    """ Class for a job in crontab. """
    def __init__(self, minute=alwaysMatch, hour=alwaysMatch,
                 monthday=alwaysMatch, month=alwaysMatch, weekday=alwaysMatch,
                 repeat="*", buffer="core.weechat", command=""):
        self.minute = minute
        self.minutes = cron_str2set(minute, 0, 59)
        self.hour = hour
        self.hours = cron_str2set(hour, 0, 23)
        self.monthday = monthday
        self.monthdays = cron_str2set(monthday, 1, 31)
        self.month = month
        self.months = cron_str2set(month, 1, 12)
        self.weekday = weekday
        self.weekdays = cron_str2set(weekday, 0, 6)
        if repeat == "*":
            self.repeat = -1
        else:
            try:
                self.repeat = int(repeat)
                if self.repeat < 1:
                    self.repeat = 1
            except:
                self.remaining_exec = 1
        self.buffer = buffer
        self.command = command

    def str_repeat(self):
        if self.repeat < 0:
            return "*"
        return "%d" % self.repeat

    def __str__(self):
        """ Job to string. """
        return "%s %s %s %s %s %s %s %s" % (self.minute, self.hour, self.monthday,
                                            self.month, self.weekday,
                                            self.str_repeat(), self.buffer, self.command)

    def str_debug(self):
        """ Job to string with detail (to view sets). """
        return "%s %s %s %s %s %d %s %s" % (cron_set2str(self.minutes),
                                            cron_set2str(self.hours),
                                            cron_set2str(self.monthdays),
                                            cron_set2str(self.months),
                                            cron_set2str(self.weekdays),
                                            self.repeat, self.buffer, self.command)

    def matchtime(self, t):
        """ Check if this job matches given time (ie command must be executed). """
        return ((t.minute in self.minutes) and
                (t.hour in self.hours) and
                (t.day in self.monthdays) and
                (t.month in self.months) and
                ((t.isoweekday() % 7) in self.weekdays))

    def exec_command(self, userExec=False):
        """ Execute job command. """
        global cron_commands
        display_error = weechat.config_get_plugin("quiet_exec") == "off"
        buf = ""
        if self.buffer == "current":
            buf = weechat.current_buffer()
        else:
            items = self.buffer.split(".", 1)
            if len(items) >= 2:
                buf = weechat.buffer_search(items[0], items[1])
        if buf:
            argv = self.command.split(None, 1)
            if len(argv) > 0 and argv[0] in cron_commands:
                if argv[0] == "print":
                    weechat.prnt(buf, "%s" % argv[1])
                elif argv[0] == "print_hl":
                    weechat.prnt_date_tags(buf, 0, "notify_highlight", "%s" % argv[1])
                elif argv[0] == "print_msg":
                    weechat.prnt_date_tags(buf, 0, "notify_message", "%s" % argv[1])
                elif argv[0] == "command":
                    weechat.command(buf, "%s" % argv[1])
                elif argv[0] == "commands":
                    cmds = argv[1].split(";")
                    for cmd in cmds:
                        weechat.command(buf, "%s" % cmd)
                elif argv[0] == "python":
                    eval(argv[1])
            elif display_error:
                weechat.prnt("", "%scron: unknown command (\"%s\")"
                             % (weechat.prefix("error"), self.command))
        else:
            if display_error:
                weechat.prnt("", "%scron: buffer \"%s\" not found"
                             % (weechat.prefix("error"), self.buffer))
        if not userExec and self.repeat > 0:
            self.repeat -= 1

# ============================[ load/save crontab ]===========================

def cron_filename():
    """ Get crontab filename. """
    options = {
        'directory': 'config',
    }
    return weechat.string_eval_path_home(
        weechat.config_get_plugin("filename"), {}, {}, options)

def cron_str_job_count(number):
    """ Get string with "%d jobs". """
    if number <= 1:
        return "%d job" % number
    return "%d jobs" % number

def cron_load(force_message=False):
    """ Load crontab from file. """
    global crontab, cron_last_read_time
    display_message = weechat.config_get_plugin("quiet_load") == "off" or force_message
    crontab = []
    filename = cron_filename()
    if os.path.isfile(filename):
        f = open(filename)
        for line in f:
            line = line.lstrip().rstrip("\r\n")
            if not line.startswith("#"):
                argv = line.split(None, 7)
                if len(argv) >= 8:
                    crontab.append(CronJob(argv[0], argv[1], argv[2], argv[3],
                                           argv[4], argv[5], argv[6], argv[7]))
        f.close()
        cron_last_read_time = time.time()
        if display_message:
            weechat.prnt("", "cron: %s loaded from \"%s\""
                         % (cron_str_job_count(len(crontab)), filename))
    else:
        if cron_last_read_time != 0:
            cron_last_read_time = 0
            if display_message:
                weechat.prnt("", "cron: reset (file not found: \"%s\")" % filename)
        elif display_message:
            weechat.prnt("", "cron: file not found: \"%s\"" % filename)

def cron_save(force_message=False):
    """ Save crontab in file. """
    global crontab, cron_last_read_time
    display_message = weechat.config_get_plugin("quiet_save") == "off" or force_message
    filename = cron_filename()
    f = open(filename, "w")
    f.write("#\n")
    f.write("# WeeChat crontab for script cron.py\n")
    f.write("# format: min hour monthday month weekday repeat buffer command args\n")
    f.write("#\n")
    for job in crontab:
        f.write("%s\n" % job)
    f.close()
    cron_last_read_time = time.time()
    if display_message:
        weechat.prnt("", "cron: %s saved to \"%s\""
                     % (cron_str_job_count(len(crontab)), filename))

def cron_reload_needed():
    """ Return True if crontab must be reloaded, False if it is not needed. """
    global cron_last_read_time
    filename = cron_filename()
    if os.path.isfile(filename):
        return cron_last_read_time == 0 or os.stat(filename)[stat.ST_MTIME] > cron_last_read_time
    else:
        return cron_last_read_time != 0

# ============================[ crontab functions ]===========================

def cron_list(debug=False):
    """ Display list of jobs in crontab. """
    global crontab
    if len(crontab) == 0:
        weechat.prnt("", "cron: empty crontab")
    else:
        weechat.prnt("", "crontab:")
        for i, job in enumerate(crontab):
            if debug:
                str_job = "%s" % job.str_debug()
            else:
                str_job = "%s" % job
            weechat.prnt("", "  %s[%s%03d%s]%s %s"
                         % (weechat.color("chat_delimiters"), weechat.color("chat"),
                            i + 1,
                            weechat.color("chat_delimiters"), weechat.color("chat"),
                            str_job))

def cron_add(minute, hour, monthday, month, weekday, repeat, buffer, command):
    """ Add a job to crontab. """
    global crontab
    job = CronJob(minute, hour, monthday, month, weekday, repeat, buffer, command)
    crontab.append(job)
    weechat.prnt("", "cron: job added:  %s" % job)
    if weechat.config_get_plugin("auto_save") == "on":
        cron_save()

def cron_current_time():
    """ Get current time. """
    return datetime(*datetime.now().timetuple()[:5])

# ==============================[ at functions ]==============================

def cron_at_time(strtime):
    reftime = datetime.now()
    if strtime.startswith("+"):
        try:
            strtime = strtime[1:]
            delta = timedelta(minutes=1)
            unit = strtime[-1]
            if unit in ["m", "h", "d"]:
                value = int(strtime[:-1])
            else:
                value = int(strtime)
                unit = "m"
            if unit == "m":
                delta = timedelta(minutes=value)
            elif unit == "h":
                delta = timedelta(hours=value)
            elif unit == "d":
                delta = timedelta(days=value)
            reftime += delta
        except:
            return None
        return [reftime.hour, reftime.minute]
    else:
        items = strtime.split(":", 1)
        if len(items) >= 2:
            try:
                hour = int(items[0])
                minute = int(items[1])
            except:
                return None
            return [hour, minute]

# ================================[ commands ]================================

def cron_completion_time_cb(data, completion_item, buffer, completion):
    """ Complete with time, for command '/cron'. """
    weechat.completion_list_add(completion, "*",
                                0, weechat.WEECHAT_LIST_POS_BEGINNING)
    return weechat.WEECHAT_RC_OK

def cron_completion_repeat_cb(data, completion_item, buffer, completion):
    """ Complete with repeat, for command '/cron'. """
    weechat.completion_list_add(completion, "*",
                                0, weechat.WEECHAT_LIST_POS_BEGINNING)
    return weechat.WEECHAT_RC_OK

def cron_completion_buffer_cb(data, completion_item, buffer, completion):
    """ Complete with buffer, for command '/cron'. """
    infolist = weechat.infolist_get("buffer", "", "")
    while weechat.infolist_next(infolist):
        plugin_name = weechat.infolist_string(infolist, "plugin_name")
        name = weechat.infolist_string(infolist, "name")
        weechat.completion_list_add(completion,
                                    "%s.%s" % (plugin_name, name),
                                    0, weechat.WEECHAT_LIST_POS_SORT)
    weechat.infolist_free(infolist)
    weechat.completion_list_add(completion, "current",
                                0, weechat.WEECHAT_LIST_POS_BEGINNING)
    weechat.completion_list_add(completion, "core.weechat",
                                0, weechat.WEECHAT_LIST_POS_BEGINNING)
    return weechat.WEECHAT_RC_OK

def cron_completion_keyword_cb(data, completion_item, buffer, completion):
    """ Complete with cron keyword, for command '/cron'. """
    global cron_commands
    for command in sorted(cron_commands.keys()):
        weechat.completion_list_add(completion, command,
                                    0, weechat.WEECHAT_LIST_POS_END)
    return weechat.WEECHAT_RC_OK

def cron_completion_commands_cb(data, completion_item, buffer, completion):
    """ Complete with commands, for command '/cron'. """
    infolist = weechat.infolist_get("hook", "command", "")
    while weechat.infolist_next(infolist):
        command = weechat.infolist_string(infolist, "command")
        if command.startswith("/"):
            command = command[1:]
        if command:
            weechat.completion_list_add(completion, "/%s" % command,
                                        0, weechat.WEECHAT_LIST_POS_SORT)
    weechat.infolist_free(infolist)
    return weechat.WEECHAT_RC_OK

def cron_completion_number_cb(data, completion_item, buffer, completion):
    """ Complete with jobs numbers, for command '/cron'. """
    global crontab
    if len(crontab) > 0:
        for i in reversed(range(0, len(crontab))):
            weechat.completion_list_add(completion, "%d" % (i + 1),
                                        0, weechat.WEECHAT_LIST_POS_BEGINNING)
    return weechat.WEECHAT_RC_OK

def cron_completion_at_time_cb(data, completion_item, buffer, completion):
    """ Complete with time, for command '/at'. """
    weechat.completion_list_add(completion, "+5m",
                                0, weechat.WEECHAT_LIST_POS_END)
    weechat.completion_list_add(completion, "20:00",
                                0, weechat.WEECHAT_LIST_POS_END)
    return weechat.WEECHAT_RC_OK

def cron_cmd_cb(data, buffer, args):
    """ Command /cron. """
    global crontab, cron_commands
    if args in ["", "list"]:
        cron_list()
        return weechat.WEECHAT_RC_OK
    if args == "debug":
        cron_list(debug=True)
        return weechat.WEECHAT_RC_OK
    argv = args.split(None, 9)
    if len(argv) > 0:
        if argv[0] == "add":
            if len(argv) >= 10:
                if argv[8] not in cron_commands:
                    weechat.prnt("", "%scron: unknown keyword \"%s\""
                                 % (weechat.prefix("error"), argv[8]))
                    return weechat.WEECHAT_RC_ERROR
                cron_add(argv[1], argv[2], argv[3], argv[4], argv[5],
                         argv[6], argv[7], argv[8] + " " + argv[9])
                return weechat.WEECHAT_RC_OK
        elif argv[0] in ["del", "exec"]:
            if argv[0] == "del" and argv[1] == "-all":
                crontab = []
                weechat.prnt("", "cron: all jobs deleted")
                if weechat.config_get_plugin("auto_save") == "on":
                    cron_save()
                return weechat.WEECHAT_RC_OK
            try:
                number = int(argv[1])
                if number < 1 or number > len(crontab):
                    weechat.prnt("", "%scron: job number not found" % weechat.prefix("error"))
                    return weechat.WEECHAT_RC_OK
                if argv[0] == "del":
                    job = crontab.pop(number - 1)
                    weechat.prnt("", "cron: job #%d deleted (%s)" % (number, job))
                    if weechat.config_get_plugin("auto_save") == "on":
                        cron_save()
                elif argv[0] == "exec":
                    job = crontab[number - 1]
                    job.exec_command(userExec=True)
            except:
                weechat.prnt("", "%scron: job number not found" % weechat.prefix("error"))
            return weechat.WEECHAT_RC_OK
        elif argv[0] == "reload":
            cron_load(force_message=True)
            return weechat.WEECHAT_RC_OK
        elif argv[0] == "save":
            cron_save(force_message=True)
            return weechat.WEECHAT_RC_OK
    weechat.prnt("", "%scron: invalid arguments" % weechat.prefix("error"))
    return weechat.WEECHAT_RC_OK

def cron_at_cmd_cb(data, buffer, args):
    """ Command /at. """
    global crontab, cron_commands
    if args in ["", "list"]:
        cron_list()
        return weechat.WEECHAT_RC_OK
    if args == "debug":
        cron_list(debug=True)
        return weechat.WEECHAT_RC_OK
    argv = args.split(None, 3)
    if len(argv) >= 4:
        if argv[2] not in cron_commands:
            weechat.prnt("", "%scron: unknown keyword \"%s\""
                         % (weechat.prefix("error"), argv[2]))
            return weechat.WEECHAT_RC_ERROR
        hour_min = cron_at_time(argv[0])
        if hour_min == None:
            weechat.prnt("", "%scron: invalid time \"%s\""
                         % (weechat.prefix("error"), argv[0]))
            return weechat.WEECHAT_RC_OK
        cron_add(str(hour_min[1]), str(hour_min[0]), "*", "*", "*",
                 "1", argv[1], argv[2] + " " + argv[3])
        return weechat.WEECHAT_RC_OK
    weechat.prnt("", "%scron: invalid arguments" % weechat.prefix("error"))
    return weechat.WEECHAT_RC_OK

# ==================================[ timer ]=================================

def cron_timer_cb(data, remaining_calls):
    """ Timer called each minute. """
    global crontab
    t1 = cron_current_time()
    if weechat.config_get_plugin("auto_reload") == "on":
        if cron_reload_needed():
            cron_load()
    crontab2 = []
    for job in crontab:
        if job.matchtime(t1):
            job.exec_command()
        if job.repeat != 0:
            crontab2.append(job)
    crontab = crontab2
    return weechat.WEECHAT_RC_OK

# ==================================[ main ]==================================

def cron_unload():
    """ Called when script is unloaded. """
    if weechat.config_get_plugin("auto_save") == "on":
        cron_save()
    return weechat.WEECHAT_RC_OK

if __name__ == "__main__" and import_ok:
    if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION,
                        SCRIPT_LICENSE, SCRIPT_DESC,
                        "cron_unload", ""):
        # set default settings
        for option, default_value in cron_settings.items():
            if not weechat.config_is_set_plugin(option):
                weechat.config_set_plugin(option, default_value)
        # completions for commands
        weechat.hook_completion("cron_time", "cron time", "cron_completion_time_cb", "")
        weechat.hook_completion("cron_repeat", "cron repeat", "cron_completion_repeat_cb", "")
        weechat.hook_completion("cron_buffer", "cron buffer", "cron_completion_buffer_cb", "")
        weechat.hook_completion("cron_keyword", "cron keyword", "cron_completion_keyword_cb", "")
        weechat.hook_completion("cron_commands", "cron commands", "cron_completion_commands_cb", "")
        weechat.hook_completion("cron_number", "cron number", "cron_completion_number_cb", "")
        weechat.hook_completion("at_time", "at time", "cron_completion_at_time_cb", "")
        # commands /cron and /at
        str_buffer = "  buffer: buffer where command is executed\n" \
            "          (\"current\" for current buffer, \"core.weechat\" for WeeChat core buffer)\n"
        str_commands = " command: a keyword, followed by arguments:\n"
        for cmd in sorted(cron_commands.keys()):
            str_commands += "          - " + cmd + ": " + cron_commands[cmd] + "\n";
        weechat.hook_command("cron",
                             "Manage jobs in crontab",
                             "list || add <minute> <hour> <monthday> <month> <weekday> <repeat> <buffer> <command> || "
                             "del <number>|-all || exec <number> || reload|save",
                             "    list: display jobs in crontab\n"
                             "     add: add a job in crontab\n"
                             "  minute: minute (0-59)\n"
                             "    hour: hour (0-23)\n"
                             "monthday: day of month (1-31)\n"
                             "   month: month (1-12)\n"
                             " weekday: day of week (0-6, or name: "
                             "sun (0), mon (1), tue (2), wed (3), thu (4), fri (5), sat (6))\n"
                             "  repeat: number of times job will be executed, must be >= 1 or special value * (repeat forever)\n"
                             + str_buffer + str_commands +
                             "     del: remove job(s) from crontab\n"
                             "  number: job number\n"
                             "    -all: remove all jobs\n"
                             "    exec: execute a command for a job (useful for testing command)\n"
                             "  reload: reload crontab file (automatic by default)\n"
                             "    save: save current crontab to file (automatic by default)\n\n"
                             "Format for time and date is similar to crontab, see man 5 crontab.\n\n"
                             "Examples:\n"
                             "  Display \"short drink!\" at 12:00 each day:\n"
                             "    /cron add 0 12 * * * * core.weechat print short drink!\n"
                             "  Same example with python code:\n"
                             "    /cron add 0 12 * * * * core.weechat python weechat.prnt(\"\", \"short drink!\")\n"
                             "  Set away status on all servers at 23:30:\n"
                             "    /cron add 30 23 * * * * core.weechat command /away -all I am sleeping\n"
                             "  Remove away status on all servers at 07:00:\n"
                             "    /cron add 0 7 * * * * core.weechat command /away -all I am sleeping\n"
                             "  Set away status on all servers at 10:00 every sunday:\n"
                             "    /cron add 0 10 * * sun * core.weechat command /away -all I am playing tennis\n"
                             "  Say \"hello\" on IRC channel #private at 08:00 from monday to friday:\n"
                             "    /cron add 0 8 * * mon-fri * irc.freenode.#private command hello\n"
                             "  Display \"wake up!\" at 06:00 next monday, only one time, with highlight:\n"
                             "    /cron add 0 6 * * mon 1 core.weechat print_hl wake up!\n"
                             "  Delete first entry in crontab:\n"
                             "    /cron del 1",
                             "list"
                             " || add %(cron_time) %(cron_time) %(cron_time) "
                             "%(cron_time) %(cron_time) %(cron_repeat) %(cron_buffer) "
                             "%(cron_keyword) %(cron_commands)"
                             " || del %(cron_number)|-all"
                             " || exec %(cron_number)"
                             " || reload"
                             " || save",
                             "cron_cmd_cb", "")
        weechat.hook_command("at",
                             "Queue job for later execution",
                             "list || <time> <buffer> <command>",
                             "    list: display jobs in crontab\n"
                             "    time: time for job, can be absolute (HH:MM) or relative "
                             "with \"+\" followed by value and optional unit\n"
                             "          (units: m=minutes (default), h=hours, d=days)\n"
                             + str_buffer + str_commands + "\n"
                             "To manage scheduled jobs, or use date and time to schedule a job, "
                             "use /cron command.\n\n"
                             "Examples:\n"
                             "  Display \"water the garden\" at 20:00:\n"
                             "    /at 20:00 core.weechat print water the garden\n"
                             "  Display \"phone to mom\" in 15 minutes:\n"
                             "    /at +15 core.weechat print phone to mom\n"
                             "  Display \"phone to dad\" in 2 hours:\n"
                             "    /at +2h core.weechat print phone to dad",
                             "list|%(at_time) %(cron_buffer) %(cron_keyword) %(cron_commands)",
                             "cron_at_cmd_cb", "")
        # load crontab
        cron_load()
        # timer
        weechat.hook_timer(60 * 1000, 60, 0, "cron_timer_cb", "")