nncli

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

commit 84ab7c29061f8b67bed5d18a518ea5117e06dfdd
parent 99d614d8b700c4860f410ab0726a033ea6f3d99e
Author: Eric Davis <edavis@insanum.com>
Date:   Mon, 14 Jul 2014 22:14:17 -0700

new keybind command for viewing the previous/next version of a note
non-latest version note content views have their own color scheme
fixed some minor crashes dues to key errors

Diffstat:
Mconfig.py | 106+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
Mnotes_db.py | 4++++
Msncli.py | 55++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mutils.py | 13++++++++-----
Mview_note.py | 50++++++++++++++++++++++++++++++++++----------------
5 files changed, 160 insertions(+), 68 deletions(-)

diff --git a/config.py b/config.py @@ -46,6 +46,9 @@ def __init__(self): 'kb_tabstop2' : '2', 'kb_tabstop4' : '4', 'kb_tabstop8' : '8', + 'kb_prev_version' : '<', + 'kb_next_version' : '>', + 'kb_latest_version' : 'L', 'kb_search_gstyle' : '/', 'kb_search_regex' : 'meta /', 'kb_clear_search' : 'A', @@ -56,46 +59,50 @@ def __init__(self): 'kb_note_markdown' : 'm', 'kb_note_tags' : 't', - 'clr_default_fg' : 'default', - 'clr_default_bg' : 'default', - 'clr_status_bar_fg' : 'dark gray', - 'clr_status_bar_bg' : 'light gray', - 'clr_log_fg' : 'dark gray', - 'clr_log_bg' : 'light gray', - 'clr_search_bar_fg' : 'white', - 'clr_search_bar_bg' : 'light red', - 'clr_note_focus_fg' : 'white', - 'clr_note_focus_bg' : 'light red', - 'clr_note_title_day_fg' : 'light red', - 'clr_note_title_day_bg' : 'default', - 'clr_note_title_week_fg' : 'light green', - 'clr_note_title_week_bg' : 'default', - 'clr_note_title_month_fg' : 'brown', - 'clr_note_title_month_bg' : 'default', - 'clr_note_title_year_fg' : 'light blue', - 'clr_note_title_year_bg' : 'default', - 'clr_note_title_ancient_fg' : 'light blue', - 'clr_note_title_ancient_bg' : 'default', - 'clr_note_date_fg' : 'dark blue', - 'clr_note_date_bg' : 'default', - 'clr_note_flags_fg' : 'dark magenta', - 'clr_note_flags_bg' : 'default', - 'clr_note_tags_fg' : 'dark red', - 'clr_note_tags_bg' : 'default', - 'clr_note_content_fg' : 'default', - 'clr_note_content_bg' : 'default', - 'clr_note_content_focus_fg' : 'white', - 'clr_note_content_focus_bg' : 'light red', - 'clr_help_focus_fg' : 'white', - 'clr_help_focus_bg' : 'light red', - 'clr_help_header_fg' : 'dark blue', - 'clr_help_header_bg' : 'default', - 'clr_help_config_fg' : 'dark green', - 'clr_help_config_bg' : 'default', - 'clr_help_value_fg' : 'dark red', - 'clr_help_value_bg' : 'default', - 'clr_help_descr_fg' : 'default', - 'clr_help_descr_bg' : 'default' + 'clr_default_fg' : 'default', + 'clr_default_bg' : 'default', + 'clr_status_bar_fg' : 'dark gray', + 'clr_status_bar_bg' : 'light gray', + 'clr_log_fg' : 'dark gray', + 'clr_log_bg' : 'light gray', + 'clr_search_bar_fg' : 'white', + 'clr_search_bar_bg' : 'light red', + 'clr_note_focus_fg' : 'white', + 'clr_note_focus_bg' : 'light red', + 'clr_note_title_day_fg' : 'light red', + 'clr_note_title_day_bg' : 'default', + 'clr_note_title_week_fg' : 'light green', + 'clr_note_title_week_bg' : 'default', + 'clr_note_title_month_fg' : 'brown', + 'clr_note_title_month_bg' : 'default', + 'clr_note_title_year_fg' : 'light blue', + 'clr_note_title_year_bg' : 'default', + 'clr_note_title_ancient_fg' : 'light blue', + 'clr_note_title_ancient_bg' : 'default', + 'clr_note_date_fg' : 'dark blue', + 'clr_note_date_bg' : 'default', + 'clr_note_flags_fg' : 'dark magenta', + 'clr_note_flags_bg' : 'default', + 'clr_note_tags_fg' : 'dark red', + 'clr_note_tags_bg' : 'default', + 'clr_note_content_fg' : 'default', + 'clr_note_content_bg' : 'default', + 'clr_note_content_focus_fg' : 'white', + 'clr_note_content_focus_bg' : 'light red', + 'clr_note_content_old_fg' : 'yellow', + 'clr_note_content_old_bg' : 'dark gray', + 'clr_note_content_old_focus_fg' : 'white', + 'clr_note_content_old_focus_bg' : 'light red', + 'clr_help_focus_fg' : 'white', + 'clr_help_focus_bg' : 'light red', + 'clr_help_header_fg' : 'dark blue', + 'clr_help_header_bg' : 'default', + 'clr_help_config_fg' : 'dark green', + 'clr_help_config_bg' : 'default', + 'clr_help_value_fg' : 'dark red', + 'clr_help_value_bg' : 'default', + 'clr_help_descr_fg' : 'default', + 'clr_help_descr_bg' : 'default' } cp = ConfigParser.SafeConfigParser(defaults) @@ -134,7 +141,7 @@ def __init__(self): self.keybinds['page_down'] = [ cp.get(cfg_sec, 'kb_page_down'), [ 'common' ], 'Page down' ] self.keybinds['page_up'] = [ cp.get(cfg_sec, 'kb_page_up'), [ 'common' ], 'Page up' ] self.keybinds['half_page_down'] = [ cp.get(cfg_sec, 'kb_half_page_down'), [ 'common' ], 'Half page down' ] - self.keybinds['half_page_up'] = [ cp.get(cfg_sec, 'kb_half_page_up'), [ 'common' ], 'Half page up' ] + self.keybinds['half_page_up'] = [ cp.get(cfg_sec, 'kb_half_page_up'), [ 'common' ], 'Half page up' ] self.keybinds['bottom'] = [ cp.get(cfg_sec, 'kb_bottom'), [ 'common' ], 'Goto bottom' ] self.keybinds['top'] = [ cp.get(cfg_sec, 'kb_top'), [ 'common' ], 'Goto top' ] self.keybinds['status'] = [ cp.get(cfg_sec, 'kb_status'), [ 'common' ], 'Toggle status bar' ] @@ -144,11 +151,14 @@ def __init__(self): self.keybinds['view_note'] = [ cp.get(cfg_sec, 'kb_view_note'), [ 'titles' ], 'View note' ] self.keybinds['view_note_ext'] = [ cp.get(cfg_sec, 'kb_view_note_ext'), [ 'titles', 'notes' ], 'View note with pager' ] self.keybinds['pipe_note'] = [ cp.get(cfg_sec, 'kb_pipe_note'), [ 'titles', 'notes' ], 'Pipe note contents' ] - self.keybinds['view_next_note'] = [ cp.get(cfg_sec, 'kb_view_next_note'), [ 'notes' ], 'View next note' ] - self.keybinds['view_prev_note'] = [ cp.get(cfg_sec, 'kb_view_prev_note'), [ 'notes' ], 'View previous note' ] - self.keybinds['tabstop2'] = [ cp.get(cfg_sec, 'kb_tabstop2'), [ 'notes' ], 'View with tabstop=2' ] - self.keybinds['tabstop4'] = [ cp.get(cfg_sec, 'kb_tabstop4'), [ 'notes' ], 'View with tabstop=4' ] - self.keybinds['tabstop8'] = [ cp.get(cfg_sec, 'kb_tabstop8'), [ 'notes' ], 'View with tabstop=8' ] + self.keybinds['view_next_note'] = [ cp.get(cfg_sec, 'kb_view_next_note'), [ 'notes' ], 'View next note' ] + self.keybinds['view_prev_note'] = [ cp.get(cfg_sec, 'kb_view_prev_note'), [ 'notes' ], 'View previous note' ] + self.keybinds['tabstop2'] = [ cp.get(cfg_sec, 'kb_tabstop2'), [ 'notes' ], 'View with tabstop=2' ] + self.keybinds['tabstop4'] = [ cp.get(cfg_sec, 'kb_tabstop4'), [ 'notes' ], 'View with tabstop=4' ] + self.keybinds['tabstop8'] = [ cp.get(cfg_sec, 'kb_tabstop8'), [ 'notes' ], 'View with tabstop=8' ] + self.keybinds['prev_version'] = [ cp.get(cfg_sec, 'kb_prev_version'), [ 'notes' ], 'View previous version' ] + self.keybinds['next_version'] = [ cp.get(cfg_sec, 'kb_next_version'), [ 'notes' ], 'View next version' ] + self.keybinds['latest_version'] = [ cp.get(cfg_sec, 'kb_latest_version'), [ 'notes' ], 'View latest version' ] self.keybinds['search_gstyle'] = [ cp.get(cfg_sec, 'kb_search_gstyle'), [ 'titles' ], 'Search using gstyle' ] self.keybinds['search_regex'] = [ cp.get(cfg_sec, 'kb_search_regex'), [ 'titles' ], 'Search using regex' ] self.keybinds['clear_search'] = [ cp.get(cfg_sec, 'kb_clear_search'), [ 'titles' ], 'Show all notes' ] @@ -190,6 +200,10 @@ def __init__(self): self.colors['note_content_bg'] = [ cp.get(cfg_sec, 'clr_note_content_bg'), 'Note content bg' ] self.colors['note_content_focus_fg'] = [ cp.get(cfg_sec, 'clr_note_content_focus_fg'), 'Note content focus fg' ] self.colors['note_content_focus_bg'] = [ cp.get(cfg_sec, 'clr_note_content_focus_bg'), 'Note content focus bg' ] + self.colors['note_content_old_fg'] = [ cp.get(cfg_sec, 'clr_note_content_old_fg'), 'Old note content fg' ] + self.colors['note_content_old_bg'] = [ cp.get(cfg_sec, 'clr_note_content_old_bg'), 'Old note content bg' ] + self.colors['note_content_old_focus_fg'] = [ cp.get(cfg_sec, 'clr_note_content_old_focus_fg'), 'Old note content focus fg' ] + self.colors['note_content_old_focus_bg'] = [ cp.get(cfg_sec, 'clr_note_content_old_focus_bg'), 'Old note content focus bg' ] self.colors['help_focus_fg'] = [ cp.get(cfg_sec, 'clr_help_focus_fg'), 'Help focus fg' ] self.colors['help_focus_bg'] = [ cp.get(cfg_sec, 'clr_help_focus_bg'), 'Help focus bg' ] self.colors['help_header_fg'] = [ cp.get(cfg_sec, 'clr_help_header_fg'), 'Help header fg' ] diff --git a/notes_db.py b/notes_db.py @@ -565,6 +565,10 @@ def sync_notes(self, server_sync=True, full_sync=True): return sync_errors + def get_note_version(self, key, version): + gret = self.simplenote.get_note(key, version) + return gret[0] if gret[1] == 0 else None + def get_note_status(self, key): n = self.notes[key] o = utils.KeyValueObject(saved=False, synced=False, modified=False) diff --git a/sncli.py b/sncli.py @@ -205,7 +205,7 @@ def gui_update_view(self): self.view_titles.focus_note(cur_key) if self.gui_body_get().__class__ == view_note.ViewNote: - self.view_note.update_note(self.view_note.note['key']) + self.view_note.update_note(self.view_note.note) self.gui_update_status_bar() @@ -398,6 +398,53 @@ def gui_frame_keypress(self, size, key): self.view_titles.note_list[self.view_titles.focus_position].note['key']) self.gui_switch_frame_body(self.view_note) + elif key == self.config.get_keybind('latest_version'): + if self.gui_body_get().__class__ != view_note.ViewNote: + return key + + self.view_note.old_note_version = None + self.view_note.old_note = None + + lb.update_note() + + elif key == self.config.get_keybind('prev_version') or \ + key == self.config.get_keybind('next_version'): + if self.gui_body_get().__class__ != view_note.ViewNote: + return key + + diff = -1 if key == self.config.get_keybind('prev_version') \ + else 1 + limit = 0 if key == self.config.get_keybind('prev_version') \ + else self.view_note.note['version'] + 1 + + if not self.view_note.old_note_version: + next_note_version = self.view_note.note['version'] + diff + else: + next_note_version = self.view_note.old_note_version + diff + + if next_note_version == limit: + self.log(u'Version v{0} is unavailable (key={1})'. + format(next_note_version, self.view_note.key)) + return None + + self.log(u'Fetching version v{0} of note (key={1})'. + format(next_note_version, self.view_note.key)) + next_note = self.ndb.get_note_version(self.view_note.key, next_note_version) + + if not next_note: + self.log(u'Failed to get version v{0} of note (key={1})'. + format(next_note_version, self.view_note.key)) + return None + + if next_note_version != self.view_note.note['version']: + self.view_note.old_note_version = next_note_version + self.view_note.old_note = next_note + else: + self.view_note.old_note_version = None + self.view_note.old_note = None + + lb.update_note() + elif key == self.config.get_keybind('status'): if self.status_bar == 'yes': self.status_bar = 'no' @@ -717,6 +764,12 @@ def gui(self, key): ('note_content_focus', self.config.get_color('note_content_focus_fg'), self.config.get_color('note_content_focus_bg') ), + ('note_content_old', + self.config.get_color('note_content_old_fg'), + self.config.get_color('note_content_old_bg') ), + ('note_content_old_focus', + self.config.get_color('note_content_old_focus_fg'), + self.config.get_color('note_content_old_focus_bg') ), ('help_focus', self.config.get_color('help_focus_fg'), self.config.get_color('help_focus_bg') ), diff --git a/utils.py b/utils.py @@ -15,15 +15,18 @@ def generate_random_key(): return '%030x' % (random.randrange(256**15),) def get_note_tags(note): - tags = '%s' % ','.join(note['tags']) - if note['deleted']: - if tags: tags += u',trash' - else: tags = u'trash' + if 'tags' in note: + tags = '%s' % ','.join(note['tags']) + if 'deleted' in note and note['deleted']: + if tags: tags += u',trash' + else: tags = u'trash' + else: + tags = u'' return tags def get_note_flags(note): flags = '' - flags += u'T' if note['deleted'] else u' ' + flags += u'T' if 'deleted' in note and note['deleted'] else u' ' if 'systemtags' in note: flags += u'*' if 'pinned' in note['systemtags'] else u' ' flags += u'S' if 'published' in note['systemtags'] else u' ' diff --git a/view_note.py b/view_note.py @@ -8,6 +8,9 @@ def __init__(self, config, args): self.config = config self.ndb = args['ndb'] self.key = args['key'] + self.log = args['log'] + self.old_note_version = None + self.old_note = None self.note = self.ndb.get_note(self.key) if self.key else None self.tabstop = int(self.config.get_config('tabstop')) super(ViewNote, self).__init__( @@ -17,17 +20,25 @@ def get_note_content_as_list(self): lines = [] if not self.key: return lines - for l in self.note['content'].split('\n'): - lines.append( - urwid.AttrMap(urwid.Text(l.replace('\t', ' ' * self.tabstop)), - 'note_content', - 'note_content_focus')) + if self.old_note: + for l in self.old_note['content'].split('\n'): + lines.append( + urwid.AttrMap(urwid.Text(l.replace('\t', ' ' * self.tabstop)), + 'note_content_old', + 'note_content_old_focus')) + else: + for l in self.note['content'].split('\n'): + lines.append( + urwid.AttrMap(urwid.Text(l.replace('\t', ' ' * self.tabstop)), + 'note_content', + 'note_content_focus')) lines.append(urwid.AttrMap(urwid.Divider(u'-'), 'default')) return lines - def update_note(self, key): - self.key = key - self.note = self.ndb.get_note(self.key) if self.key else None + def update_note(self, key=None): + if key: + self.key = key + self.note = self.ndb.get_note(self.key) if self.key else None self.body[:] = \ urwid.SimpleFocusListWalker(self.get_note_content_as_list()) self.focus_position = 0 @@ -44,11 +55,18 @@ def get_status_bar(self): cur = self.focus_position total = len(self.body.positions()) - t = time.localtime(float(self.note['modifydate'])) + if self.old_note: + tnote = self.old_note + t = time.localtime(float(tnote['versiondate'])) + else: + tnote = self.note + t = time.localtime(float(tnote['modifydate'])) + mod_time = time.strftime(u'Date: %a, %d %b %Y %H:%M:%S', t) - title = utils.get_note_title(self.note) - flags = utils.get_note_flags(self.note) - tags = utils.get_note_tags(self.note) + title = utils.get_note_title(tnote) + flags = utils.get_note_flags(tnote) + tags = utils.get_note_tags(tnote) + version = tnote['version'] status_title = \ urwid.AttrMap(urwid.Text(u'Title: ' + @@ -57,7 +75,7 @@ def get_status_bar(self): 'status_bar') status_key_index = \ ('pack', urwid.AttrMap(urwid.Text(u' [' + - self.note['key'] + + self.key + u'] ' + str(cur + 1) + u'/' + @@ -71,7 +89,7 @@ def get_status_bar(self): ('pack', urwid.AttrMap(urwid.Text(u'[' + tags + u'] [v' + - str(self.note['version']) + + str(version) + u'] [' + flags + u']'), @@ -79,10 +97,10 @@ def get_status_bar(self): pile_top = urwid.Columns([ status_title, status_key_index ]) pile_bottom = urwid.Columns([ status_date, status_tags_flags ]) - if utils.note_published(self.note) and 'publishkey' in self.note: + if utils.note_published(tnote) and 'publishkey' in tnote: pile_publish = \ urwid.AttrMap(urwid.Text(u'Published: http://simp.ly/publish/' + - self.note['publishkey']), + tnote['publishkey']), 'status_bar') return \ urwid.AttrMap(urwid.Pile([ pile_top, pile_bottom, pile_publish ]),