calendar: add recurring events, event management, and safety rules

Add --rrule flag to send with DTSTART/BYDAY validation (RFC 5545).
Add event list (via khal) and event delete (safe single-event removal).
Document safety rules from 2026-03-25 incident: always dry-run recurring
events, never rm .ics files, space out SMTP sends.
This commit is contained in:
Yanxin Lu
2026-03-25 09:32:22 -07:00
parent 810a9923f9
commit 35f048dd83
6 changed files with 347 additions and 19 deletions

View File

@@ -73,6 +73,7 @@ _这份文件记录持续性项目和重要状态跨会话保留。_
### 3. 日历邀请 + CalDAV 同步 ### 3. 日历邀请 + CalDAV 同步
**状态**: 运行中 **状态**: 运行中
**创建**: 2026-03-18 **创建**: 2026-03-18
**更新**: 2026-03-25添加 RRULE 支持 + 事件管理 + 安全规则)
**配置**: **配置**:
- 技能: `~/.openclaw/workspace/skills/calendar/` - 技能: `~/.openclaw/workspace/skills/calendar/`
- 日历数据: `~/.openclaw/workspace/calendars/` (home/work/tasks/journals) - 日历数据: `~/.openclaw/workspace/calendars/` (home/work/tasks/journals)
@@ -82,12 +83,19 @@ _这份文件记录持续性项目和重要状态跨会话保留。_
- 运行方式: `uv run`(依赖 `icalendar` 库) - 运行方式: `uv run`(依赖 `icalendar` 库)
**功能**: **功能**:
- 发送日历邀请给外部收件人 - 发送日历邀请,支持周期性事件(`--rrule`
- 接受/拒绝/暂定回复邀请(回复给 organizer - 接受/拒绝/暂定回复邀请(回复给 organizer
- VTODO 待办管理add/list/complete/delete/check - 事件管理(`event list` / `event delete`
- VTODO 待办管理add/list/edit/complete/delete/check
- 发送/回复/待办操作后自动 `vdirsyncer sync` 同步到 CalDAV - 发送/回复/待办操作后自动 `vdirsyncer sync` 同步到 CalDAV
- 心跳定期同步日历 - 心跳定期同步日历
**⚠️ 重要安全规则**2026-03-25 事故教训):
- 周期性邀请**必须先 `--dry-run`** 验证 ICS 内容
- `--rrule``BYDAY` 必须和 `--start` 日期的星期匹配(工具会自动校验)
- **绝不用 `rm` 删日历 .ics 文件**,只用 `event delete --match`
- 连续发多封邮件时,每封间隔 10 秒以上Migadu SMTP 限频)
--- ---
## 📁 项目文件索引 ## 📁 项目文件索引
@@ -100,4 +108,4 @@ _这份文件记录持续性项目和重要状态跨会话保留。_
--- ---
_最后更新: 2026-03-24_ _最后更新: 2026-03-25_

View File

@@ -118,12 +118,20 @@ $SKILL_DIR/scripts/calendar.sh send \
--subject "Lunch" --summary "Lunch at Tartine" \ --subject "Lunch" --summary "Lunch at Tartine" \
--start "2026-03-20T12:00:00" --end "2026-03-20T13:00:00" --start "2026-03-20T12:00:00" --end "2026-03-20T13:00:00"
# 接受邀请(从邮件中提取 .ics # 发送周期性邀请(--start 必须落在 BYDAY 指定的那天!
$SKILL_DIR/scripts/calendar.sh send \
--to "alice@example.com" \
--subject "Weekly Shot" --summary "Allergy Shot (Tue)" \
--start "2026-03-31T14:30:00" --end "2026-03-31T15:00:00" \
--rrule "FREQ=WEEKLY;COUNT=13;BYDAY=TU"
# 接受/拒绝邀请
$SKILL_DIR/scripts/calendar.sh reply --envelope-id 42 --action accept $SKILL_DIR/scripts/calendar.sh reply --envelope-id 42 --action accept
# 拒绝邀请(附带留言 # 事件管理(查看、搜索、删除
$SKILL_DIR/scripts/calendar.sh reply --envelope-id 42 --action decline \ $SKILL_DIR/scripts/calendar.sh event list
--comment "Sorry, I have a conflict." $SKILL_DIR/scripts/calendar.sh event list --search "Allergy"
$SKILL_DIR/scripts/calendar.sh event delete --match "Allergy Shot (Tue)"
# 待办管理 # 待办管理
$SKILL_DIR/scripts/calendar.sh todo add --summary "跟进报销" --due "2026-03-25" --priority high $SKILL_DIR/scripts/calendar.sh todo add --summary "跟进报销" --due "2026-03-25" --priority high
@@ -132,16 +140,18 @@ $SKILL_DIR/scripts/calendar.sh todo edit --match "报销" --due "2026-03-28"
$SKILL_DIR/scripts/calendar.sh todo complete --match "报销" $SKILL_DIR/scripts/calendar.sh todo complete --match "报销"
$SKILL_DIR/scripts/calendar.sh todo delete --match "报销" $SKILL_DIR/scripts/calendar.sh todo delete --match "报销"
$SKILL_DIR/scripts/calendar.sh todo check # 每日摘要cron $SKILL_DIR/scripts/calendar.sh todo check # 每日摘要cron
# 查看日历(检查冲突)
khal list today 7d
``` ```
**支持操作**: 发送邀请 (`send`)、接受/拒绝/暂定 (`reply`)、待办管理 (`todo add/list/edit/complete/delete/check`) **支持操作**: 发送邀请 (`send`)、接受/拒绝/暂定 (`reply`)、事件管理 (`event list/delete`)、待办管理 (`todo add/list/edit/complete/delete/check`)
**依赖**: himalaya邮件、vdirsyncerCalDAV 同步、khal查看日历、todoman待办管理 **依赖**: himalaya邮件、vdirsyncerCalDAV 同步、khal查看日历、todoman待办管理
**同步**: 发送/回复/待办变更后自动 `vdirsyncer sync`,心跳也会定期同步 **同步**: 发送/回复/待办变更后自动 `vdirsyncer sync`,心跳也会定期同步
**注意**: 发送日历邀请属于对外邮件,需先确认 **注意**: 发送日历邀请属于对外邮件,需先确认
**安全规则**:
- 周期性邀请务必先 `--dry-run` 验证 ICS 内容
- **绝不用 `rm` 删日历 .ics 文件**,只用 `event delete`
- 连续发多封邮件时,每封间隔 10 秒以上Migadu SMTP 限频)
### OpenClaw Cron 定时任务 ### OpenClaw Cron 定时任务
**规则**: 确定性 shell 任务用 `systemEvent`,需要 LLM 判断的用 `agentTurn` **规则**: 确定性 shell 任务用 `systemEvent`,需要 LLM 判断的用 `agentTurn`

View File

@@ -33,12 +33,16 @@ All commands go through the wrapper script:
```bash ```bash
SKILL_DIR=~/.openclaw/workspace/skills/calendar SKILL_DIR=~/.openclaw/workspace/skills/calendar
# Send an invite # Send an invite (supports recurring events via --rrule)
$SKILL_DIR/scripts/calendar.sh send [options] $SKILL_DIR/scripts/calendar.sh send [options]
# Reply to an invite # Reply to an invite
$SKILL_DIR/scripts/calendar.sh reply [options] $SKILL_DIR/scripts/calendar.sh reply [options]
# Manage events
$SKILL_DIR/scripts/calendar.sh event list [options]
$SKILL_DIR/scripts/calendar.sh event delete [options]
# Manage todos # Manage todos
$SKILL_DIR/scripts/calendar.sh todo add [options] $SKILL_DIR/scripts/calendar.sh todo add [options]
$SKILL_DIR/scripts/calendar.sh todo list [options] $SKILL_DIR/scripts/calendar.sh todo list [options]
@@ -76,6 +80,7 @@ $SKILL_DIR/scripts/calendar.sh send \
| `--location` | No | Event location | | `--location` | No | Event location |
| `--description` | No | Event description / notes | | `--description` | No | Event description / notes |
| `--organizer` | No | Organizer display name (defaults to `--from`) | | `--organizer` | No | Organizer display name (defaults to `--from`) |
| `--rrule` | No | Recurrence rule (e.g. `FREQ=WEEKLY;COUNT=13;BYDAY=TU`) |
| `--uid` | No | Custom event UID (auto-generated if omitted) | | `--uid` | No | Custom event UID (auto-generated if omitted) |
| `--account` | No | Himalaya account name (if not default) | | `--account` | No | Himalaya account name (if not default) |
| `--dry-run` | No | Print ICS + MIME without sending | | `--dry-run` | No | Print ICS + MIME without sending |
@@ -101,7 +106,17 @@ $SKILL_DIR/scripts/calendar.sh send \
--location "Zoom - https://zoom.us/j/123456" \ --location "Zoom - https://zoom.us/j/123456" \
--description "Weekly check-in. Agenda: updates, blockers, action items." --description "Weekly check-in. Agenda: updates, blockers, action items."
# Dry run # Recurring: every Tuesday for 13 weeks (--start MUST fall on a Tuesday)
$SKILL_DIR/scripts/calendar.sh send \
--to "alice@example.com" \
--subject "Allergy Shot (Tue)" \
--summary "Allergy Shot (Tue)" \
--start "2026-03-31T14:30:00" \
--end "2026-03-31T15:00:00" \
--location "11965 Venice Blvd. #300, LA" \
--rrule "FREQ=WEEKLY;COUNT=13;BYDAY=TU"
# Dry run (always use for recurring events to verify)
$SKILL_DIR/scripts/calendar.sh send \ $SKILL_DIR/scripts/calendar.sh send \
--to "test@example.com" \ --to "test@example.com" \
--subject "Test" \ --subject "Test" \
@@ -111,6 +126,52 @@ $SKILL_DIR/scripts/calendar.sh send \
--dry-run --dry-run
``` ```
### Recurring Events (--rrule)
The `--rrule` flag accepts an RFC 5545 RRULE string. Common patterns:
| Pattern | RRULE |
|---------|-------|
| Weekly on Tue, 13 weeks | `FREQ=WEEKLY;COUNT=13;BYDAY=TU` |
| Weekly on Mon/Wed/Fri, until date | `FREQ=WEEKLY;UNTIL=20260630T000000Z;BYDAY=MO,WE,FR` |
| Every 2 weeks on Thu | `FREQ=WEEKLY;INTERVAL=2;BYDAY=TH` |
| Monthly on the 15th, 6 times | `FREQ=MONTHLY;COUNT=6;BYMONTHDAY=15` |
| Daily for 5 days | `FREQ=DAILY;COUNT=5` |
**Critical rule**: For `FREQ=WEEKLY` with a single `BYDAY`, the `--start` date **must fall on that day of the week**. The tool validates this and will reject mismatches. RFC 5545 says mismatched DTSTART/BYDAY produces undefined behavior.
**Best practice**: Always `--dry-run` first for recurring events to verify the generated ICS.
---
## Managing Events
```bash
# List upcoming events (next 90 days)
$SKILL_DIR/scripts/calendar.sh event list
# Search events by text
$SKILL_DIR/scripts/calendar.sh event list --search "Allergy"
# List with UIDs (for deletion)
$SKILL_DIR/scripts/calendar.sh event list --format "{uid} {title}"
# Custom date range
$SKILL_DIR/scripts/calendar.sh event list --range-start "2026-04-01" --range-end "2026-04-30"
# Delete by UID
$SKILL_DIR/scripts/calendar.sh event delete --uid "abc123@openclaw"
# Delete by summary match
$SKILL_DIR/scripts/calendar.sh event delete --match "Allergy Shot (Tue)"
```
### Event Delete Safety
- Deletes only ONE event at a time. If multiple events match, it lists them and exits.
- **NEVER use `rm` on calendar .ics files directly.** Always use `event delete`.
- After deleting, verify with `event list` or `khal list`.
--- ---
## Replying to Invites ## Replying to Invites

View File

@@ -293,9 +293,98 @@ $SKILL_DIR/scripts/calendar.sh todo check
# Should produce no output # Should produce no output
``` ```
## 14. Regression: Existing Invite Commands ## 14. Dry Run: Recurring Event (--rrule)
Verify the rename didn't break VEVENT flow. Test recurring event generation. Use a date that falls on a Tuesday.
```bash
# Find next Tuesday
NEXT_TUE=$(python3 -c "from datetime import date,timedelta; d=date.today(); d+=timedelta((1-d.weekday())%7 or 7); print(d)")
$SKILL_DIR/scripts/calendar.sh send \
--to "mail@luyx.org" \
--subject "Recurring Test (Tue)" \
--summary "Recurring Test (Tue)" \
--start "${NEXT_TUE}T14:30:00" \
--end "${NEXT_TUE}T15:00:00" \
--rrule "FREQ=WEEKLY;COUNT=4;BYDAY=TU" \
--dry-run
```
**Verify:**
- [ ] ICS has `RRULE:FREQ=WEEKLY;BYDAY=TU;COUNT=4`
- [ ] DTSTART falls on a Tuesday
- [ ] No validation errors
## 15. Validation: DTSTART/BYDAY Mismatch
Verify the tool rejects mismatched DTSTART and BYDAY.
```bash
# This should FAIL — start is on a Tuesday but BYDAY=TH
$SKILL_DIR/scripts/calendar.sh send \
--to "mail@luyx.org" \
--subject "Mismatch Test" \
--summary "Mismatch Test" \
--start "${NEXT_TUE}T09:00:00" \
--end "${NEXT_TUE}T09:30:00" \
--rrule "FREQ=WEEKLY;COUNT=4;BYDAY=TH" \
--dry-run
```
**Verify:**
- [ ] Script exits with error
- [ ] Error message says DTSTART falls on TU but RRULE says BYDAY=TH
- [ ] Suggests changing --start to a date that falls on TH
## 16. Event List
```bash
# List upcoming events
$SKILL_DIR/scripts/calendar.sh event list
# Search by text
$SKILL_DIR/scripts/calendar.sh event list --search "Calendar Skill Test"
# List with UIDs
$SKILL_DIR/scripts/calendar.sh event list --format "{uid} {title}"
```
**Verify:**
- [ ] Events from earlier tests appear
- [ ] Search narrows results correctly
- [ ] UIDs are displayed with --format
## 17. Event Delete
```bash
# Send a throwaway event first
$SKILL_DIR/scripts/calendar.sh send \
--to "mail@luyx.org" \
--subject "Delete Me Event" \
--summary "Delete Me Event" \
--start "${TEST_DATE}T20:00:00" \
--end "${TEST_DATE}T21:00:00"
# Verify it exists
$SKILL_DIR/scripts/calendar.sh event list --search "Delete Me"
# Delete it
$SKILL_DIR/scripts/calendar.sh event delete --match "Delete Me Event"
# Verify it's gone
$SKILL_DIR/scripts/calendar.sh event list --search "Delete Me"
```
**Verify:**
- [ ] Event is created and visible
- [ ] Delete removes exactly one event
- [ ] Other events are untouched
- [ ] `vdirsyncer sync` ran after delete
## 18. Regression: Existing Invite Commands
Verify new features didn't break VEVENT flow.
```bash ```bash
$SKILL_DIR/scripts/calendar.sh send \ $SKILL_DIR/scripts/calendar.sh send \
@@ -309,7 +398,8 @@ $SKILL_DIR/scripts/calendar.sh send \
**Verify:** **Verify:**
- [ ] ICS has `BEGIN:VEVENT`, `METHOD:REQUEST` - [ ] ICS has `BEGIN:VEVENT`, `METHOD:REQUEST`
- [ ] No errors from the renamed script - [ ] No RRULE present (single event)
- [ ] No errors
--- ---
@@ -347,3 +437,7 @@ todo list
| `todo` command not found | Install with `uv tool install todoman` | | `todo` command not found | Install with `uv tool install todoman` |
| `todo list` errors | Check `~/.config/todoman/config.py` exists and `path` points to tasks dir | | `todo list` errors | Check `~/.config/todoman/config.py` exists and `path` points to tasks dir |
| Todo not syncing | Check `~/.openclaw/workspace/calendars/tasks/` exists, verify vdirsyncer `cal/tasks` pair | | Todo not syncing | Check `~/.openclaw/workspace/calendars/tasks/` exists, verify vdirsyncer `cal/tasks` pair |
| DTSTART/BYDAY mismatch error | `--start` date doesn't fall on the BYDAY day. Change the start date to match |
| Recurring events on wrong day | DTSTART was not aligned with BYDAY. Delete the event and resend with correct `--start` |
| SMTP rate limit / EOF error | Too many sends too fast. Wait 10+ seconds between sends (Migadu limit) |
| Events disappeared after cleanup | **Never use `rm *.ics`** on calendar dirs. Use `event delete --match` instead |

View File

@@ -6,8 +6,10 @@ Uses the icalendar library for proper RFC 5545 ICS generation and parsing.
Uses himalaya CLI for email delivery. Syncs to local CalDAV via vdirsyncer. Uses himalaya CLI for email delivery. Syncs to local CalDAV via vdirsyncer.
Subcommands: Subcommands:
python calendar.py send [options] # create and send an invite python calendar.py send [options] # create and send an invite (supports --rrule)
python calendar.py reply [options] # accept/decline/tentative python calendar.py reply [options] # accept/decline/tentative
python calendar.py event list [options] # list/search calendar events
python calendar.py event delete [options] # delete an event by UID or summary
python calendar.py todo add [options] # create a VTODO task python calendar.py todo add [options] # create a VTODO task
python calendar.py todo list [options] # list pending tasks python calendar.py todo list [options] # list pending tasks
python calendar.py todo edit [options] # edit a task's fields python calendar.py todo edit [options] # edit a task's fields
@@ -27,7 +29,7 @@ from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText from email.mime.text import MIMEText
from icalendar import Alarm, Calendar, Event, Todo, vCalAddress, vText from icalendar import Alarm, Calendar, Event, Todo, vCalAddress, vRecur, vText
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Config # Config
@@ -111,6 +113,54 @@ def _parse_date(date_str):
return datetime.strptime(date_str, "%Y-%m-%d").date() return datetime.strptime(date_str, "%Y-%m-%d").date()
# iCalendar day abbreviation -> Python weekday (0=Mon, 6=Sun)
_ICAL_DAY_TO_WEEKDAY = {
"MO": 0, "TU": 1, "WE": 2, "TH": 3, "FR": 4, "SA": 5, "SU": 6,
}
def _validate_rrule_dtstart(rrule_dict, dtstart):
"""Validate that DTSTART day-of-week matches BYDAY when FREQ=WEEKLY.
RFC 5545: 'The DTSTART property value SHOULD be synchronized with the
recurrence rule. The recurrence set generated with a DTSTART property
value not synchronized with the recurrence rule is undefined.'
"""
freq = rrule_dict.get("FREQ")
if not freq:
return
# Normalize: icalendar may return freq as list or string
if isinstance(freq, list):
freq = freq[0]
if str(freq).upper() != "WEEKLY":
return
byday = rrule_dict.get("BYDAY")
if not byday:
return
if not isinstance(byday, list):
byday = [byday]
# Only validate alignment when there's a single BYDAY value
if len(byday) != 1:
return
day_str = str(byday[0]).upper()
expected_weekday = _ICAL_DAY_TO_WEEKDAY.get(day_str)
if expected_weekday is None:
return
if dtstart.weekday() != expected_weekday:
actual_day = [k for k, v in _ICAL_DAY_TO_WEEKDAY.items() if v == dtstart.weekday()][0]
print(
f"Error: DTSTART falls on {actual_day} but RRULE says BYDAY={day_str}. "
f"RFC 5545 requires these to match for FREQ=WEEKLY. "
f"Change --start to a date that falls on {day_str}.",
file=sys.stderr,
)
sys.exit(1)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Send invite # Send invite
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -154,6 +204,12 @@ def cmd_send(args):
"RSVP": "TRUE", "RSVP": "TRUE",
}) })
# Recurrence rule
if args.rrule:
rrule = vRecur.from_ical(args.rrule)
_validate_rrule_dtstart(rrule, start)
event.add("rrule", rrule)
# 1-day reminder # 1-day reminder
alarm = Alarm() alarm = Alarm()
alarm.add("action", "DISPLAY") alarm.add("action", "DISPLAY")
@@ -683,6 +739,81 @@ def cmd_todo_check(args):
print(output) print(output)
# ---------------------------------------------------------------------------
# Event management
# ---------------------------------------------------------------------------
def cmd_event_list(args):
"""List calendar events via khal."""
if args.search:
cmd = ["khal", "search"]
if args.format:
cmd += ["--format", args.format]
cmd.append(args.search)
else:
cmd = ["khal", "list"]
if args.format:
cmd += ["--format", args.format]
cmd += [args.range_start, args.range_end]
try:
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
print(result.stdout.rstrip())
except subprocess.CalledProcessError as e:
print(f"Error: khal failed: {e.stderr.strip()}", file=sys.stderr)
sys.exit(1)
except FileNotFoundError:
print("Error: khal is not installed", file=sys.stderr)
sys.exit(1)
def cmd_event_delete(args):
"""Delete a calendar event by UID or summary match."""
if not args.uid and not args.match:
print("Error: --uid or --match is required", file=sys.stderr)
sys.exit(1)
if not CALENDAR_DIR.is_dir():
print(f"Error: Calendar directory not found: {CALENDAR_DIR}", file=sys.stderr)
sys.exit(1)
ics_files = list(CALENDAR_DIR.glob("*.ics"))
if not ics_files:
print("No events found in calendar.", file=sys.stderr)
sys.exit(1)
# Find matching event(s)
matches = []
for ics_path in ics_files:
try:
cal = Calendar.from_ical(ics_path.read_bytes())
except Exception:
continue
for component in cal.walk():
if component.name == "VEVENT":
uid = str(component.get("uid", ""))
summary = str(component.get("summary", ""))
if args.uid and args.uid in uid:
matches.append((ics_path, uid, summary))
elif args.match and args.match in summary:
matches.append((ics_path, uid, summary))
if not matches:
target = args.uid or args.match
print(f"Error: No event matching '{target}'", file=sys.stderr)
sys.exit(1)
if len(matches) > 1:
print(f"Error: Multiple events match:", file=sys.stderr)
for _, uid, summary in matches:
print(f" - {summary} (uid: {uid})", file=sys.stderr)
sys.exit(1)
ics_path, uid, summary = matches[0]
ics_path.unlink()
print(f"Deleted event: {summary} (uid: {uid})")
_sync_calendar()
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# CLI # CLI
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -703,6 +834,7 @@ def main():
send_p.add_argument("--location", default="", help="Event location") send_p.add_argument("--location", default="", help="Event location")
send_p.add_argument("--description", default="", help="Event description") send_p.add_argument("--description", default="", help="Event description")
send_p.add_argument("--organizer", default="", help="Organizer display name") send_p.add_argument("--organizer", default="", help="Organizer display name")
send_p.add_argument("--rrule", default="", help="RRULE string (e.g. FREQ=WEEKLY;COUNT=13;BYDAY=TU)")
send_p.add_argument("--uid", default="", help="Custom event UID") send_p.add_argument("--uid", default="", help="Custom event UID")
send_p.add_argument("--account", default="", help="Himalaya account") send_p.add_argument("--account", default="", help="Himalaya account")
send_p.add_argument("--dry-run", action="store_true", help="Preview without sending") send_p.add_argument("--dry-run", action="store_true", help="Preview without sending")
@@ -718,6 +850,22 @@ def main():
reply_p.add_argument("--comment", default="", help="Message to include in reply") reply_p.add_argument("--comment", default="", help="Message to include in reply")
reply_p.add_argument("--dry-run", action="store_true", help="Preview without sending") reply_p.add_argument("--dry-run", action="store_true", help="Preview without sending")
# --- event ---
event_p = subparsers.add_parser("event", help="Manage calendar events")
event_sub = event_p.add_subparsers(dest="event_command", required=True)
# event list
elist_p = event_sub.add_parser("list", help="List events (via khal)")
elist_p.add_argument("--search", default="", help="Search events by text")
elist_p.add_argument("--range-start", default="today", help="Start of range (default: today)")
elist_p.add_argument("--range-end", default="90d", help="End of range (default: 90d)")
elist_p.add_argument("--format", default="", help="khal format string (e.g. '{uid} {title}')")
# event delete
edel_p = event_sub.add_parser("delete", help="Delete an event")
edel_p.add_argument("--uid", default="", help="Event UID")
edel_p.add_argument("--match", default="", help="Match on summary text")
# --- todo --- # --- todo ---
todo_p = subparsers.add_parser("todo", help="Manage VTODO tasks") todo_p = subparsers.add_parser("todo", help="Manage VTODO tasks")
todo_sub = todo_p.add_subparsers(dest="todo_command", required=True) todo_sub = todo_p.add_subparsers(dest="todo_command", required=True)
@@ -761,6 +909,11 @@ def main():
cmd_send(args) cmd_send(args)
elif args.command == "reply": elif args.command == "reply":
cmd_reply(args) cmd_reply(args)
elif args.command == "event":
if args.event_command == "list":
cmd_event_list(args)
elif args.event_command == "delete":
cmd_event_delete(args)
elif args.command == "todo": elif args.command == "todo":
if args.todo_command == "add": if args.todo_command == "add":
cmd_todo_add(args) cmd_todo_add(args)

View File

@@ -2,8 +2,10 @@
# calendar — wrapper script for the calendar and todo tool. # calendar — wrapper script for the calendar and todo tool.
# #
# Usage: # Usage:
# ./calendar.sh send [options] # send a calendar invite # ./calendar.sh send [options] # send a calendar invite (supports --rrule)
# ./calendar.sh reply [options] # accept/decline/tentative # ./calendar.sh reply [options] # accept/decline/tentative
# ./calendar.sh event list [options] # list/search calendar events
# ./calendar.sh event delete [options] # delete an event by UID or summary
# ./calendar.sh todo add [options] # create a todo # ./calendar.sh todo add [options] # create a todo
# ./calendar.sh todo list [options] # list pending todos # ./calendar.sh todo list [options] # list pending todos
# ./calendar.sh todo edit [options] # edit a todo's fields # ./calendar.sh todo edit [options] # edit a todo's fields