nncli

NextCloud Notes Command Line Interface
git clone git://git.danielmoch.com/nncli.git
Log | Files | Refs | LICENSE

commit c00741e79ba159b6d3d3357d83aafad682c83f28
parent 9c75c96a0aa04c0af46d600bf3b180d68dec1d59
Author: Eric Davis <edavis@insanum.com>
Date:   Wed,  2 Jul 2014 14:05:46 -0700

first stab at getting note pin/unpin and modify tags working
simplenote servers suck... constant HTTP 400 errors

Diffstat:
Msncli.py | 211++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
Mview_titles.py | 90++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
2 files changed, 201 insertions(+), 100 deletions(-)

diff --git a/sncli.py b/sncli.py @@ -1,8 +1,9 @@ #!/usr/bin/env python2 -import os, sys, re, signal, time, datetime, logging, subprocess +import os, sys, re, signal, time, datetime, logging +import subprocess, thread, threading import copy, json, urwid, datetime, tempfile -import view_titles, view_note, view_help, view_log, search_notes +import view_titles, view_note, view_help, view_log, user_input import utils from config import Config from simplenote import Simplenote @@ -12,6 +13,7 @@ class sncli: def __init__(self, do_sync): + self.do_sync = do_sync self.config = Config() if not os.path.exists(self.config.get_config('db_path')): @@ -39,61 +41,67 @@ def __init__(self, do_sync): self.last_view = [] self.status_bar = self.config.get_config('status_bar') + self.status_message_alarm = None + self.status_message_lock = threading.Lock() + self.sync_notes_alarm = None + self.sync_notes_lock = threading.Lock() self.ndb.add_observer('synced:note', self.observer_notes_db_synced_note) self.ndb.add_observer('change:note-status', self.observer_notes_db_change_note_status) self.ndb.add_observer('progress:sync_full', self.observer_notes_db_sync_full) - if do_sync: - self.sync_full() - def sync_full(self): - try: - sync_from_server_errors = self.ndb.sync_full() - except Exception, e: - print e - exit(1) - else: - if sync_from_server_errors > 0: - print('Error syncing %d notes from server. Please check sncli.log for details.' % (sync_from_server_errors)) + thread.start_new_thread(self.ndb.sync_full, ()) + + def sync_full_initial(self, loop, arg): + self.sync_full() + + def sync_notes_cancel(self): + self.sync_notes_lock.acquire() + if self.sync_notes_alarm: + self.sncli_loop.remove_alarm(self.sync_notes_alarm) + self.sync_notes_alarm = None + self.sync_notes_lock.release() + + def sync_notes_timeout(self, loop, arg): + self.status_message_set('Starting sync...') + self.ndb.sync_to_server_threaded() + self.status_message_set('Sync complete.') + self.sync_notes_alarm = None + + def sync_notes_schedule(self): + self.sync_notes_lock.acquire() + self.sync_notes_cancel() + self.ndb.save_threaded() + self.sync_notes_alarm = \ + self.sncli_loop.set_alarm_at(time.time() + 4, + self.sync_notes_timeout, + None) + self.sync_notes_lock.release() def observer_notes_db_change_note_status(self, ndb, evt_type, evt): logging.debug(evt.msg) - print type(self) - print dir(self) self.status_message_set(evt.msg) def observer_notes_db_sync_full(self, ndb, evt_type, evt): logging.debug(evt.msg) - # XXX set status text someplace visible - print evt.msg + # XXX + #self.status_message_set(evt.msg) def observer_notes_db_synced_note(self, ndb, evt_type, evt): - """This observer gets called only when a note returns from - a sync that's more recent than our most recent mod to that note. - """ - - selected_note_o = self.notes_list_model.list[self.selected_note_idx] - print "observer_notes_db_synced_note: " + evt.msg - - # if the note synced back matches our currently selected note, - # we overwrite. - + logging.debug(evt.msg) + self.status_message_set(evt.msg) # XXX - #if selected_note_o.key == evt.lkey: - # if selected_note_o.note['content'] != evt.old_note['content']: - # self.view.mute_note_data_changes() - # # in this case, we want to keep the user's undo buffer so that they - # # can undo synced back changes if they would want to. - # self.view.set_note_data(selected_note_o.note, reset_undo=False) - # self.view.unmute_note_data_changes() + # update view if note synced back is the visible one def header_clear(self): self.master_frame.contents['header'] = ( None, None ) + #self.sncli_loop.draw_screen() def header_set(self, w): self.master_frame.contents['header'] = ( w, None ) + #self.sncli_loop.draw_screen() def header_get(self): return self.master_frame.contents['header'][0] @@ -103,9 +111,11 @@ def header_focus(self): def footer_clear(self): self.master_frame.contents['footer'] = ( None, None ) + #self.sncli_loop.draw_screen() def footer_set(self, w): self.master_frame.contents['footer'] = ( w, None ) + #self.sncli_loop.draw_screen() def footer_get(self): return self.master_frame.contents['footer'][0] @@ -115,9 +125,11 @@ def footer_focus(self): def body_clear(self): self.master_frame.contents['body'] = ( None, None ) + #self.sncli_loop.draw_screen() def body_set(self, w): self.master_frame.contents['body'] = ( w, None ) + #self.sncli_loop.draw_screen() def body_get(self): return self.master_frame.contents['body'][0] @@ -130,28 +142,41 @@ def status_message_timeout(self, loop, arg): self.status_message_alarm = None def status_message_cancel(self): + self.status_message_lock.acquire() + if self.status_message_alarm: self.sncli_loop.remove_alarm(self.status_message_alarm) - self.footer_clear() self.status_message_alarm = None + self.status_message_lock.release() + def status_message_set(self, msg): + self.status_message_lock.acquire() + + # XXX check if sncli_loop has started, if not stuff msg in a local + # if there is already a message showing then concatenate them existing_msg = '' if self.status_message_alarm and \ 'footer' in self.master_frame.contents.keys(): existing_msg = \ self.master_frame.contents['footer'][0].base_widget.text + u'\n' + # cancel any existing state message alarm - self.status_message_cancel() - self.footer_set(urwid.AttrMap(urwid.Text(existing_msg + msg, - wrap='clip'), + if self.status_message_alarm: + self.sncli_loop.remove_alarm(self.status_message_alarm) + self.status_message_alarm = None + + self.footer_set(urwid.AttrMap(urwid.Text(existing_msg + msg), 'status_message')) + self.status_message_alarm = \ self.sncli_loop.set_alarm_at(time.time() + 5, self.status_message_timeout, None) + self.status_message_lock.release() + def update_status_bar(self): if self.status_bar != 'yes': self.header_clear() @@ -161,6 +186,7 @@ def update_status_bar(self): def switch_frame_body(self, args): if args == None: if len(self.last_view) == 0: + self.ndb.sync_to_server_threaded(False) self.sncli_loop.widget = None raise urwid.ExitMainLoop() else: @@ -176,19 +202,41 @@ def search_quit(self): self.body_focus() self.master_frame.keypress = self.frame_keypress - def search_complete(self, search_string): - self.footer_clear() - self.body_focus() - self.master_frame.keypress = self.frame_keypress - self.body_set( - view_titles.ViewTitles(self.config, - { - 'ndb' : self.ndb, - 'search_string' : search_string, - 'body_changer' : self.switch_frame_body, - 'status_message' : self.status_message_set - })) - self.update_status_bar() + def search_input(self, search_string): + if search_string: + self.footer_clear() + self.body_focus() + self.master_frame.keypress = self.frame_keypress + self.body_set( + view_titles.ViewTitles(self.config, + { + 'ndb' : self.ndb, + 'search_string' : search_string, + 'body_changer' : self.switch_frame_body, + 'status_message' : self.status_message_set, + 'sync_func' : self.sync_notes_schedule + })) + self.update_status_bar() + else: + self.footer_clear() + self.body_focus() + self.master_frame.keypress = self.frame_keypress + + def tags_input(self, tags): + if tags != None: + self.footer_clear() + self.body_focus() + self.master_frame.keypress = self.frame_keypress + + lb = self.body_get() + self.ndb.set_note_tags(lb.all_notes[lb.focus_position].note['key'], tags) + lb.update_note_title(None, lb.focus_position) + self.update_status_bar() + self.sync_notes_schedule() + else: + self.footer_clear() + self.body_focus() + self.master_frame.keypress = self.frame_keypress def frame_keypress(self, size, key): @@ -291,17 +339,34 @@ def frame_keypress(self, size, key): self.status_bar = self.config.get_config('status_bar') elif key == self.config.get_keybind('search'): - # search when viewing the note list if self.body_get().__class__ == view_titles.ViewTitles: self.status_message_cancel() self.footer_set(urwid.AttrMap( - search_notes.SearchNotes(self.config, - key, self), + user_input.UserInput(self.config, + key, '', + self.search_input), 'search_bar')) self.footer_focus() self.master_frame.keypress = self.footer_get().keypress + elif key == 't': + # edit tags when viewing the note list + if self.body_get().__class__ == view_titles.ViewTitles: + self.status_message_cancel() + self.footer_set( + urwid.AttrMap( + user_input.UserInput(self.config, + 'Tags: ', + '%s' % ','.join(lb.all_notes[lb.focus_position].note['tags']), + self.tags_input), + 'search_bar')) + self.footer_focus() + self.master_frame.keypress = self.footer_get().keypress + + elif key == 'S': + self.sync_full() + elif key == self.config.get_keybind('clear_search'): self.body_set( view_titles.ViewTitles(self.config, @@ -309,13 +374,28 @@ def frame_keypress(self, size, key): 'ndb' : self.ndb, 'search_string' : None, 'body_changer' : self.switch_frame_body, - 'status_message' : self.status_message_set + 'status_message' : self.status_message_set, + 'sync_func' : self.sync_notes_schedule })) self.update_status_bar() else: - lb.keypress(size, key) + return lb.keypress(size, key) + + self.update_status_bar() + return None + def init_view(self, loop, arg): + self.master_frame.keypress = self.frame_keypress + self.body_set( + view_titles.ViewTitles(self.config, + { + 'ndb' : self.ndb, + 'search_string' : None, + 'body_changer' : self.switch_frame_body, + 'status_message' : self.status_message_set, + 'sync_func' : self.sync_notes_schedule + })) self.update_status_bar() def ba_bam_what(self): @@ -384,24 +464,21 @@ def ba_bam_what(self): self.config.get_color('help_descr_bg') ) ] - self.master_frame = urwid.Frame(body=None, + self.master_frame = urwid.Frame(body=urwid.Filler(urwid.Text(u'')), header=None, footer=None, focus_part='body') + self.sncli_loop = urwid.MainLoop(self.master_frame, palette, handle_mouse=False) - self.master_frame.keypress = self.frame_keypress - self.body_set( - view_titles.ViewTitles(self.config, - { - 'ndb' : self.ndb, - 'search_string' : None, - 'body_changer' : self.switch_frame_body, - 'status_message' : self.status_message_set - })) - self.update_status_bar() + #self.sncli_loop.set_alarm_in(0, self.init_view, None) + self.init_view(None, None) + + if self.do_sync: + # start full sync in one second after initial view is up + self.sncli_loop.set_alarm_in(1, self.sync_full_initial, None) self.sncli_loop.run() diff --git a/view_titles.py b/view_titles.py @@ -10,6 +10,7 @@ def __init__(self, config, args): self.search_string = args['search_string'] self.body_changer = args['body_changer'] self.status_message = args['status_message'] + self.sync_func = args['sync_func'] self.all_notes, match_regex, self.all_notes_cnt = \ self.ndb.filter_notes(self.search_string) if len(self.all_notes) == 0: @@ -121,21 +122,23 @@ def recursive_format(title_format): title_line = recursive_format(self.config.get_config('format_note_title')) return urwid.Columns(title_line) + def get_note_title(self, note): + return urwid.AttrMap(self.format_title(note), + 'default', + { 'default' : 'note_focus', + 'note_title_day' : 'note_focus', + 'note_title_week' : 'note_focus', + 'note_title_month' : 'note_focus', + 'note_title_year' : 'note_focus', + 'note_title_ancient' : 'note_focus', + 'note_date' : 'note_focus', + 'note_flags' : 'note_focus', + 'note_tags' : 'note_focus' }) + def get_note_titles(self): lines = [] for n in self.all_notes: - lines.append( - urwid.AttrMap(self.format_title(n.note), - 'default', - { 'default' : 'note_focus', - 'note_title_day' : 'note_focus', - 'note_title_week' : 'note_focus', - 'note_title_month' : 'note_focus', - 'note_title_year' : 'note_focus', - 'note_title_ancient' : 'note_focus', - 'note_date' : 'note_focus', - 'note_flags' : 'note_focus', - 'note_tags' : 'note_focus' })) + lines.append(self.get_note_title(n.note)) return lines def get_status_bar(self): @@ -163,36 +166,57 @@ def get_status_bar(self): urwid.AttrMap(urwid.Columns([ status_title, status_index ]), 'status_bar') + def update_note_title(self, key, pos): + if not key: + self.body[pos] = self.get_note_title(self.all_notes[pos].note) + else: + for i in xrange(len(self.all_notes)): + if self.all_notes[i].note['key'] == key: + self.body[i] = self.get_note_title(self.all_notes[i].note) + def keypress(self, size, key): if key == self.config.get_keybind('note_pin'): - self.ndb.set_note_pinned( - self.all_notes[self.focus_position].note['key'], 1) + if len(self.body.positions()) > 0: + self.ndb.set_note_pinned( + self.all_notes[self.focus_position].note['key'], 1) + self.update_note_title(None, self.focus_position) + self.sync_func() + return None elif key == self.config.get_keybind('note_unpin'): - self.ndb.set_note_pinned( - self.all_notes[self.focus_position].note['key'], 0) + if len(self.body.positions()) > 0: + self.ndb.set_note_pinned( + self.all_notes[self.focus_position].note['key'], 0) + self.update_note_title(None, self.focus_position) + self.sync_func() + return None elif key == self.config.get_keybind('view_note'): if len(self.body.positions()) > 0: self.body_changer({ 'view' : view_note.ViewNote, 'note' : self.all_notes[self.focus_position].note }) + return None elif key == self.config.get_keybind('view_note_ext'): - pager = None - if self.config.get_config('pager'): - pager = self.config.get_config('pager') - if not pager and os.environ['PAGER']: - pager = os.environ['PAGER'] - if not pager: - self.status_message(u'No pager configured!') - return - - tf = temp.tempfile_create(self.all_notes[self.focus_position].note) - try: - subprocess.check_call(pager + u' ' + temp.tempfile_name(tf), shell=True) - except Exception, e: - self.status_message(u'Pager error: ' + str(e)) - - # XXX check if modified, if so update it - temp.tempfile_delete(tf) + if len(self.body.positions()) > 0: + pager = None + if self.config.get_config('pager'): + pager = self.config.get_config('pager') + if not pager and os.environ['PAGER']: + pager = os.environ['PAGER'] + if not pager: + self.status_message(u'No pager configured!') + return None + + tf = temp.tempfile_create(self.all_notes[self.focus_position].note) + try: + subprocess.check_call(pager + u' ' + temp.tempfile_name(tf), shell=True) + except Exception, e: + self.status_message(u'Pager error: ' + str(e)) + + # XXX check if modified, if so update it + temp.tempfile_delete(tf) + return None + + return key