-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathquick_simplenote.py
647 lines (554 loc) · 23.6 KB
/
quick_simplenote.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
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
import sublime, sublime_plugin
from simplenote import Simplenote
import functools
import time
import copy
from collections import deque
from os import path, makedirs, remove, listdir
from datetime import datetime
from threading import Semaphore, Lock
from operations import NoteCreator, MultipleNoteContentDownloader, GetNotesDelta, NoteDeleter, NoteUpdater
def cmp_to_key(mycmp):
'Convert a cmp= function into a key= function'
class K(object):
def __init__(self, obj, *args):
self.obj = obj
def __lt__(self, other):
return mycmp(self.obj, other.obj) < 0
def __gt__(self, other):
return mycmp(self.obj, other.obj) > 0
def __eq__(self, other):
return mycmp(self.obj, other.obj) == 0
def __le__(self, other):
return mycmp(self.obj, other.obj) <= 0
def __ge__(self, other):
return mycmp(self.obj, other.obj) >= 0
def __ne__(self, other):
return mycmp(self.obj, other.obj) != 0
return K
def sort_notes(a_note, b_note):
if 'pinned' in a_note['systemtags']:
return 1
elif 'pinned' in b_note['systemtags']:
return -1
else:
date_a = datetime.fromtimestamp(float(a_note['modifydate']))
date_b = datetime.fromtimestamp(float(b_note['modifydate']))
return cmp(date_a, date_b)
def show_message(message):
if not message:
message = ''
for window in sublime.windows():
for currentView in window.views():
currentView.set_status('QuickSimplenote', message)
def remove_status():
show_message(None)
def write_note_to_path(note, filepath):
f = open(filepath, 'wb')
try:
content = note['content']
f.write(content)
except KeyError:
pass
f.close()
def open_note(note, window=None):
if not window:
window = sublime.active_window()
filepath = get_path_for_note(note)
write_note_to_path(note, filepath)
return window.open_file(filepath)
def get_filename_for_note(note):
# Take out invalid characters from title and use that as base for the name
import string
valid_chars = "-_.() %s%s" % (string.ascii_letters, string.digits)
note_name = get_note_name(note)
base = ''.join(c for c in note_name if c in valid_chars)
# Determine extension based on title
extension_map = settings.get('title_extension_map')
extension = ''
if extension_map:
for item in extension_map:
import re
pattern = re.compile(item['title_regex'], re.UNICODE)
if re.search(pattern, note_name):
extension = '.' + item['extension']
break
return base + ' (' + note['key'] + ')' + extension
def get_path_for_note(note):
return path.join(temp_path, get_filename_for_note(note))
def get_note_from_path(view_filepath):
note = None
if view_filepath:
if path.dirname(view_filepath) == temp_path:
note_filename = path.split(view_filepath)[1]
note = [note for note in notes if get_filename_for_note(note) == note_filename]
if not note:
import re
pattern = re.compile(ur'\((.*?)\)')
results = re.findall(pattern, note_filename)
if results:
noteKey = results[ len(results) - 1]
note = [note for note in notes if note['key'] == noteKey]
if note:
note = note[0]
return note
def get_note_name(note):
try:
content = note['content']
except Exception, e:
return 'untitled'
index = content.find('\n');
if index > -1:
title = content[:index]
else:
if content:
title = content
else:
title = 'untitled'
title = title.decode('utf-8')
return title
def handle_open_filename_change(old_file_path, updated_note):
new_file_path = get_path_for_note(updated_note)
old_note_view = None
new_view = None
# If name changed
if old_file_path != new_file_path:
# Save the current active view because we might lose the focus
old_active_view = sublime.active_window().active_view()
# Search for the view of the open note
for view_list in [window.views() for window in sublime.windows()]:
for view in view_list:
if view.file_name() == old_file_path:
old_note_view = view
break
# If found
if old_note_view:
# Open the note in a new view
new_view = open_note(updated_note, old_note_view.window())
# Close the old dirty note
old_note_view_id = old_note_view.id()
old_active_view_id = old_active_view.id()
if old_note_view.window():
old_note_window_id = old_note_view.window().id()
else:
old_note_window_id = sublime.active_window() # Sometimes this happens on Sublime 2...
close_view(old_note_view)
# Focus on the new view or on the previous one depending
# on where we were
if old_note_view_id == old_active_view_id:
old_note_window = [window for window in sublime.windows() if window.id() == old_note_window_id]
if old_note_window:
old_note_window[0].focus_view(new_view)
else:
sublime.active_window().focus_view(old_active_view)
try:
remove(old_file_path)
except OSError as e:
pass
return True
return False
def close_view(view):
view.set_scratch(True)
view_window = view.window()
if not view_window:
view_window = sublime.active_window()
view_window.focus_view(view)
view_window.run_command("close_file")
def synch_note_resume(existing_note_entry, updated_note_resume):
for key in updated_note_resume:
existing_note_entry[key] = updated_note_resume[key]
def update_note(existing_note, updated_note):
synch_note_resume(existing_note, updated_note)
existing_note['local_modifydate'] = time.time()
existing_note['needs_update'] = False
existing_note['filename'] = get_filename_for_note(existing_note)
def load_notes():
notes = []
try:
with open(path.join(package_path, 'note_cache'),'rb') as cache_file:
notes = pickle.load(cache_file)
except (EOFError, IOError) as e:
pass
return notes
def save_notes(notes):
with open(path.join(package_path, 'note_cache'),'w+b') as cache_file:
pickle.dump(notes, cache_file)
class OperationManager:
_instance = None
_lock = Lock()
@classmethod
def instance(cls):
if not cls._instance:
with cls._lock:
if not cls._instance:
cls._instance = OperationManager()
return cls._instance
def __init__(self):
self.operations = deque([])
self.running = False
self.current_operation = None
def is_running(self):
return self.running
def add_operation(self, operation):
self.operations.append(operation)
if (not self.running):
self.run()
def check_operations(self):
if self.current_operation == None:
return
# If it's still running, update the status
if self.current_operation.is_alive():
text = self.current_operation.get_update_run_text()
else:
# If not running, show finished text
# call callback with result and do the
# next operation
text = self.current_operation.get_run_finished_text()
self.current_operation.join()
if len( self.operations ) > 0:
self.start_next_operation()
else:
self.running = False
sublime.set_timeout(remove_status, 1000)
show_message(text)
if self.running:
sublime.set_timeout(self.check_operations, 1000)
def run(self):
self.start_next_operation()
sublime.set_timeout(self.check_operations, 1000)
self.running = True
def start_next_operation(self):
self.current_operation = self.operations.popleft()
self.current_operation.start()
class HandleNoteViewCommand(sublime_plugin.EventListener):
waiting_to_save = []
def on_modified(self, view):
def flush_saves():
if OperationManager.instance().is_running():
sublime.set_timeout(flush_saves, 1000)
return
for entry in HandleNoteViewCommand.waiting_to_save:
if entry['note_key'] == note['key']:
with entry['lock']:
entry['count'] = entry['count'] - 1
if entry['count'] == 0:
view.run_command("save")
break
view_filepath = view.file_name()
note = get_note_from_path(view_filepath)
if note:
debounce_time = settings.get('autosave_debounce_time')
if not debounce_time:
return
debounce_time = debounce_time * 1000
found = False
for entry in HandleNoteViewCommand.waiting_to_save:
if entry['note_key'] == note['key']:
with entry['lock']:
entry['count'] = entry['count'] + 1
found = True
break
if not found:
new_entry = {}
new_entry['note_key'] = note['key']
new_entry['lock'] = Lock()
new_entry['count'] = 1
HandleNoteViewCommand.waiting_to_save.append(new_entry)
sublime.set_timeout(flush_saves, debounce_time)
def on_load(self, view):
view_filepath = view.file_name()
note = get_note_from_path(view_filepath)
syntax = settings.get('note_syntax')
if note and syntax:
view.set_syntax_file(syntax)
def get_current_content(self, view):
note_file_content = ""
try:
with open(view.file_name(),'r') as content_file:
note_file_content = content_file.read()
except (EOFError, IOError) as e:
pass
return note_file_content
def handle_note_changed(self, modified_note_resume, content, old_file_path, open_view):
global notes
# We get all the resume data back. We have to merge it
# with our data (extended fields and content)
for note in notes:
if note['key'] == modified_note_resume['key']:
# Set content to the updated one
# or to the view's content if we don't have any update
updated_from_server = False
if not 'content' in modified_note_resume:
modified_note_resume['content'] = content
else:
updated_from_server = True
update_note(note, modified_note_resume) # Update all fields
name_changed = handle_open_filename_change(old_file_path, note)
# If we didn't reopen the view with the name changed, but the content has changed
# we have to update the view anyway
if updated_from_server and not name_changed:
filepath = get_path_for_note(note)
write_note_to_path(note, filepath)
sublime.set_timeout(functools.partial(open_view.run_command, 'revert'), 0)
break
notes.sort(key=cmp_to_key(sort_notes), reverse=True)
save_notes(notes)
def on_post_save(self, view):
view_filepath = view.file_name()
note = get_note_from_path(view_filepath)
if note:
# Update with new content
updated_note = copy.deepcopy(note)
# Handle when the note changes elsewhere and the user goes to that tab:
# sublime reloads the view, it's handled as changed and sent here
if 'content' in updated_note and updated_note['content'] == self.get_current_content(view):
return
updated_note['content'] = self.get_current_content(view)
# Send update
update_op = NoteUpdater(note=updated_note, simplenote_instance=simplenote_instance)
update_op.set_callback(self.handle_note_changed,
{'content': updated_note['content'],
'old_file_path': view_filepath,
'open_view': view})
OperationManager.instance().add_operation(update_op)
class ShowQuickSimplenoteNotesCommand(sublime_plugin.ApplicationCommand):
def handle_selected(self, selected_index):
if not selected_index > -1:
return
selected_note = notes[selected_index]
open_note(selected_note)
def run(self):
if not started:
if not start():
return
i = 0
keys = []
for note in notes:
i += 1
title = get_note_name(note)
keys.append(title)
sublime.active_window().show_quick_panel(keys, self.handle_selected)
import pickle
class StartQuickSimplenoteSyncCommand(sublime_plugin.ApplicationCommand):
def set_result(self, new_notes):
global notes
notes = new_notes
notes.sort(key=cmp_to_key(sort_notes), reverse=True)
def merge_delta(self, updated_note_resume, existing_notes):
# Here we create the note_resume we use on the rest of the app.
# The note_resume we store consists of:
# The note resume as it comes from the simplenote api.
# The title, filename and last modified date of the local cache entry
# Look at the new resume and find existing entries
for current_updated_note_resume in updated_note_resume:
existing_note_entry = None
for existing_note in existing_notes:
if existing_note['key'] == current_updated_note_resume['key']:
existing_note_entry = existing_note
break
# If we have it already
if existing_note_entry:
# Mark for update if needed
try:
# Note with old content
if existing_note_entry['local_modifydate'] < float(current_updated_note_resume['modifydate']):
synch_note_resume(existing_note_entry, current_updated_note_resume)
existing_note_entry['needs_update'] = True
else:
# Up to date note
existing_note_entry['needs_update'] = False
except KeyError as e:
# Note that never got the content downloaded:
existing_note_entry['needs_update'] = True
# New note
else:
new_note_entry = {'needs_update': True}
synch_note_resume(new_note_entry, current_updated_note_resume)
existing_notes.append(new_note_entry)
# Look at the existing notes to find deletions
updated_note_resume_keys = [note['key'] for note in updated_note_resume]
deleted_notes = [deleted_note for deleted_note in existing_notes if deleted_note['key'] not in updated_note_resume_keys]
for deleted_note in deleted_notes:
existing_notes.remove(deleted_note)
save_notes(existing_notes)
self.notes_synch(existing_notes)
def notes_synch(self, notes):
# Here we synch updated notes in order of priority.
# Open notes:
# Locally unsaved
# Locally saved
# Other notes in order of modifydate and priority
open_files_dirty = []
open_files_ok = []
for view_list in [window.views() for window in sublime.windows()]:
for view in view_list:
if view.file_name() == None:
continue
if view.is_dirty():
open_files_dirty.append(path.split(view.file_name())[1])
else:
open_files_ok.append(path.split(view.file_name())[1])
# Classify notes
lu = []
ls = []
others = []
for note in notes:
if not note['needs_update']:
continue
try:
filename = note['filename']
except KeyError as e:
others.append(note)
continue
if filename in open_files_dirty:
lu.append(note)
elif filename in open_files_ok:
ls.append(note)
else:
others.append(note)
# Sorted by priority/importance
lu.sort(key=cmp_to_key(sort_notes), reverse=True)
ls.sort(key=cmp_to_key(sort_notes), reverse=True)
others.sort(key=cmp_to_key(sort_notes), reverse=True)
# Start updates
sem = Semaphore(3)
show_message('QuickSimplenote: Downloading content')
if lu:
down_op = MultipleNoteContentDownloader(sem, simplenote_instance=simplenote_instance, notes=lu)
down_op.set_callback(self.merge_open, {'existing_notes':notes, 'dirty':True})
OperationManager.instance().add_operation(down_op)
if ls:
down_op = MultipleNoteContentDownloader(sem, simplenote_instance=simplenote_instance, notes=ls)
down_op.set_callback(self.merge_open, {'existing_notes':notes})
OperationManager.instance().add_operation(down_op)
if others:
down_op = MultipleNoteContentDownloader(sem, simplenote_instance=simplenote_instance, notes=others)
down_op.set_callback(self.merge_notes, {'existing_notes':notes})
OperationManager.instance().add_operation(down_op)
def merge_open(self, updated_notes, existing_notes, dirty=False):
global settings
auto_overwrite_on_conflict = settings.get('on_conflict_use_server')
do_nothing_on_conflict = settings.get('on_conflict_leave_alone')
update = False
# If it's not a conflict or it's a conflict we can resolve
if ( not dirty ) or ( dirty and not do_nothing_on_conflict ):
# If we don't have an overwrite policy, ask the user
if ( not auto_overwrite_on_conflict ) and dirty and len( updated_notes ) > 0:
note_names = '\n'.join([get_note_name(updated_note) for updated_note in updated_notes])
update = sublime.ok_cancel_dialog('Note(s):\n%s\nAre in conflict. Overwrite?' % note_names, 'Overwrite')
if ( not dirty ) or update or auto_overwrite_on_conflict:
# Update notes if the change is clean, or we were asked to update
for note in existing_notes:
for updated_note in updated_notes:
# If we find the updated note
if note['key'] == updated_note['key']:
old_file_path = get_path_for_note(note)
new_file_path = get_path_for_note(updated_note)
# Update contents
write_note_to_path(updated_note, new_file_path)
# Handle filename change (note has the old filename value)
handle_open_filename_change(old_file_path, updated_note)
# Reload view of the note if it's selected
for view in [window.active_view() for window in sublime.windows()]:
if view.file_name() == new_file_path:
sublime.set_timeout(functools.partial(view.run_command, 'revert'), 0)
break
# Merge
self.merge_notes(updated_notes, existing_notes)
def merge_notes(self, updated_notes, existing_notes):
# Merge
for note in existing_notes:
if not note['needs_update']:
continue
for updated_note in updated_notes:
if note['key'] == updated_note['key']:
update_note(note, updated_note)
save_notes(existing_notes)
self.set_result(existing_notes)
def run(self):
show_message('QuickSimplenote: Synching')
get_delta_op = GetNotesDelta(simplenote_instance=simplenote_instance)
get_delta_op.set_callback(self.merge_delta, {'existing_notes':notes})
OperationManager.instance().add_operation(get_delta_op)
class CreateQuickSimplenoteNoteCommand(sublime_plugin.ApplicationCommand):
def handle_new_note(self, result):
if result:
global notes
update_note(result, result)
notes.append(result)
notes.sort(key=cmp_to_key(sort_notes), reverse=True)
save_notes(notes)
open_note(result)
def run(self):
creation_op = NoteCreator(simplenote_instance=simplenote_instance)
creation_op.set_callback(self.handle_new_note)
OperationManager.instance().add_operation(creation_op)
class DeleteQuickSimplenoteNoteCommand(sublime_plugin.ApplicationCommand):
def handle_deletion(self, result):
global notes
notes.remove(self.note)
save_notes(notes)
try:
remove(get_path_for_note(self.note))
except OSError as e:
pass
close_view(self.note_view)
def run(self):
self.note_view = sublime.active_window().active_view()
self.note = get_note_from_path(self.note_view.file_name())
if self.note:
deletion_op = NoteDeleter(note=self.note, simplenote_instance=simplenote_instance)
deletion_op.set_callback(self.handle_deletion)
OperationManager.instance().add_operation(deletion_op)
def sync():
if not OperationManager.instance().is_running():
print('QuickSimplenote: Syncing: %s' % time.time())
sublime.run_command('start_quick_simplenote_sync');
else:
print('QuickSimplenote: Sync ommited %s' % time.time())
sync_every = settings.get('sync_every')
if sync_every > 0:
sublime.set_timeout(sync, sync_every * 1000)
def start():
global started, simplenote_instance, settings
username = settings.get('username')
password = settings.get('password')
if (username and password):
simplenote_instance = Simplenote(username, password)
sync()
started = True
else:
filepath = path.join(package_path, 'quick_simplenote.sublime-settings')
sublime.active_window().open_file(filepath)
show_message('QuickSimplenote: Please configure username/password')
sublime.set_timeout(remove_status, 2000)
started = False
return started
def reload_if_needed():
global settings, started, reload_calls
# Sublime calls this twice for some reason :(
reload_calls = reload_calls + 1
if (reload_calls % 2 != 0):
return
if settings.get('autostart'):
sublime.set_timeout(start, 2000) # I know...
print('QuickSimplenote: Autostarting')
reload_calls = -1
simplenote_instance = None
started = False
notes = []
package_path = path.join(sublime.packages_path(), "QuickSimplenote")
temp_path = path.join(package_path, "temp")
notes = load_notes()
note_files = [note['filename'] for note in notes]
if not path.exists(temp_path):
makedirs(temp_path)
for f in listdir(temp_path):
if f not in note_files:
remove(path.join(temp_path, f))
settings = sublime.load_settings('quick_simplenote.sublime-settings')
settings.clear_on_change('username')
settings.clear_on_change('password')
settings.add_on_change('username', reload_if_needed)
settings.add_on_change('password', reload_if_needed)
reload_if_needed()