-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathics_to_todotxt.py
executable file
·188 lines (159 loc) · 5.75 KB
/
ics_to_todotxt.py
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
#!/usr/bin/env python3
"""
This script reads task data in iCalendar format and converts it to Gina
Trapani's todo.txt format (see http://todotxt.com/).
The task data can be exported from Remember The Milk or Apple's Reminders
app on OS X.
"""
import codecs
import re
import sys
import os
USAGE = """
Convert iCalendar task data to a todo.txt file.
See README.md for details.
Usage:
{0} INPUT_ICS OUTPUT_TXT [APPENDIX]
Where:
INPUT_ICS : Pathname of the source iCalendar file.
OUTPUT_TXT : Pathname for the output todo.txt file.
The file must not exist.
APPENDIX : Text to append to each output line.
Examples:
$ {0} Reminders.ics todo.txt.Reminders +ListName
$ {0} iCalendar_Service.ics todo.txt.RTM
"""
class ICSParser:
"""Quick and dirty iCalendar format parser."""
def __init__(self, input_file):
"""Initialize a new parser object and parse the input file."""
linebuf = None
self.__node = {}
self.__stack = []
for line in input_file:
line = line.rstrip('\r\n')
if linebuf is None:
linebuf = line
else:
if line[0] == ' ':
linebuf += line[1:]
else:
self.__parse_line(linebuf)
linebuf = line
if linebuf is not None:
self.__parse_line(linebuf)
def get_root(self):
"""Return the top-level VCALENDAR object."""
return self.__node['VCALENDAR'][0]
def __parse_line(self, line):
"""Parse an input line; update internal structures."""
if line.startswith('BEGIN:'):
self.__stack.append(self.__node)
node_name = line[6:]
new_node = {}
if node_name in self.__node:
self.__node[node_name].append(new_node)
else:
self.__node[node_name] = [new_node]
self.__node = new_node
elif line.startswith('END:'):
self.__node = self.__stack.pop()
else:
key, val = tuple(line.split(':', 1))
key = key.split(';', 1)[0]
if key not in self.__node:
self.__node[key] = val
def normalize_date(date):
"""Bring the date argument to a uniform format, which is YYYY-MM-DD."""
# Remove the time portion.
time_pos = date.find('T')
if time_pos >= 0:
date = date[:time_pos]
# Insert dashes.
if len(date) == 8:
date = date[:4] + '-' + date[4:6] + '-' + date[6:]
return date
def camel_case(words):
"""Convert a noun phrase to a CamelCase identifier."""
result = ''
for word in re.split(r'(?:\W|_)+', words):
if word:
if word[:1].islower():
word = word.capitalize()
result += word
return result
def unescape(string):
"""Remove backslashes from the string."""
return re.sub(r'\\(.)', r'\1', string)
def process_description(description):
"""Extract tags, location, and notes from the DESCRIPTION component."""
result = ''
sep = ' '
next_sep = '; '
for field in description.split(r'\n'):
if field.startswith('Time estimate:') or \
field.startswith('Updated:') or not field:
continue
elif field.startswith('Tags:'):
tags = field[5:].strip()
if tags != 'none':
for tag in unescape(tags).split(','):
result += ' @' + camel_case(tag)
elif field.startswith('Location:'):
location = field[9:].strip()
if location != 'none':
result += ' @' + camel_case(location)
elif re.match('^--+$', field):
next_sep = ': '
else:
result += sep + unescape(field)
sep = next_sep
next_sep = '; '
return result
def main(argv):
"""Convert input_file (iCalendar_Service.ics) to output_file (todo.txt)."""
if len(argv) == 4:
appendix = ' ' + argv[3]
elif len(argv) == 3:
appendix = ''
else:
print(USAGE.format(argv[0]), file=sys.stderr)
return 1
input_file_name = argv[1]
output_file_name = argv[2]
if os.path.exists(output_file_name):
print("Error: '" + output_file_name + "' exists.", file=sys.stderr)
return 1
try:
todos = ICSParser(codecs.open(input_file_name,
'r', 'utf-8')).get_root()['VTODO']
output_file = codecs.open(output_file_name, 'w', 'utf-8')
for todo in todos:
output_line = ''
if 'STATUS' in todo and todo['STATUS'] == 'COMPLETED':
output_line = 'x '
if 'PRIORITY' in todo:
output_line += '(' + \
chr(((ord(todo['PRIORITY']) - 49) >> 2) + 65) + ') '
dates = set()
for field in 'DTSTAMP', 'DTSTART', 'LAST-MODIFIED', 'COMPLETED':
if field in todo:
dates.add(normalize_date(todo[field]))
for date in sorted(dates, reverse=True):
output_line += date + ' '
output_line += unescape(todo['SUMMARY'])
if 'URL' in todo:
output_line += ' ' + todo['URL']
if 'DUE' in todo:
output_line += ' due:' + normalize_date(todo['DUE'])
if 'DESCRIPTION' in todo:
output_line += process_description(todo['DESCRIPTION'])
print(output_line + appendix, file=output_file)
except IOError as err:
print(err, file=sys.stderr)
return 2
print('Conversion completed. Please carefully review the contents of')
print(output_file_name + ' before merging it into your existing todo.txt.')
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))