-
Notifications
You must be signed in to change notification settings - Fork 0
/
adgenda.py
398 lines (327 loc) · 13.6 KB
/
adgenda.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
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
#!/usr/bin/env python3
#Required module for the full-featured Linux-based calendar utility for blind users with Grade2 braille support
#Copyright Blazie Technologies LLC, All rights reserved
#by Stephen Blazie
import os, re, json, configparser
from BTSpeak import dialogs, host, script, braille
from datetime import datetime
dateFormats = [
"%B %d, %Y", #January 01, 2024
"%B %d %Y", # January 01 2024
"%b %-d, %Y", # Jan 1, 2024
"%b %-d %Y", # Jan 1 2024
"%B %d", # January 01
"%m-%d", # 01-01
"%b %d", # Jan 01
"%b %-d", # Jan 1
"%m/%d", # 1/01 or 1/1
"%m/%d/%y", # 1/1/24
"%m-%d", # 1-01 or 1-1
"%m-%d-%y", #01-01-2024
]
invalidDateMessage = "The date is invalid. " \
"Enter date in one of the following formats: " \
"1/1, 01/01, 1/1/24, 01-01, 01-01-24, Jan 01, " \
"Jan 1, January 01, January 01, 2024, or January 1, 2024"
class DateError(Exception):
pass
class calendar:
def __init__(self, invalidDateMessage = invalidDateMessage, calendarFile=os.path.join(host.stateDirectory, "calendar")):
self.calendarFile=calendarFile
self.load()
self.invalidDateMessage = invalidDateMessage
settings = Settings().getSettings()
self.contractedBraille = settings.getboolean('braille input')
if settings.getboolean('default date') == True:
self.defaultDatePrompt = todaysDate()
else:
self.defaultDatePrompt = None
def load(self):
try:
with open(self.calendarFile, 'r') as file:
self.calendar = json.load(file)
except FileNotFoundError:
self.calendar={}
def save(self):
with open(self.calendarFile, 'w') as file:
json.dump(self.calendar, file, indent=4)
def saveAsText(self, calendarTextPath):
with open(calendarTextPath, 'w') as calendarTextFile:
for date, entries in self.calendar.items():
for entry in entries:
time = entry['time']
event = entry['event']
calendarTextFile.write(f"{date}, {time}, {event}\n")
def parseUserEntry(self):
initialText = None
if self.defaultDatePrompt and self.contractedBraille:
initialText = translateInitial(self.defaultDatePrompt, self.contractedBraille)
elif self.defaultDatePrompt:
initialText = self.defaultDatePrompt
crudeDate = dialogs.requestInput("Date?", initialText = initialText)
if crudeDate is None:
exit(0)
else:
if self.contractedBraille:
crudeDate = translateBraille(crudeDate, backTranslate=True)
crudeDate, date = parseDate(crudeDate)
if date is None:
raise DateError("Date is None")
return
crudeTime = dialogs.requestInput("Time?")
event = dialogs.requestInput("Event?")
if self.contractedBraille:
crudeTime = translateBraille(crudeTime, backTranslate=True)
event = translateBraille(event, backTranslate=True)
time = parseTime(crudeTime)
reminder = parseReminder(event)
print(date, reminder, time, event)
return date, reminder, time, event
def appendEntry(self, entryString = None, fullEntry = False):
try:
if fullEntry:
date, reminder, time, event = parseFullEntry(entryString)
else:
date, reminder, time, event = self.parseUserEntry()
if time is None:
time = 'All day'
newEvent = {'event':event, 'time':time, 'reminder':reminder}
if date is None:
raise DateError("Date is None")
except DateError:
dialogs.showMessage(self.invalidDateMessage,
okayLabel = "Back"
)
return
if date in self.calendar:
if newEvent in self.calendar[date]:
dialogs.showMessage("Not added to calendar: That event already exists for the given date.",
okayLabel = "Back"
)
return
for existing_event in self.calendar[date]:
if existing_event['time'] == time and time is not None:
confirmationValue = dialogs.requestConfirmation(f"Another event already exists at {time} on {date}. Are you sure you would like to add the event?")
if not confirmationValue:
return
self.calendar.setdefault(date, []).append(newEvent)
self.save()
dialogs.showMessage(f"New event added to your calendar on {date}",
okayLabel = "Back"
)
return date
def removeEntry(self, date, time, eventString):
if date in self.calendar:
events = self.calendar[date]
for event in events:
if event['event'] == eventString:
request = dialogs.requestConfirmation(
f"Are you sure you want to remove {date} {eventString} from your calendar?",
yesLabel = True,
noLabel = True
)
if request == True:
self.calendar[date].remove(event)
if len(self.calendar[date])==0:
del self.calendar[date]
self.save()
dialogs.showMessage(f"Removed {eventString} on {date} from your calendar",
okayLabel = "Okay"
)
elif request == False:
return
def showEvents(self, dateList):
eventsDict={}
for date in dateList:
if date in self.calendar:
eventsDict[date]=self.calendar[date]
return eventsDict
def editDate(self, date, eventString, time, newDate):
if newDate == date:
return
crudeDate, newDate = parseDate(newDate)
if newDate is None:
dialogs.showMessage(self.invalidDateMessage,
okayLabel = "Back"
)
return
if date in self.calendar:
events = self.calendar[date]
for event in events:
if event['event'] == eventString:
events.remove(event)
if len(self.calendar[date])==0:
del self.calendar[date]
if newDate in self.calendar:
self.calendar[newDate].append(event)
else:
self.calendar[newDate] = [event]
dialogs.showMessage(f"Event date changed to {newDate}",
okayLabel = "okay"
)
self.save()
return
def editTime(self, date, eventString, time, newTime):
newTime=parseTime(newTime)
if newTime == time:
return
if newTime == None:
newTime = 'All day'
if date in self.calendar:
events = self.calendar[date]
for event in events:
if event['event'] == eventString and event['time'] == time:
event['time'] = newTime
dialogs.showMessage(f"Event time changed to {newTime}",
okayLabel = "okay"
)
self.save()
return
def editEvent(self, date, eventString, time, newEvent):
if newEvent == eventString or newEvent == None:
return
if date in self.calendar:
events = self.calendar[date]
for event in events:
if event['event'] == eventString and event['time'] == time:
event['event'] = newEvent
dialogs.showMessage(f"Event changed to {newEvent}",
okayLabel = "okay"
)
self.save()
return
def searchDate(self, date):
crudeDate, date = parseDate(date)
if date == None:
dialogs.showMessage(self.invalidDateMessage,
okayLabel = "Back"
)
return None
searchResultCalendar = {}
for key, value in self.calendar.items():
if date == key:
searchResultCalendar[key] = value
return searchResultCalendar
def searchEvent(self, event):
searchResultCalendar = {}
for key, value in self.calendar.items():
matchingEntries = []
for entry in value:
if event.lower() in entry['event'].lower():
matchingEntries.append(entry)
if matchingEntries:
searchResultCalendar[key] = matchingEntries
return searchResultCalendar
class Settings():
def __init__(self):
config = configparser.ConfigParser()
self.config = config
self.configFile = os.path.join(host.stateDirectory, "calendar.config")
self.getSettings()
def getSettings(self):
self.config.read(self.configFile)
return self.config['Settings']
def writeSettings(self, settingName, value):
self.config.set('Settings', settingName, str(value))
with open(self.configFile, 'w') as file:
self.config.write(file)
return
def listSettings(self):
settings = []
for setting in self.getSettings():
settings.append(setting)
return settings
def promptSettings(self, settingName):
defaultChoice = None
settings = ['Off', 'On']
if self.getSettings().getboolean(settingName):
choiceIndex = settings.index('On')
else:
choiceIndex = settings.index('Off')
currentSetting = settings[choiceIndex]
choiceIndex+=1
while True:
choice=dialogs.requestChoice(settings,
default=choiceIndex,
allowEmptyChoice=True,
okayLabel="Select",
cancelLabel="Back",
message= f"{settingName} is currently {currentSetting}"
)
if choice is None:
return
if choice.label == 'On':
value = 'True'
elif choice.label == 'Off':
value = 'False'
self.writeSettings(settingName, value)
choiceIndex = choice.key
currentSetting = choice.label
return
def saveCalendar(self):
now = datetime.now().strftime("%y%m%d")
calendarTextPath = os.path.join(os.getenv("HOME"), f"{now}-{script.scriptName}.txt")
try:
calendar().saveAsText(calendarTextPath)
dialogs.showMessage(f"Calendar saved as {now}-{script.scriptName}.txt to your home directory",
okayLabel = "okay"
)
except: pass
return
def translateInitial(text, contractedBraille):
if contractedBraille:
return translateBraille(text, backTranslate=False)
else:
return text
def translateBraille(text, backTranslate=False):
translation = braille.translateText (text, backTranslate=backTranslate, table=None)
return translation
def formatDate(date, dateFormats):
for fmt in dateFormats:
try:
dtDate = datetime.strptime(date, fmt)
if "%Y" not in fmt and "%y" not in fmt:
dtDate = dtDate.replace(year=datetime.now().year)
formattedDate = dtDate.strftime('%A, %B %d, %Y')
return formattedDate
except ValueError:
pass
return None
def parseDate(entry):
datePattern = r"(\w{3,9} \d{1,2}(?:,? \d{4})?|\d{1,2}[/-]\d{1,2}(?:[/-]\d{2,4})?|\d{1,2}/\d{1,2}(?:/\d{2,4})?|\w{3,9} \d{1,2})"
match=re.search(datePattern, entry, re.IGNORECASE)
if match:
crudeDate=match.group(0)
return crudeDate, formatDate(crudeDate.upper(), dateFormats)
else:
return "", None
def parseTime(entry):
timePattern=r'\b\d{1,2}(:\d{2})?\s*(?:am|pm)\b'
match=re.search(timePattern, entry, re.IGNORECASE)
if match:
return match.group(0)
else:
return None
def parseReminder(entry):
if entry.endswith("*"):
return True
else:
return False
def todaysDate():
return datetime.now().strftime("%A, %B %d, %Y")
def parseFullEntry(entryString):
if entryString.startswith(".cal"):
entryString = entryString[len(".cal"):].lstrip()
crudeDate, date = parseDate(entryString)
if date is None:
return
reminder = parseReminder(entryString)
time = parseTime(entryString)
event = entryString.replace(crudeDate, "")
if time is not None:
event = event.replace(time, "")
event = re.sub(r'^[^a-zA-Z0-9]+', '', event)
return date, reminder, time, event
if __name__=="__main__":
entryString = "March 27, 8pm Appointment"
calendar().appendEntry(entryString)