nncli

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

commit e166ededa96667ad3c3d29caba46513ea39897f7
parent 5d6a475ac960644046a235ec63bd6f29b905a5a2
Author: Eric Davis <edavis@insanum.com>
Date:   Mon, 28 Jul 2014 22:31:24 -0700

added a setup.py file for easy installation

Diffstat:
Dconfig.py | 248-------------------------------------------------------------------------------
Dnotes_db.py | 613-------------------------------------------------------------------------------
Asetup.py | 20++++++++++++++++++++
Dsimplenote.py | 339-------------------------------------------------------------------------------
Asimplenote_cli/__init__.py | 8++++++++
Asimplenote_cli/config.py | 251+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asimplenote_cli/notes_db.py | 617+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asimplenote_cli/simplenote.py | 343+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asimplenote_cli/sncli.py | 1198+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asimplenote_cli/temp.py | 40++++++++++++++++++++++++++++++++++++++++
Asimplenote_cli/user_input.py | 26++++++++++++++++++++++++++
Asimplenote_cli/utils.py | 172+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asimplenote_cli/view_help.py | 128+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asimplenote_cli/view_log.py | 50++++++++++++++++++++++++++++++++++++++++++++++++++
Asimplenote_cli/view_note.py | 172+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asimplenote_cli/view_titles.py | 188+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msncli | 29++++++++++++++++++++++++++++-
Dsncli.py | 1195-------------------------------------------------------------------------------
Dtemp.py | 37-------------------------------------
Duser_input.py | 23-----------------------
Dutils.py | 168-------------------------------------------------------------------------------
Dview_help.py | 125-------------------------------------------------------------------------------
Dview_log.py | 47-----------------------------------------------
Dview_note.py | 169-------------------------------------------------------------------------------
Dview_titles.py | 185-------------------------------------------------------------------------------
25 files changed, 3241 insertions(+), 3150 deletions(-)

diff --git a/config.py b/config.py @@ -1,248 +0,0 @@ - -import os, urwid, collections, ConfigParser - -class Config: - - def __init__(self): - self.home = os.path.abspath(os.path.expanduser('~')) - defaults = \ - { - 'cfg_sn_username' : '', - 'cfg_sn_password' : '', - 'cfg_db_path' : os.path.join(self.home, '.sncli'), - 'cfg_search_tags' : 'yes', # with regex searches - 'cfg_sort_mode' : 'date', # 'alpha' or 'date' - 'cfg_pinned_ontop' : 'yes', - 'cfg_tabstop' : '4', - 'cfg_format_strftime' : '%Y/%m/%d', - 'cfg_format_note_title' : '[%D] %F %-N %T', - 'cfg_status_bar' : 'yes', - 'cfg_editor' : 'vim', - 'cfg_pager' : 'less -c', - 'cfg_diff' : 'diff -b -U10', - 'cfg_max_logs' : '5', - 'cfg_log_timeout' : '5', - 'cfg_log_reversed' : 'yes', - - 'kb_help' : 'h', - 'kb_quit' : 'q', - 'kb_sync' : 'S', - 'kb_down' : 'j', - 'kb_up' : 'k', - 'kb_page_down' : ' ', - 'kb_page_up' : 'b', - 'kb_half_page_down' : 'ctrl d', - 'kb_half_page_up' : 'ctrl u', - 'kb_bottom' : 'G', - 'kb_top' : 'g', - 'kb_status' : 's', - 'kb_create_note' : 'C', - 'kb_edit_note' : 'e', - 'kb_view_note' : 'enter', - 'kb_view_note_ext' : 'meta enter', - 'kb_view_note_json' : 'O', - 'kb_pipe_note' : '|', - 'kb_view_next_note' : 'J', - 'kb_view_prev_note' : 'K', - 'kb_view_log' : 'l', - 'kb_tabstop2' : '2', - 'kb_tabstop4' : '4', - 'kb_tabstop8' : '8', - 'kb_prev_version' : '<', - 'kb_next_version' : '>', - 'kb_diff_version' : 'D', - 'kb_restore_version' : 'R', - 'kb_latest_version' : 'L', - 'kb_select_version' : '#', - 'kb_search_gstyle' : '/', - 'kb_search_regex' : 'meta /', - 'kb_clear_search' : 'A', - 'kb_sort_date' : 'd', - 'kb_sort_alpha' : 'a', - 'kb_note_trash' : 'T', - 'kb_note_pin' : 'p', - '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_user_input_bar_fg' : 'white', - 'clr_user_input_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) - self.configs_read = cp.read([os.path.join(self.home, '.snclirc')]) - - cfg_sec = 'sncli' - - if not cp.has_section(cfg_sec): - cp.add_section(cfg_sec) - - # ordered dicts used to ease help - - self.configs = collections.OrderedDict() - self.configs['sn_username'] = [ cp.get(cfg_sec, 'cfg_sn_username', raw=True), 'Simplenote Username' ] - self.configs['sn_password'] = [ cp.get(cfg_sec, 'cfg_sn_password', raw=True), 'Simplenote Password' ] - self.configs['db_path'] = [ cp.get(cfg_sec, 'cfg_db_path'), 'Note storage path' ] - self.configs['search_tags'] = [ cp.get(cfg_sec, 'cfg_search_tags'), 'Search tags as well' ] - self.configs['sort_mode'] = [ cp.get(cfg_sec, 'cfg_sort_mode'), 'Sort mode' ] - self.configs['pinned_ontop'] = [ cp.get(cfg_sec, 'cfg_pinned_ontop'), 'Pinned at top of list' ] - self.configs['tabstop'] = [ cp.get(cfg_sec, 'cfg_tabstop'), 'Tabstop spaces' ] - self.configs['format_strftime'] = [ cp.get(cfg_sec, 'cfg_format_strftime', raw=True), 'Date strftime format' ] - self.configs['format_note_title'] = [ cp.get(cfg_sec, 'cfg_format_note_title', raw=True), 'Note title format' ] - self.configs['status_bar'] = [ cp.get(cfg_sec, 'cfg_status_bar'), 'Show the status bar' ] - self.configs['editor'] = [ cp.get(cfg_sec, 'cfg_editor'), 'Editor command' ] - self.configs['pager'] = [ cp.get(cfg_sec, 'cfg_pager'), 'External pager command' ] - self.configs['diff'] = [ cp.get(cfg_sec, 'cfg_diff'), 'External diff command' ] - self.configs['max_logs'] = [ cp.get(cfg_sec, 'cfg_max_logs'), 'Max logs in footer' ] - self.configs['log_timeout'] = [ cp.get(cfg_sec, 'cfg_log_timeout'), 'Log timeout' ] - self.configs['log_reversed'] = [ cp.get(cfg_sec, 'cfg_log_reversed'), 'Log file reversed' ] - - self.keybinds = collections.OrderedDict() - self.keybinds['help'] = [ cp.get(cfg_sec, 'kb_help'), [ 'common' ], 'Help' ] - self.keybinds['quit'] = [ cp.get(cfg_sec, 'kb_quit'), [ 'common' ], 'Quit' ] - self.keybinds['sync'] = [ cp.get(cfg_sec, 'kb_sync'), [ 'common' ], 'Full sync' ] - self.keybinds['down'] = [ cp.get(cfg_sec, 'kb_down'), [ 'common' ], 'Scroll down one line' ] - self.keybinds['up'] = [ cp.get(cfg_sec, 'kb_up'), [ 'common' ], 'Scroll up one line' ] - 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['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' ] - self.keybinds['view_log'] = [ cp.get(cfg_sec, 'kb_view_log'), [ 'common' ], 'View log' ] - self.keybinds['create_note'] = [ cp.get(cfg_sec, 'kb_create_note'), [ 'titles' ], 'Create a new note' ] - self.keybinds['edit_note'] = [ cp.get(cfg_sec, 'kb_edit_note'), [ 'titles', 'notes' ], 'Edit note' ] - 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['view_note_json'] = [ cp.get(cfg_sec, 'kb_view_note_json'), [ 'titles', 'notes' ], 'View note raw json' ] - 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['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['diff_version'] = [ cp.get(cfg_sec, 'kb_diff_version'), [ 'notes' ], 'Diff version of note' ] - self.keybinds['restore_version'] = [ cp.get(cfg_sec, 'kb_restore_version'), [ 'notes' ], 'Restore version of note' ] - self.keybinds['latest_version'] = [ cp.get(cfg_sec, 'kb_latest_version'), [ 'notes' ], 'View latest version' ] - self.keybinds['select_version'] = [ cp.get(cfg_sec, 'kb_select_version'), [ 'notes' ], 'Select 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' ] - self.keybinds['sort_date'] = [ cp.get(cfg_sec, 'kb_sort_date'), [ 'titles' ], 'Sort notes by date' ] - self.keybinds['sort_alpha'] = [ cp.get(cfg_sec, 'kb_sort_alpha'), [ 'titles' ], 'Sort notes by alpha' ] - self.keybinds['note_trash'] = [ cp.get(cfg_sec, 'kb_note_trash'), [ 'titles', 'notes' ], 'Trash a note' ] - self.keybinds['note_pin'] = [ cp.get(cfg_sec, 'kb_note_pin'), [ 'titles', 'notes' ], 'Pin note' ] - self.keybinds['note_markdown'] = [ cp.get(cfg_sec, 'kb_note_markdown'), [ 'titles', 'notes' ], 'Flag note as markdown' ] - self.keybinds['note_tags'] = [ cp.get(cfg_sec, 'kb_note_tags'), [ 'titles', 'notes' ], 'Edit note tags' ] - - self.colors = collections.OrderedDict() - self.colors['default_fg'] = [ cp.get(cfg_sec, 'clr_default_fg'), 'Default fg' ] - self.colors['default_bg'] = [ cp.get(cfg_sec, 'clr_default_bg'), 'Default bg' ] - self.colors['status_bar_fg'] = [ cp.get(cfg_sec, 'clr_status_bar_fg'), 'Status bar fg' ] - self.colors['status_bar_bg'] = [ cp.get(cfg_sec, 'clr_status_bar_bg'), 'Status bar bg' ] - self.colors['log_fg'] = [ cp.get(cfg_sec, 'clr_log_fg'), 'Log message fg' ] - self.colors['log_bg'] = [ cp.get(cfg_sec, 'clr_log_bg'), 'Log message bg' ] - self.colors['user_input_bar_fg'] = [ cp.get(cfg_sec, 'clr_user_input_bar_fg'), 'User input bar fg' ] - self.colors['user_input_bar_bg'] = [ cp.get(cfg_sec, 'clr_user_input_bar_bg'), 'User input bar bg' ] - self.colors['note_focus_fg'] = [ cp.get(cfg_sec, 'clr_note_focus_fg'), 'Note title focus fg' ] - self.colors['note_focus_bg'] = [ cp.get(cfg_sec, 'clr_note_focus_bg'), 'Note title focus bg' ] - self.colors['note_title_day_fg'] = [ cp.get(cfg_sec, 'clr_note_title_day_fg'), 'Day old note title fg' ] - self.colors['note_title_day_bg'] = [ cp.get(cfg_sec, 'clr_note_title_day_bg'), 'Day old note title bg' ] - self.colors['note_title_week_fg'] = [ cp.get(cfg_sec, 'clr_note_title_week_fg'), 'Week old note title fg' ] - self.colors['note_title_week_bg'] = [ cp.get(cfg_sec, 'clr_note_title_week_bg'), 'Week old note title bg' ] - self.colors['note_title_month_fg'] = [ cp.get(cfg_sec, 'clr_note_title_month_fg'), 'Month old note title fg' ] - self.colors['note_title_month_bg'] = [ cp.get(cfg_sec, 'clr_note_title_month_bg'), 'Month old note title bg' ] - self.colors['note_title_year_fg'] = [ cp.get(cfg_sec, 'clr_note_title_year_fg'), 'Year old note title fg' ] - self.colors['note_title_year_bg'] = [ cp.get(cfg_sec, 'clr_note_title_year_bg'), 'Year old note title bg' ] - self.colors['note_title_ancient_fg'] = [ cp.get(cfg_sec, 'clr_note_title_ancient_fg'), 'Ancient note title fg' ] - self.colors['note_title_ancient_bg'] = [ cp.get(cfg_sec, 'clr_note_title_ancient_bg'), 'Ancient note title bg' ] - self.colors['note_date_fg'] = [ cp.get(cfg_sec, 'clr_note_date_fg'), 'Note date fg' ] - self.colors['note_date_bg'] = [ cp.get(cfg_sec, 'clr_note_date_bg'), 'Note date bg' ] - self.colors['note_flags_fg'] = [ cp.get(cfg_sec, 'clr_note_flags_fg'), 'Note flags fg' ] - self.colors['note_flags_bg'] = [ cp.get(cfg_sec, 'clr_note_flags_bg'), 'Note flags bg' ] - self.colors['note_tags_fg'] = [ cp.get(cfg_sec, 'clr_note_tags_fg'), 'Note tags fg' ] - self.colors['note_tags_bg'] = [ cp.get(cfg_sec, 'clr_note_tags_bg'), 'Note tags bg' ] - self.colors['note_content_fg'] = [ cp.get(cfg_sec, 'clr_note_content_fg'), 'Note content fg' ] - 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' ] - self.colors['help_header_bg'] = [ cp.get(cfg_sec, 'clr_help_header_bg'), 'Help header bg' ] - self.colors['help_config_fg'] = [ cp.get(cfg_sec, 'clr_help_config_fg'), 'Help config fg' ] - self.colors['help_config_bg'] = [ cp.get(cfg_sec, 'clr_help_config_bg'), 'Help config bg' ] - self.colors['help_value_fg'] = [ cp.get(cfg_sec, 'clr_help_value_fg'), 'Help value fg' ] - self.colors['help_value_bg'] = [ cp.get(cfg_sec, 'clr_help_value_bg'), 'Help value bg' ] - self.colors['help_descr_fg'] = [ cp.get(cfg_sec, 'clr_help_descr_fg'), 'Help description fg' ] - self.colors['help_descr_bg'] = [ cp.get(cfg_sec, 'clr_help_descr_bg'), 'Help description bg' ] - - def get_config(self, name): - return self.configs[name][0] - - def get_config_descr(self, name): - return self.configs[name][1] - - def get_keybind(self, name): - return self.keybinds[name][0] - - def get_keybind_use(self, name): - return self.keybinds[name][1] - - def get_keybind_descr(self, name): - return self.keybinds[name][2] - - def get_color(self, name): - return self.colors[name][0] - - def get_color_descr(self, name): - return self.colors[name][1] - diff --git a/notes_db.py b/notes_db.py @@ -1,613 +0,0 @@ -# nvPY: cross-platform note-taking app with simplenote syncing -# copyright 2012 by Charl P. Botha <cpbotha@vxlabs.com> -# new BSD license - -import os, time, re, glob, json, copy, threading -import utils -import simplenote -simplenote.NOTE_FETCH_LENGTH=100 -from simplenote import Simplenote - -class ReadError(RuntimeError): - pass - -class WriteError(RuntimeError): - pass - -class NotesDB(): - """NotesDB will take care of the local notes database and syncing with SN. - """ - def __init__(self, config, log, update_view): - self.config = config - self.log = log - self.update_view = update_view - - self.last_sync = 0 # set to zero to trigger a full sync - self.sync_lock = threading.Lock() - - # create db dir if it does not exist - if not os.path.exists(self.config.get_config('db_path')): - os.mkdir(self.config.get_config('db_path')) - - now = time.time() - # now read all .json files from disk - fnlist = glob.glob(self.helper_key_to_fname('*')) - - self.notes = {} - - for fn in fnlist: - try: - n = json.load(open(fn, 'rb')) - except IOError, e: - raise ReadError ('Error opening {0}: {1}'.format(fn, str(e))) - except ValueError, e: - raise ReadError ('Error reading {0}: {1}'.format(fn, str(e))) - else: - # we always have a localkey, also when we don't have a note['key'] yet (no sync) - localkey = os.path.splitext(os.path.basename(fn))[0] - self.notes[localkey] = n - # we maintain in memory a timestamp of the last save - # these notes have just been read, so at this moment - # they're in sync with the disc. - n['savedate'] = now - - # initialise the simplenote instance we're going to use - # this does not yet need network access - self.simplenote = Simplenote(self.config.get_config('sn_username'), - self.config.get_config('sn_password')) - - # we'll use this to store which notes are currently being synced by - # the background thread, so we don't add them anew if they're still - # in progress. This variable is only used by the background thread. - self.threaded_syncing_keys = {} - - def filtered_notes_sort(self, filtered_notes, sort_mode='date'): - if sort_mode == 'date': - if self.config.get_config('pinned_ontop') == 'yes': - filtered_notes.sort(utils.sort_by_modify_date_pinned, reverse=True) - else: - filtered_notes.sort(key=lambda o: -float(o.note.get('modifydate', 0))) - else: - if self.config.get_config('pinned_ontop') == 'yes': - filtered_notes.sort(utils.sort_by_title_pinned) - else: - filtered_notes.sort(key=lambda o: utils.get_note_title(o.note)) - - def filter_notes(self, search_string=None, search_mode='gstyle'): - """Return list of notes filtered with search string. - - Based on the search mode that has been selected in self.config, - this method will call the appropriate helper method to do the - actual work of filtering the notes. - - Returns a list of filtered notes with selected search mode and sorted - according to configuration. Two more elements in tuple: a regular - expression that can be used for highlighting strings in the text widget - and the total number of notes in memory. - """ - - if search_mode == 'gstyle': - filtered_notes, match_regexp, active_notes = \ - self.filter_notes_gstyle(search_string) - else: - filtered_notes, match_regexp, active_notes = \ - self.filter_notes_regex(search_string) - - self.filtered_notes_sort(filtered_notes, - self.config.get_config('sort_mode')) - - return filtered_notes, match_regexp, active_notes - - def _helper_gstyle_tagmatch(self, tag_pats, note): - # Returns: - # 2 = match - no tag patterns specified - # 1 = match - all tag patterns match a tag on this note - # 0 = no match - note has no tags or not all tag patterns match - - if not tag_pats: - # match because no tag patterns were specified - return 2 - - note_tags = note.get('tags') - - if not note_tags: - # tag patterns specified but note has no tags, so no match - return 0 - - # for each tag_pat, we have to find a matching tag - tag_pats_matched = 0 - for tp in tag_pats: - for t in note_tags: - if t.startswith(tp): - tag_pats_matched += 1 - break - - if tag_pats_matched == len(tag_pats): - # all tag patterns specified matched a tag on this note - return 1 - - # note doesn't match - return 0 - - def _helper_gstyle_wordmatch(self, word_pats, content): - if not word_pats: - return True - - word_pats_matched = 0 - lowercase_content = content.lower() # case insensitive search - for wp in word_pats: - wp = wp.lower() # case insensitive search - if wp in lowercase_content: - word_pats_matched += 1 - - if word_pats_matched == len(word_pats): - return True; - - return False - - def filter_notes_gstyle(self, search_string=None): - - filtered_notes = [] - - # total number of notes, excluding deleted - # if tag:trash then counts deleted as well - active_notes = 0 - - if not search_string: - for k in self.notes: - n = self.notes[k] - if n.get('deleted'): - continue - active_notes += 1 - filtered_notes.append(utils.KeyValueObject(key=k, note=n, tagfound=0)) - - return filtered_notes, [], active_notes - - # group0: tag:([^\s]+) - # group1: multiple words in quotes - # group2: single words - - # example result for: 'tag:tag1 tag:tag2 word1 "word2 word3" tag:tag3' - # [ ('tag1', '', ''), - # ('tag2', '', ''), - # ('', '', 'word1'), - # ('', 'word2 word3', ''), - # ('tag3', '', '') ] - - groups = re.findall('tag:([^\s]+)|"([^"]+)"|([^\s]+)', search_string) - all_pats = [[] for _ in range(3)] - - search_trash = False - for g in groups: - if g[0] == 'trash': - groups.remove(g) - search_trash = True - - # we end up with [[tag_pats],[multi_word_pats],[single_word_pats]] - for g in groups: - for i in range(3): - if g[i]: all_pats[i].append(g[i]) - - for k in self.notes: - n = self.notes[k] - - if not search_trash and n.get('deleted'): - continue - - active_notes += 1 - - if search_trash and len(groups) == 0: - # simple search of only 'tag:trash' to get all trashed notes - if n.get('deleted'): - filtered_notes.append( - utils.KeyValueObject(key=k, - note=n, - tagfound=1)) - continue - - tagmatch = self._helper_gstyle_tagmatch(all_pats[0], n) - - word_pats = all_pats[1] + all_pats[2] - - if tagmatch and \ - self._helper_gstyle_wordmatch(word_pats, n.get('content')): - # we have a note that can go through! - filtered_notes.append( - utils.KeyValueObject(key=k, - note=n, - tagfound=1 if tagmatch == 1 else 0)) - - return filtered_notes, '|'.join(all_pats[1] + all_pats[2]), active_notes - - def filter_notes_regex(self, search_string=None): - """ - Return a list of notes filtered using the regex search_string. - Each element in the list is a tuple (local_key, note). - """ - sspat = None - if search_string: - try: - sspat = re.compile(search_string) - except re.error: - sspat = None - - filtered_notes = [] - active_notes = 0 # total number of notes, including deleted ones - - for k in self.notes: - n = self.notes[k] - - active_notes += 1 - - if not sspat: - filtered_notes.append(utils.KeyValueObject(key=k, note=n, tagfound=0)) - continue - - if self.config.get_config('search_tags') == 'yes': - tag_matched = False - for t in n.get('tags'): - if sspat.search(t): - tag_matched = True - filtered_notes.append(utils.KeyValueObject(key=k, note=n, tagfound=1)) - break - if tag_matched: - continue - - if sspat.search(n.get('content')): - filtered_notes.append(utils.KeyValueObject(key=k, note=n, tagfound=0)) - - match_regexp = search_string if sspat else '' - return filtered_notes, match_regexp, active_notes - - def create_note(self, content): - # need to get a key unique to this database. not really important - # what it is, as long as it's unique. - new_key = utils.generate_random_key() - while new_key in self.notes: - new_key = utils.generate_random_key() - - timestamp = time.time() - - # note has no internal key yet. - new_note = { - 'content' : content, - 'deleted' : 0, - 'modifydate' : timestamp, - 'createdate' : timestamp, - 'savedate' : 0, # never been written to disc - 'syncdate' : 0, # never been synced with server - 'tags' : [] - } - - self.notes[new_key] = new_note - - return new_key - - def get_note(self, key): - return self.notes[key] - - def get_note_systemtags(self, key): - return self.notes[key].get('systemtags') - - def get_note_tags(self, key): - return self.notes[key].get('tags') - - def get_note_content(self, key): - return self.notes[key].get('content') - - def flag_what_changed(self, note, what_changed): - if 'what_changed' not in note: - note['what_changed'] = [] - if what_changed not in note['what_changed']: - note['what_changed'].append(what_changed) - - def set_note_deleted(self, key, deleted): - n = self.notes[key] - if (not n['deleted'] and deleted) or \ - (n['deleted'] and not deleted): - n['deleted'] = deleted - n['modifydate'] = time.time() - self.flag_what_changed(n, 'deleted') - self.log('Note {0} (key={1})'.format('trashed' if deleted else 'untrashed', key)) - - def set_note_content(self, key, content): - n = self.notes[key] - old_content = n.get('content') - if content != old_content: - n['content'] = content - n['modifydate'] = time.time() - self.flag_what_changed(n, 'content') - self.log('Note content updated (key={0})'.format(key)) - - def set_note_tags(self, key, tags): - n = self.notes[key] - old_tags = n.get('tags') - tags = utils.sanitise_tags(tags) - if tags != old_tags: - n['tags'] = tags - n['modifydate'] = time.time() - self.flag_what_changed(n, 'tags') - self.log('Note tags updated (key={0})'.format(key)) - - def set_note_pinned(self, key, pinned): - n = self.notes[key] - old_pinned = utils.note_pinned(n) - if pinned != old_pinned: - if 'systemtags' not in n: - n['systemtags'] = [] - systemtags = n['systemtags'] - if pinned: - systemtags.append('pinned') - else: - systemtags.remove('pinned') - n['modifydate'] = time.time() - self.flag_what_changed(n, 'systemtags') - self.log('Note {0} (key={1})'.format('pinned' if pinned else 'unpinned', key)) - - def set_note_markdown(self, key, markdown): - n = self.notes[key] - old_markdown = utils.note_markdown(n) - if markdown != old_markdown: - if 'systemtags' not in n: - n['systemtags'] = [] - systemtags = n['systemtags'] - if markdown: - systemtags.append('markdown') - else: - systemtags.remove('markdown') - n['modifydate'] = time.time() - self.flag_what_changed(n, 'systemtags') - self.log('Note markdown {0} (key={1})'.format('flagged' if markdown else 'unflagged', key)) - - def helper_key_to_fname(self, k): - return os.path.join(self.config.get_config('db_path'), k) + '.json' - - def helper_save_note(self, k, note): - # Save a single note to disc. - fn = self.helper_key_to_fname(k) - json.dump(note, open(fn, 'wb'), indent=2) - - # record that we saved this to disc. - note['savedate'] = time.time() - - def sync_notes(self, server_sync=True, full_sync=True): - """Perform a full bi-directional sync with server. - - This follows the recipe in the SimpleNote 2.0 API documentation. - After this, it could be that local keys have been changed, so - reset any views that you might have! - - From Simplenote API v2.1.3... - - To check for changes you can use 'syncnum' and 'version'. 'syncnum' will - increment whenever there is any change to a note, content change, tag - change, etc. 'version' will increment whenever the content property is - changed. You should store both these numbers in your client to track - changes and determine when a note needs to be updated or saved. - - Psuedo-code algorithm for syncing: - - 1. for any note changed locally, including new notes: - save note to server, update note with response - // (new syncnum, version, possible newly-merged content) - - 2. get the note index - - 3. for each remote note - if remote syncnum > local syncnum || - a new note and key is not in local store - retrieve note, update note with response - - 4. for each local note not in the index - PERMANENT DELETE, remove note from local store - """ - - local_updates = {} - local_deletes = {} - server_keys = {} - now = time.time() - - sync_start_time = time.time() - sync_errors = 0 - - if server_sync and full_sync: - self.log("Starting full sync") - - # 1. for any note changed locally, including new notes: - # save note to server, update note with response - for note_index, local_key in enumerate(self.notes.keys()): - n = self.notes[local_key] - if not n.get('key') or \ - float(n.get('modifydate')) > float(n.get('syncdate')): - - savedate = float(n.get('savedate')) - if float(n.get('modifydate')) > savedate or \ - float(n.get('syncdate')) > savedate: - # this will trigger a save to disk after sync algorithm - # we want this note saved even if offline or sync fails - local_updates[local_key] = True - - if not server_sync: - # the 'what_changed' field will be written to disk and - # picked up whenever the next full server sync occurs - continue - - # only send required fields - cn = copy.deepcopy(n) - if 'what_changed' in n: - del n['what_changed'] - - if 'minversion' in cn: - del cn['minversion'] - del cn['createdate'] - del cn['syncdate'] - del cn['savedate'] - - if 'what_changed' in cn: - if 'deleted' not in cn['what_changed']: - del cn['deleted'] - if 'systemtags' not in cn['what_changed']: - del cn['systemtags'] - if 'tags' not in cn['what_changed']: - del cn['tags'] - if 'content' not in cn['what_changed']: - del cn['content'] - del cn['what_changed'] - - uret = self.simplenote.update_note(cn) - - if uret[1] == 0: # success - # if this is a new note our local key is not valid anymore - # merge the note we got back (content could be empty) - # record syncdate and save the note at the assigned key - del self.notes[local_key] - k = uret[0].get('key') - n.update(uret[0]) - n['syncdate'] = now - self.notes[k] = n - - local_updates[k] = True - if local_key != k: - # if local_key was a different key it should be deleted - local_deletes[local_key] = True - if local_key in local_updates: - del local_updates[local_key] - - self.log('Synced note to server (key={0})'.format(local_key)) - else: - self.log('ERROR: Failed to sync note to server (key={0})'.format(local_key)) - sync_errors += 1 - - # 2. get the note index - if not server_sync: - nl = [] - else: - nl = self.simplenote.get_note_list(since=None if full_sync else self.last_sync) - - if nl[1] == 0: # success - nl = nl[0] - else: - self.log('ERROR: Failed to get note list from server') - sync_errors += 1 - nl = [] - - # 3. for each remote note - # if remote syncnum > local syncnum || - # a new note and key is not in local store - # retrieve note, update note with response - len_nl = len(nl) - sync_errors = 0 - for note_index, n in enumerate(nl): - k = n.get('key') - server_keys[k] = True - # this works because in the prior step we rewrite local keys to - # server keys when we get an updated note back from the server - if k in self.notes: - # we already have this note - # if the server note has a newer syncnum we need to get it - if int(n.get('syncnum')) > int(self.notes[k].get('syncnum', -1)): - gret = self.simplenote.get_note(k) - if gret[1] == 0: - self.notes[k].update(gret[0]) - local_updates[k] = True - self.notes[k]['syncdate'] = now - - self.log('Synced newer note from server (key={0})'.format(k)) - else: - self.log('ERROR: Failed to sync newer note from server (key={0})'.format(k)) - sync_errors += 1 - else: - # this is a new note - gret = self.simplenote.get_note(k) - if gret[1] == 0: - self.notes[k] = gret[0] - local_updates[k] = True - self.notes[k]['syncdate'] = now - - self.log('Synced new note from server (key={0})'.format(k)) - else: - self.log('ERROR: Failed syncing new note from server (key={0})'.format(k)) - sync_errors += 1 - - # 4. for each local note not in the index - # PERMANENT DELETE, remove note from local store - # Only do this when a full sync (i.e. entire index) is performed! - if server_sync and full_sync: - for local_key in self.notes.keys(): - if local_key not in server_keys: - del self.notes[local_key] - local_deletes[local_key] = True - - # sync done, now write changes to db_path - - for k in local_updates.keys(): - try: - self.helper_save_note(k, self.notes[k]) - except WriteError, e: - raise WriteError (str(e)) - self.log("Saved note to disk (key={0})".format(k)) - - for k in local_deletes.keys(): - fn = self.helper_key_to_fname(k) - if os.path.exists(fn): - os.unlink(fn) - self.log("Deleted note from disk (key={0})".format(k)) - - if not sync_errors: - self.last_sync = sync_start_time - - # if there were any changes then update the current view - if len(local_updates) > 0 or len(local_deletes) > 0: - self.update_view() - - if server_sync and full_sync: - self.log("Full sync completed") - - 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) - modifydate = float(n['modifydate']) - savedate = float(n['savedate']) - syncdate = float(n['syncdate']) - - if savedate > modifydate: - o.saved = True - else: - o.modified = True - - if syncdate > modifydate: - o.synced = True - - return o - - def verify_all_saved(self): - all_saved = True - self.sync_lock.acquire() - for k in self.notes.keys(): - o = self.get_note_status(k) - if not o.saved: - all_saved = False - break - self.sync_lock.release() - return all_saved - - def sync_now(self, do_server_sync=True): - self.sync_lock.acquire() - self.sync_notes(server_sync=do_server_sync, - full_sync=True if not self.last_sync else False) - self.sync_lock.release() - - # sync worker thread... - def sync_worker(self, do_server_sync): - time.sleep(1) # give some time to wait for GUI initialization - self.log('Sync worker: started') - while True: - self.sync_now(do_server_sync) - time.sleep(5) - diff --git a/setup.py b/setup.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python2 + +# Copyright (c) 2014 Eric Davis +# Licensed under the MIT License + +from distutils.core import setup +import simplenote_cli + +setup( + name=simplenote_cli.__productname__, + description=simplenote_cli.__description__, + version=simplenote_cli.__version__, + author=simplenote_cli.__author__, + author_email=simplenote_cli.__author_email__, + utl=simplenote_cli.__url__, + requires=[ 'urwid' ], + packages=[ 'simplenote_cli' ], + scripts=[ 'sncli' ] + ) + diff --git a/simplenote.py b/simplenote.py @@ -1,339 +0,0 @@ -# -*- coding: utf-8 -*- -""" - simplenote.py - ~~~~~~~~~~~~~~ - - Python library for accessing the Simplenote API - - :copyright: (c) 2011 by Daniel Schauenberg - :license: MIT, see LICENSE for more details. -""" - -import urllib -import urllib2 -from urllib2 import HTTPError -import base64 -import time -import datetime -import logging - -try: - import json -except ImportError: - try: - import simplejson as json - except ImportError: - # For Google AppEngine - from django.utils import simplejson as json - -AUTH_URL = 'https://simple-note.appspot.com/api/login' -DATA_URL = 'https://simple-note.appspot.com/api2/data' -INDX_URL = 'https://simple-note.appspot.com/api2/index?' -NOTE_FETCH_LENGTH = 100 - -class SimplenoteLoginFailed(Exception): - pass - -class Simplenote(object): - """ Class for interacting with the simplenote web service """ - - def __init__(self, username, password): - """ object constructor """ - self.username = urllib2.quote(username) - self.password = urllib2.quote(password) - self.token = None - - def authenticate(self, user, password): - """ Method to get simplenote auth token - - Arguments: - - user (string): simplenote email address - - password (string): simplenote password - - Returns: - Simplenote API token as string - - """ - auth_params = "email=%s&password=%s" % (user, password) - values = base64.encodestring(auth_params) - request = Request(AUTH_URL, values) - try: - res = urllib2.urlopen(request).read() - token = urllib2.quote(res) - except HTTPError: - raise SimplenoteLoginFailed('Login to Simplenote API failed!') - except IOError: # no connection exception - token = None - return token - - def get_token(self): - """ Method to retrieve an auth token. - - The cached global token is looked up and returned if it exists. If it - is `None` a new one is requested and returned. - - Returns: - Simplenote API token as string - - """ - if self.token == None: - self.token = self.authenticate(self.username, self.password) - return self.token - - - def get_note(self, noteid, version=None): - """ method to get a specific note - - Arguments: - - noteid (string): ID of the note to get - - version (int): optional version of the note to get - - Returns: - A tuple `(note, status)` - - - note (dict): note object - - status (int): 0 on sucesss and -1 otherwise - - """ - # request note - params_version = "" - if version is not None: - params_version = '/' + str(version) - - params = '/%s%s?auth=%s&email=%s' % (str(noteid), params_version, self.get_token(), self.username) - #logging.debug('REQUEST: ' + DATA_URL+params) - request = Request(DATA_URL+params) - try: - response = urllib2.urlopen(request) - except HTTPError, e: - #logging.debug('RESPONSE ERROR: ' + str(e)) - return e, -1 - except IOError, e: - #logging.debug('RESPONSE ERROR: ' + str(e)) - return e, -1 - note = json.loads(response.read()) - # use UTF-8 encoding - note["content"] = note["content"].encode('utf-8') - # For early versions of notes, tags not always available - if note.has_key("tags"): - note["tags"] = [t.encode('utf-8') for t in note["tags"]] - #logging.debug('RESPONSE OK: ' + str(note)) - return note, 0 - - def update_note(self, note): - """ function to update a specific note object, if the note object does not - have a "key" field, a new note is created - - Arguments - - note (dict): note object to update - - Returns: - A tuple `(note, status)` - - - note (dict): note object - - status (int): 0 on sucesss and -1 otherwise - - """ - # use UTF-8 encoding - # cpbotha: in both cases check if it's not unicode already - # otherwise you get "TypeError: decoding Unicode is not supported" - if note.has_key("content"): - if isinstance(note["content"], str): - note["content"] = unicode(note["content"], 'utf-8') - - if note.has_key("tags"): - # if a tag is a string, unicode it, otherwise pass it through - # unchanged (it's unicode already) - # using the ternary operator, because I like it: a if test else b - note["tags"] = [unicode(t, 'utf-8') if isinstance(t, str) else t for t in note["tags"]] - - # determine whether to create a new note or updated an existing one - if "key" in note: - # set modification timestamp if not set by client - if 'modifydate' not in note: - note["modifydate"] = time.time() - - url = '%s/%s?auth=%s&email=%s' % (DATA_URL, note["key"], - self.get_token(), self.username) - else: - url = '%s?auth=%s&email=%s' % (DATA_URL, self.get_token(), self.username) - #logging.debug('REQUEST: ' + url + ' - ' + str(note)) - request = Request(url, urllib.quote(json.dumps(note))) - response = "" - try: - response = urllib2.urlopen(request) - except IOError, e: - #logging.debug('RESPONSE ERROR: ' + str(e)) - return e, -1 - note = json.loads(response.read()) - if note.has_key("content"): - # use UTF-8 encoding - note["content"] = note["content"].encode('utf-8') - if note.has_key("tags"): - note["tags"] = [t.encode('utf-8') for t in note["tags"]] - #logging.debug('RESPONSE OK: ' + str(note)) - return note, 0 - - def add_note(self, note): - """wrapper function to add a note - - The function can be passed the note as a dict with the `content` - property set, which is then directly send to the web service for - creation. Alternatively, only the body as string can also be passed. In - this case the parameter is used as `content` for the new note. - - Arguments: - - note (dict or string): the note to add - - Returns: - A tuple `(note, status)` - - - note (dict): the newly created note - - status (int): 0 on sucesss and -1 otherwise - - """ - if type(note) == str: - return self.update_note({"content": note}) - elif (type(note) == dict) and "content" in note: - return self.update_note(note) - else: - return "No string or valid note.", -1 - - def get_note_list(self, since=None, tags=[]): - """ function to get the note list - - The function can be passed optional arguments to limit the - date range of the list returned and/or limit the list to notes - containing a certain tag. If omitted a list of all notes - is returned. - - Arguments: - - since=time.time() epoch stamp: only return notes modified - since this date - - tags=[] list of tags as string: return notes that have - at least one of these tags - - Returns: - A tuple `(notes, status)` - - - notes (list): A list of note objects with all properties set except - `content`. - - status (int): 0 on sucesss and -1 otherwise - - """ - # initialize data - status = 0 - ret = [] - response = {} - notes = { "data" : [] } - - # get the note index - params = 'auth=%s&email=%s&length=%s' % (self.get_token(), self.username, - NOTE_FETCH_LENGTH) - if since is not None: - params += '&since=%s' % since - - # perform initial HTTP request - try: - #logging.debug('REQUEST: ' + INDX_URL+params) - request = Request(INDX_URL+params) - response = json.loads(urllib2.urlopen(request).read()) - #logging.debug('RESPONSE OK: ' + str(response)) - notes["data"].extend(response["data"]) - except IOError: - status = -1 - - # get additional notes if bookmark was set in response - while "mark" in response: - vals = (self.get_token(), self.username, response["mark"], NOTE_FETCH_LENGTH) - params = 'auth=%s&email=%s&mark=%s&length=%s' % vals - if since is not None: - params += '&since=%s' % since - - # perform the actual HTTP request - try: - #logging.debug('REQUEST: ' + INDX_URL+params) - request = Request(INDX_URL+params) - response = json.loads(urllib2.urlopen(request).read()) - #logging.debug('RESPONSE OK: ' + str(response)) - notes["data"].extend(response["data"]) - except IOError: - status = -1 - - # parse data fields in response - note_list = notes["data"] - - # Can only filter for tags at end, once all notes have been retrieved. - #Below based on simplenote.vim, except we return deleted notes as well - if (len(tags) > 0): - note_list = [n for n in note_list if (len(set(n["tags"]).intersection(tags)) > 0)] - - return note_list, status - - def trash_note(self, note_id): - """ method to move a note to the trash - - Arguments: - - note_id (string): key of the note to trash - - Returns: - A tuple `(note, status)` - - - note (dict): the newly created note or an error message - - status (int): 0 on sucesss and -1 otherwise - - """ - # get note - note, status = self.get_note(note_id) - if (status == -1): - return note, status - # set deleted property - note["deleted"] = 1 - # update note - return self.update_note(note) - - def delete_note(self, note_id): - """ method to permanently delete a note - - Arguments: - - note_id (string): key of the note to trash - - Returns: - A tuple `(note, status)` - - - note (dict): an empty dict or an error message - - status (int): 0 on sucesss and -1 otherwise - - """ - # notes have to be trashed before deletion - note, status = self.trash_note(note_id) - if (status == -1): - return note, status - - params = '/%s?auth=%s&email=%s' % (str(note_id), self.get_token(), - self.username) - #logging.debug('REQUEST DELETE: ' + DATA_URL+params) - request = Request(url=DATA_URL+params, method='DELETE') - try: - urllib2.urlopen(request) - except IOError, e: - return e, -1 - return {}, 0 - - -class Request(urllib2.Request): - """ monkey patched version of urllib2's Request to support HTTP DELETE - Taken from http://python-requests.org, thanks @kennethreitz - """ - - def __init__(self, url, data=None, headers={}, origin_req_host=None, - unverifiable=False, method=None): - urllib2.Request.__init__(self, url, data, headers, origin_req_host, unverifiable) - self.method = method - - def get_method(self): - if self.method: - return self.method - - return urllib2.Request.get_method(self) - diff --git a/simplenote_cli/__init__.py b/simplenote_cli/__init__.py @@ -0,0 +1,8 @@ +__productname__ = 'sncli' +__version__ = '0.1.1' +__copyright__ = "Copyright (c) 2014 Eric Davis" +__author__ = "Eric Davis" +__author_email__ = "edavis@insanum.com" +__description__ = "Simplenote Command Line Interface" +__url__ = "https://github.com/insanum/sncli" +__license__ = "Licensed under the MIT License" diff --git a/simplenote_cli/config.py b/simplenote_cli/config.py @@ -0,0 +1,251 @@ + +# Copyright (c) 2014 Eric Davis +# Licensed under the MIT License + +import os, urwid, collections, ConfigParser + +class Config: + + def __init__(self): + self.home = os.path.abspath(os.path.expanduser('~')) + defaults = \ + { + 'cfg_sn_username' : '', + 'cfg_sn_password' : '', + 'cfg_db_path' : os.path.join(self.home, '.sncli'), + 'cfg_search_tags' : 'yes', # with regex searches + 'cfg_sort_mode' : 'date', # 'alpha' or 'date' + 'cfg_pinned_ontop' : 'yes', + 'cfg_tabstop' : '4', + 'cfg_format_strftime' : '%Y/%m/%d', + 'cfg_format_note_title' : '[%D] %F %-N %T', + 'cfg_status_bar' : 'yes', + 'cfg_editor' : 'vim', + 'cfg_pager' : 'less -c', + 'cfg_diff' : 'diff -b -U10', + 'cfg_max_logs' : '5', + 'cfg_log_timeout' : '5', + 'cfg_log_reversed' : 'yes', + + 'kb_help' : 'h', + 'kb_quit' : 'q', + 'kb_sync' : 'S', + 'kb_down' : 'j', + 'kb_up' : 'k', + 'kb_page_down' : ' ', + 'kb_page_up' : 'b', + 'kb_half_page_down' : 'ctrl d', + 'kb_half_page_up' : 'ctrl u', + 'kb_bottom' : 'G', + 'kb_top' : 'g', + 'kb_status' : 's', + 'kb_create_note' : 'C', + 'kb_edit_note' : 'e', + 'kb_view_note' : 'enter', + 'kb_view_note_ext' : 'meta enter', + 'kb_view_note_json' : 'O', + 'kb_pipe_note' : '|', + 'kb_view_next_note' : 'J', + 'kb_view_prev_note' : 'K', + 'kb_view_log' : 'l', + 'kb_tabstop2' : '2', + 'kb_tabstop4' : '4', + 'kb_tabstop8' : '8', + 'kb_prev_version' : '<', + 'kb_next_version' : '>', + 'kb_diff_version' : 'D', + 'kb_restore_version' : 'R', + 'kb_latest_version' : 'L', + 'kb_select_version' : '#', + 'kb_search_gstyle' : '/', + 'kb_search_regex' : 'meta /', + 'kb_clear_search' : 'A', + 'kb_sort_date' : 'd', + 'kb_sort_alpha' : 'a', + 'kb_note_trash' : 'T', + 'kb_note_pin' : 'p', + '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_user_input_bar_fg' : 'white', + 'clr_user_input_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) + self.configs_read = cp.read([os.path.join(self.home, '.snclirc')]) + + cfg_sec = 'sncli' + + if not cp.has_section(cfg_sec): + cp.add_section(cfg_sec) + + # ordered dicts used to ease help + + self.configs = collections.OrderedDict() + self.configs['sn_username'] = [ cp.get(cfg_sec, 'cfg_sn_username', raw=True), 'Simplenote Username' ] + self.configs['sn_password'] = [ cp.get(cfg_sec, 'cfg_sn_password', raw=True), 'Simplenote Password' ] + self.configs['db_path'] = [ cp.get(cfg_sec, 'cfg_db_path'), 'Note storage path' ] + self.configs['search_tags'] = [ cp.get(cfg_sec, 'cfg_search_tags'), 'Search tags as well' ] + self.configs['sort_mode'] = [ cp.get(cfg_sec, 'cfg_sort_mode'), 'Sort mode' ] + self.configs['pinned_ontop'] = [ cp.get(cfg_sec, 'cfg_pinned_ontop'), 'Pinned at top of list' ] + self.configs['tabstop'] = [ cp.get(cfg_sec, 'cfg_tabstop'), 'Tabstop spaces' ] + self.configs['format_strftime'] = [ cp.get(cfg_sec, 'cfg_format_strftime', raw=True), 'Date strftime format' ] + self.configs['format_note_title'] = [ cp.get(cfg_sec, 'cfg_format_note_title', raw=True), 'Note title format' ] + self.configs['status_bar'] = [ cp.get(cfg_sec, 'cfg_status_bar'), 'Show the status bar' ] + self.configs['editor'] = [ cp.get(cfg_sec, 'cfg_editor'), 'Editor command' ] + self.configs['pager'] = [ cp.get(cfg_sec, 'cfg_pager'), 'External pager command' ] + self.configs['diff'] = [ cp.get(cfg_sec, 'cfg_diff'), 'External diff command' ] + self.configs['max_logs'] = [ cp.get(cfg_sec, 'cfg_max_logs'), 'Max logs in footer' ] + self.configs['log_timeout'] = [ cp.get(cfg_sec, 'cfg_log_timeout'), 'Log timeout' ] + self.configs['log_reversed'] = [ cp.get(cfg_sec, 'cfg_log_reversed'), 'Log file reversed' ] + + self.keybinds = collections.OrderedDict() + self.keybinds['help'] = [ cp.get(cfg_sec, 'kb_help'), [ 'common' ], 'Help' ] + self.keybinds['quit'] = [ cp.get(cfg_sec, 'kb_quit'), [ 'common' ], 'Quit' ] + self.keybinds['sync'] = [ cp.get(cfg_sec, 'kb_sync'), [ 'common' ], 'Full sync' ] + self.keybinds['down'] = [ cp.get(cfg_sec, 'kb_down'), [ 'common' ], 'Scroll down one line' ] + self.keybinds['up'] = [ cp.get(cfg_sec, 'kb_up'), [ 'common' ], 'Scroll up one line' ] + 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['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' ] + self.keybinds['view_log'] = [ cp.get(cfg_sec, 'kb_view_log'), [ 'common' ], 'View log' ] + self.keybinds['create_note'] = [ cp.get(cfg_sec, 'kb_create_note'), [ 'titles' ], 'Create a new note' ] + self.keybinds['edit_note'] = [ cp.get(cfg_sec, 'kb_edit_note'), [ 'titles', 'notes' ], 'Edit note' ] + 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['view_note_json'] = [ cp.get(cfg_sec, 'kb_view_note_json'), [ 'titles', 'notes' ], 'View note raw json' ] + 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['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['diff_version'] = [ cp.get(cfg_sec, 'kb_diff_version'), [ 'notes' ], 'Diff version of note' ] + self.keybinds['restore_version'] = [ cp.get(cfg_sec, 'kb_restore_version'), [ 'notes' ], 'Restore version of note' ] + self.keybinds['latest_version'] = [ cp.get(cfg_sec, 'kb_latest_version'), [ 'notes' ], 'View latest version' ] + self.keybinds['select_version'] = [ cp.get(cfg_sec, 'kb_select_version'), [ 'notes' ], 'Select 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' ] + self.keybinds['sort_date'] = [ cp.get(cfg_sec, 'kb_sort_date'), [ 'titles' ], 'Sort notes by date' ] + self.keybinds['sort_alpha'] = [ cp.get(cfg_sec, 'kb_sort_alpha'), [ 'titles' ], 'Sort notes by alpha' ] + self.keybinds['note_trash'] = [ cp.get(cfg_sec, 'kb_note_trash'), [ 'titles', 'notes' ], 'Trash a note' ] + self.keybinds['note_pin'] = [ cp.get(cfg_sec, 'kb_note_pin'), [ 'titles', 'notes' ], 'Pin note' ] + self.keybinds['note_markdown'] = [ cp.get(cfg_sec, 'kb_note_markdown'), [ 'titles', 'notes' ], 'Flag note as markdown' ] + self.keybinds['note_tags'] = [ cp.get(cfg_sec, 'kb_note_tags'), [ 'titles', 'notes' ], 'Edit note tags' ] + + self.colors = collections.OrderedDict() + self.colors['default_fg'] = [ cp.get(cfg_sec, 'clr_default_fg'), 'Default fg' ] + self.colors['default_bg'] = [ cp.get(cfg_sec, 'clr_default_bg'), 'Default bg' ] + self.colors['status_bar_fg'] = [ cp.get(cfg_sec, 'clr_status_bar_fg'), 'Status bar fg' ] + self.colors['status_bar_bg'] = [ cp.get(cfg_sec, 'clr_status_bar_bg'), 'Status bar bg' ] + self.colors['log_fg'] = [ cp.get(cfg_sec, 'clr_log_fg'), 'Log message fg' ] + self.colors['log_bg'] = [ cp.get(cfg_sec, 'clr_log_bg'), 'Log message bg' ] + self.colors['user_input_bar_fg'] = [ cp.get(cfg_sec, 'clr_user_input_bar_fg'), 'User input bar fg' ] + self.colors['user_input_bar_bg'] = [ cp.get(cfg_sec, 'clr_user_input_bar_bg'), 'User input bar bg' ] + self.colors['note_focus_fg'] = [ cp.get(cfg_sec, 'clr_note_focus_fg'), 'Note title focus fg' ] + self.colors['note_focus_bg'] = [ cp.get(cfg_sec, 'clr_note_focus_bg'), 'Note title focus bg' ] + self.colors['note_title_day_fg'] = [ cp.get(cfg_sec, 'clr_note_title_day_fg'), 'Day old note title fg' ] + self.colors['note_title_day_bg'] = [ cp.get(cfg_sec, 'clr_note_title_day_bg'), 'Day old note title bg' ] + self.colors['note_title_week_fg'] = [ cp.get(cfg_sec, 'clr_note_title_week_fg'), 'Week old note title fg' ] + self.colors['note_title_week_bg'] = [ cp.get(cfg_sec, 'clr_note_title_week_bg'), 'Week old note title bg' ] + self.colors['note_title_month_fg'] = [ cp.get(cfg_sec, 'clr_note_title_month_fg'), 'Month old note title fg' ] + self.colors['note_title_month_bg'] = [ cp.get(cfg_sec, 'clr_note_title_month_bg'), 'Month old note title bg' ] + self.colors['note_title_year_fg'] = [ cp.get(cfg_sec, 'clr_note_title_year_fg'), 'Year old note title fg' ] + self.colors['note_title_year_bg'] = [ cp.get(cfg_sec, 'clr_note_title_year_bg'), 'Year old note title bg' ] + self.colors['note_title_ancient_fg'] = [ cp.get(cfg_sec, 'clr_note_title_ancient_fg'), 'Ancient note title fg' ] + self.colors['note_title_ancient_bg'] = [ cp.get(cfg_sec, 'clr_note_title_ancient_bg'), 'Ancient note title bg' ] + self.colors['note_date_fg'] = [ cp.get(cfg_sec, 'clr_note_date_fg'), 'Note date fg' ] + self.colors['note_date_bg'] = [ cp.get(cfg_sec, 'clr_note_date_bg'), 'Note date bg' ] + self.colors['note_flags_fg'] = [ cp.get(cfg_sec, 'clr_note_flags_fg'), 'Note flags fg' ] + self.colors['note_flags_bg'] = [ cp.get(cfg_sec, 'clr_note_flags_bg'), 'Note flags bg' ] + self.colors['note_tags_fg'] = [ cp.get(cfg_sec, 'clr_note_tags_fg'), 'Note tags fg' ] + self.colors['note_tags_bg'] = [ cp.get(cfg_sec, 'clr_note_tags_bg'), 'Note tags bg' ] + self.colors['note_content_fg'] = [ cp.get(cfg_sec, 'clr_note_content_fg'), 'Note content fg' ] + 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' ] + self.colors['help_header_bg'] = [ cp.get(cfg_sec, 'clr_help_header_bg'), 'Help header bg' ] + self.colors['help_config_fg'] = [ cp.get(cfg_sec, 'clr_help_config_fg'), 'Help config fg' ] + self.colors['help_config_bg'] = [ cp.get(cfg_sec, 'clr_help_config_bg'), 'Help config bg' ] + self.colors['help_value_fg'] = [ cp.get(cfg_sec, 'clr_help_value_fg'), 'Help value fg' ] + self.colors['help_value_bg'] = [ cp.get(cfg_sec, 'clr_help_value_bg'), 'Help value bg' ] + self.colors['help_descr_fg'] = [ cp.get(cfg_sec, 'clr_help_descr_fg'), 'Help description fg' ] + self.colors['help_descr_bg'] = [ cp.get(cfg_sec, 'clr_help_descr_bg'), 'Help description bg' ] + + def get_config(self, name): + return self.configs[name][0] + + def get_config_descr(self, name): + return self.configs[name][1] + + def get_keybind(self, name): + return self.keybinds[name][0] + + def get_keybind_use(self, name): + return self.keybinds[name][1] + + def get_keybind_descr(self, name): + return self.keybinds[name][2] + + def get_color(self, name): + return self.colors[name][0] + + def get_color_descr(self, name): + return self.colors[name][1] + diff --git a/simplenote_cli/notes_db.py b/simplenote_cli/notes_db.py @@ -0,0 +1,617 @@ + +# Copyright (c) 2014 Eric Davis +# This file is *heavily* modified from nvpy. + +# nvPY: cross-platform note-taking app with simplenote syncing +# copyright 2012 by Charl P. Botha <cpbotha@vxlabs.com> +# new BSD license + +import os, time, re, glob, json, copy, threading +import utils +import simplenote +simplenote.NOTE_FETCH_LENGTH=100 +from simplenote import Simplenote + +class ReadError(RuntimeError): + pass + +class WriteError(RuntimeError): + pass + +class NotesDB(): + """NotesDB will take care of the local notes database and syncing with SN. + """ + def __init__(self, config, log, update_view): + self.config = config + self.log = log + self.update_view = update_view + + self.last_sync = 0 # set to zero to trigger a full sync + self.sync_lock = threading.Lock() + + # create db dir if it does not exist + if not os.path.exists(self.config.get_config('db_path')): + os.mkdir(self.config.get_config('db_path')) + + now = time.time() + # now read all .json files from disk + fnlist = glob.glob(self.helper_key_to_fname('*')) + + self.notes = {} + + for fn in fnlist: + try: + n = json.load(open(fn, 'rb')) + except IOError, e: + raise ReadError ('Error opening {0}: {1}'.format(fn, str(e))) + except ValueError, e: + raise ReadError ('Error reading {0}: {1}'.format(fn, str(e))) + else: + # we always have a localkey, also when we don't have a note['key'] yet (no sync) + localkey = os.path.splitext(os.path.basename(fn))[0] + self.notes[localkey] = n + # we maintain in memory a timestamp of the last save + # these notes have just been read, so at this moment + # they're in sync with the disc. + n['savedate'] = now + + # initialise the simplenote instance we're going to use + # this does not yet need network access + self.simplenote = Simplenote(self.config.get_config('sn_username'), + self.config.get_config('sn_password')) + + # we'll use this to store which notes are currently being synced by + # the background thread, so we don't add them anew if they're still + # in progress. This variable is only used by the background thread. + self.threaded_syncing_keys = {} + + def filtered_notes_sort(self, filtered_notes, sort_mode='date'): + if sort_mode == 'date': + if self.config.get_config('pinned_ontop') == 'yes': + filtered_notes.sort(utils.sort_by_modify_date_pinned, reverse=True) + else: + filtered_notes.sort(key=lambda o: -float(o.note.get('modifydate', 0))) + else: + if self.config.get_config('pinned_ontop') == 'yes': + filtered_notes.sort(utils.sort_by_title_pinned) + else: + filtered_notes.sort(key=lambda o: utils.get_note_title(o.note)) + + def filter_notes(self, search_string=None, search_mode='gstyle'): + """Return list of notes filtered with search string. + + Based on the search mode that has been selected in self.config, + this method will call the appropriate helper method to do the + actual work of filtering the notes. + + Returns a list of filtered notes with selected search mode and sorted + according to configuration. Two more elements in tuple: a regular + expression that can be used for highlighting strings in the text widget + and the total number of notes in memory. + """ + + if search_mode == 'gstyle': + filtered_notes, match_regexp, active_notes = \ + self.filter_notes_gstyle(search_string) + else: + filtered_notes, match_regexp, active_notes = \ + self.filter_notes_regex(search_string) + + self.filtered_notes_sort(filtered_notes, + self.config.get_config('sort_mode')) + + return filtered_notes, match_regexp, active_notes + + def _helper_gstyle_tagmatch(self, tag_pats, note): + # Returns: + # 2 = match - no tag patterns specified + # 1 = match - all tag patterns match a tag on this note + # 0 = no match - note has no tags or not all tag patterns match + + if not tag_pats: + # match because no tag patterns were specified + return 2 + + note_tags = note.get('tags') + + if not note_tags: + # tag patterns specified but note has no tags, so no match + return 0 + + # for each tag_pat, we have to find a matching tag + tag_pats_matched = 0 + for tp in tag_pats: + for t in note_tags: + if t.startswith(tp): + tag_pats_matched += 1 + break + + if tag_pats_matched == len(tag_pats): + # all tag patterns specified matched a tag on this note + return 1 + + # note doesn't match + return 0 + + def _helper_gstyle_wordmatch(self, word_pats, content): + if not word_pats: + return True + + word_pats_matched = 0 + lowercase_content = content.lower() # case insensitive search + for wp in word_pats: + wp = wp.lower() # case insensitive search + if wp in lowercase_content: + word_pats_matched += 1 + + if word_pats_matched == len(word_pats): + return True; + + return False + + def filter_notes_gstyle(self, search_string=None): + + filtered_notes = [] + + # total number of notes, excluding deleted + # if tag:trash then counts deleted as well + active_notes = 0 + + if not search_string: + for k in self.notes: + n = self.notes[k] + if n.get('deleted'): + continue + active_notes += 1 + filtered_notes.append(utils.KeyValueObject(key=k, note=n, tagfound=0)) + + return filtered_notes, [], active_notes + + # group0: tag:([^\s]+) + # group1: multiple words in quotes + # group2: single words + + # example result for: 'tag:tag1 tag:tag2 word1 "word2 word3" tag:tag3' + # [ ('tag1', '', ''), + # ('tag2', '', ''), + # ('', '', 'word1'), + # ('', 'word2 word3', ''), + # ('tag3', '', '') ] + + groups = re.findall('tag:([^\s]+)|"([^"]+)"|([^\s]+)', search_string) + all_pats = [[] for _ in range(3)] + + search_trash = False + for g in groups: + if g[0] == 'trash': + groups.remove(g) + search_trash = True + + # we end up with [[tag_pats],[multi_word_pats],[single_word_pats]] + for g in groups: + for i in range(3): + if g[i]: all_pats[i].append(g[i]) + + for k in self.notes: + n = self.notes[k] + + if not search_trash and n.get('deleted'): + continue + + active_notes += 1 + + if search_trash and len(groups) == 0: + # simple search of only 'tag:trash' to get all trashed notes + if n.get('deleted'): + filtered_notes.append( + utils.KeyValueObject(key=k, + note=n, + tagfound=1)) + continue + + tagmatch = self._helper_gstyle_tagmatch(all_pats[0], n) + + word_pats = all_pats[1] + all_pats[2] + + if tagmatch and \ + self._helper_gstyle_wordmatch(word_pats, n.get('content')): + # we have a note that can go through! + filtered_notes.append( + utils.KeyValueObject(key=k, + note=n, + tagfound=1 if tagmatch == 1 else 0)) + + return filtered_notes, '|'.join(all_pats[1] + all_pats[2]), active_notes + + def filter_notes_regex(self, search_string=None): + """ + Return a list of notes filtered using the regex search_string. + Each element in the list is a tuple (local_key, note). + """ + sspat = None + if search_string: + try: + sspat = re.compile(search_string) + except re.error: + sspat = None + + filtered_notes = [] + active_notes = 0 # total number of notes, including deleted ones + + for k in self.notes: + n = self.notes[k] + + active_notes += 1 + + if not sspat: + filtered_notes.append(utils.KeyValueObject(key=k, note=n, tagfound=0)) + continue + + if self.config.get_config('search_tags') == 'yes': + tag_matched = False + for t in n.get('tags'): + if sspat.search(t): + tag_matched = True + filtered_notes.append(utils.KeyValueObject(key=k, note=n, tagfound=1)) + break + if tag_matched: + continue + + if sspat.search(n.get('content')): + filtered_notes.append(utils.KeyValueObject(key=k, note=n, tagfound=0)) + + match_regexp = search_string if sspat else '' + return filtered_notes, match_regexp, active_notes + + def create_note(self, content): + # need to get a key unique to this database. not really important + # what it is, as long as it's unique. + new_key = utils.generate_random_key() + while new_key in self.notes: + new_key = utils.generate_random_key() + + timestamp = time.time() + + # note has no internal key yet. + new_note = { + 'content' : content, + 'deleted' : 0, + 'modifydate' : timestamp, + 'createdate' : timestamp, + 'savedate' : 0, # never been written to disc + 'syncdate' : 0, # never been synced with server + 'tags' : [] + } + + self.notes[new_key] = new_note + + return new_key + + def get_note(self, key): + return self.notes[key] + + def get_note_systemtags(self, key): + return self.notes[key].get('systemtags') + + def get_note_tags(self, key): + return self.notes[key].get('tags') + + def get_note_content(self, key): + return self.notes[key].get('content') + + def flag_what_changed(self, note, what_changed): + if 'what_changed' not in note: + note['what_changed'] = [] + if what_changed not in note['what_changed']: + note['what_changed'].append(what_changed) + + def set_note_deleted(self, key, deleted): + n = self.notes[key] + if (not n['deleted'] and deleted) or \ + (n['deleted'] and not deleted): + n['deleted'] = deleted + n['modifydate'] = time.time() + self.flag_what_changed(n, 'deleted') + self.log('Note {0} (key={1})'.format('trashed' if deleted else 'untrashed', key)) + + def set_note_content(self, key, content): + n = self.notes[key] + old_content = n.get('content') + if content != old_content: + n['content'] = content + n['modifydate'] = time.time() + self.flag_what_changed(n, 'content') + self.log('Note content updated (key={0})'.format(key)) + + def set_note_tags(self, key, tags): + n = self.notes[key] + old_tags = n.get('tags') + tags = utils.sanitise_tags(tags) + if tags != old_tags: + n['tags'] = tags + n['modifydate'] = time.time() + self.flag_what_changed(n, 'tags') + self.log('Note tags updated (key={0})'.format(key)) + + def set_note_pinned(self, key, pinned): + n = self.notes[key] + old_pinned = utils.note_pinned(n) + if pinned != old_pinned: + if 'systemtags' not in n: + n['systemtags'] = [] + systemtags = n['systemtags'] + if pinned: + systemtags.append('pinned') + else: + systemtags.remove('pinned') + n['modifydate'] = time.time() + self.flag_what_changed(n, 'systemtags') + self.log('Note {0} (key={1})'.format('pinned' if pinned else 'unpinned', key)) + + def set_note_markdown(self, key, markdown): + n = self.notes[key] + old_markdown = utils.note_markdown(n) + if markdown != old_markdown: + if 'systemtags' not in n: + n['systemtags'] = [] + systemtags = n['systemtags'] + if markdown: + systemtags.append('markdown') + else: + systemtags.remove('markdown') + n['modifydate'] = time.time() + self.flag_what_changed(n, 'systemtags') + self.log('Note markdown {0} (key={1})'.format('flagged' if markdown else 'unflagged', key)) + + def helper_key_to_fname(self, k): + return os.path.join(self.config.get_config('db_path'), k) + '.json' + + def helper_save_note(self, k, note): + # Save a single note to disc. + fn = self.helper_key_to_fname(k) + json.dump(note, open(fn, 'wb'), indent=2) + + # record that we saved this to disc. + note['savedate'] = time.time() + + def sync_notes(self, server_sync=True, full_sync=True): + """Perform a full bi-directional sync with server. + + This follows the recipe in the SimpleNote 2.0 API documentation. + After this, it could be that local keys have been changed, so + reset any views that you might have! + + From Simplenote API v2.1.3... + + To check for changes you can use 'syncnum' and 'version'. 'syncnum' will + increment whenever there is any change to a note, content change, tag + change, etc. 'version' will increment whenever the content property is + changed. You should store both these numbers in your client to track + changes and determine when a note needs to be updated or saved. + + Psuedo-code algorithm for syncing: + + 1. for any note changed locally, including new notes: + save note to server, update note with response + // (new syncnum, version, possible newly-merged content) + + 2. get the note index + + 3. for each remote note + if remote syncnum > local syncnum || + a new note and key is not in local store + retrieve note, update note with response + + 4. for each local note not in the index + PERMANENT DELETE, remove note from local store + """ + + local_updates = {} + local_deletes = {} + server_keys = {} + now = time.time() + + sync_start_time = time.time() + sync_errors = 0 + + if server_sync and full_sync: + self.log("Starting full sync") + + # 1. for any note changed locally, including new notes: + # save note to server, update note with response + for note_index, local_key in enumerate(self.notes.keys()): + n = self.notes[local_key] + if not n.get('key') or \ + float(n.get('modifydate')) > float(n.get('syncdate')): + + savedate = float(n.get('savedate')) + if float(n.get('modifydate')) > savedate or \ + float(n.get('syncdate')) > savedate: + # this will trigger a save to disk after sync algorithm + # we want this note saved even if offline or sync fails + local_updates[local_key] = True + + if not server_sync: + # the 'what_changed' field will be written to disk and + # picked up whenever the next full server sync occurs + continue + + # only send required fields + cn = copy.deepcopy(n) + if 'what_changed' in n: + del n['what_changed'] + + if 'minversion' in cn: + del cn['minversion'] + del cn['createdate'] + del cn['syncdate'] + del cn['savedate'] + + if 'what_changed' in cn: + if 'deleted' not in cn['what_changed']: + del cn['deleted'] + if 'systemtags' not in cn['what_changed']: + del cn['systemtags'] + if 'tags' not in cn['what_changed']: + del cn['tags'] + if 'content' not in cn['what_changed']: + del cn['content'] + del cn['what_changed'] + + uret = self.simplenote.update_note(cn) + + if uret[1] == 0: # success + # if this is a new note our local key is not valid anymore + # merge the note we got back (content could be empty) + # record syncdate and save the note at the assigned key + del self.notes[local_key] + k = uret[0].get('key') + n.update(uret[0]) + n['syncdate'] = now + self.notes[k] = n + + local_updates[k] = True + if local_key != k: + # if local_key was a different key it should be deleted + local_deletes[local_key] = True + if local_key in local_updates: + del local_updates[local_key] + + self.log('Synced note to server (key={0})'.format(local_key)) + else: + self.log('ERROR: Failed to sync note to server (key={0})'.format(local_key)) + sync_errors += 1 + + # 2. get the note index + if not server_sync: + nl = [] + else: + nl = self.simplenote.get_note_list(since=None if full_sync else self.last_sync) + + if nl[1] == 0: # success + nl = nl[0] + else: + self.log('ERROR: Failed to get note list from server') + sync_errors += 1 + nl = [] + + # 3. for each remote note + # if remote syncnum > local syncnum || + # a new note and key is not in local store + # retrieve note, update note with response + len_nl = len(nl) + sync_errors = 0 + for note_index, n in enumerate(nl): + k = n.get('key') + server_keys[k] = True + # this works because in the prior step we rewrite local keys to + # server keys when we get an updated note back from the server + if k in self.notes: + # we already have this note + # if the server note has a newer syncnum we need to get it + if int(n.get('syncnum')) > int(self.notes[k].get('syncnum', -1)): + gret = self.simplenote.get_note(k) + if gret[1] == 0: + self.notes[k].update(gret[0]) + local_updates[k] = True + self.notes[k]['syncdate'] = now + + self.log('Synced newer note from server (key={0})'.format(k)) + else: + self.log('ERROR: Failed to sync newer note from server (key={0})'.format(k)) + sync_errors += 1 + else: + # this is a new note + gret = self.simplenote.get_note(k) + if gret[1] == 0: + self.notes[k] = gret[0] + local_updates[k] = True + self.notes[k]['syncdate'] = now + + self.log('Synced new note from server (key={0})'.format(k)) + else: + self.log('ERROR: Failed syncing new note from server (key={0})'.format(k)) + sync_errors += 1 + + # 4. for each local note not in the index + # PERMANENT DELETE, remove note from local store + # Only do this when a full sync (i.e. entire index) is performed! + if server_sync and full_sync: + for local_key in self.notes.keys(): + if local_key not in server_keys: + del self.notes[local_key] + local_deletes[local_key] = True + + # sync done, now write changes to db_path + + for k in local_updates.keys(): + try: + self.helper_save_note(k, self.notes[k]) + except WriteError, e: + raise WriteError (str(e)) + self.log("Saved note to disk (key={0})".format(k)) + + for k in local_deletes.keys(): + fn = self.helper_key_to_fname(k) + if os.path.exists(fn): + os.unlink(fn) + self.log("Deleted note from disk (key={0})".format(k)) + + if not sync_errors: + self.last_sync = sync_start_time + + # if there were any changes then update the current view + if len(local_updates) > 0 or len(local_deletes) > 0: + self.update_view() + + if server_sync and full_sync: + self.log("Full sync completed") + + 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) + modifydate = float(n['modifydate']) + savedate = float(n['savedate']) + syncdate = float(n['syncdate']) + + if savedate > modifydate: + o.saved = True + else: + o.modified = True + + if syncdate > modifydate: + o.synced = True + + return o + + def verify_all_saved(self): + all_saved = True + self.sync_lock.acquire() + for k in self.notes.keys(): + o = self.get_note_status(k) + if not o.saved: + all_saved = False + break + self.sync_lock.release() + return all_saved + + def sync_now(self, do_server_sync=True): + self.sync_lock.acquire() + self.sync_notes(server_sync=do_server_sync, + full_sync=True if not self.last_sync else False) + self.sync_lock.release() + + # sync worker thread... + def sync_worker(self, do_server_sync): + time.sleep(1) # give some time to wait for GUI initialization + self.log('Sync worker: started') + while True: + self.sync_now(do_server_sync) + time.sleep(5) + diff --git a/simplenote_cli/simplenote.py b/simplenote_cli/simplenote.py @@ -0,0 +1,343 @@ + +# Copyright (c) 2014 Eric Davis +# This file is *slightly* modified from simplynote.py. + +# -*- coding: utf-8 -*- +""" + simplenote.py + ~~~~~~~~~~~~~~ + + Python library for accessing the Simplenote API + + :copyright: (c) 2011 by Daniel Schauenberg + :license: MIT, see LICENSE for more details. +""" + +import urllib +import urllib2 +from urllib2 import HTTPError +import base64 +import time +import datetime +import logging + +try: + import json +except ImportError: + try: + import simplejson as json + except ImportError: + # For Google AppEngine + from django.utils import simplejson as json + +AUTH_URL = 'https://simple-note.appspot.com/api/login' +DATA_URL = 'https://simple-note.appspot.com/api2/data' +INDX_URL = 'https://simple-note.appspot.com/api2/index?' +NOTE_FETCH_LENGTH = 100 + +class SimplenoteLoginFailed(Exception): + pass + +class Simplenote(object): + """ Class for interacting with the simplenote web service """ + + def __init__(self, username, password): + """ object constructor """ + self.username = urllib2.quote(username) + self.password = urllib2.quote(password) + self.token = None + + def authenticate(self, user, password): + """ Method to get simplenote auth token + + Arguments: + - user (string): simplenote email address + - password (string): simplenote password + + Returns: + Simplenote API token as string + + """ + auth_params = "email=%s&password=%s" % (user, password) + values = base64.encodestring(auth_params) + request = Request(AUTH_URL, values) + try: + res = urllib2.urlopen(request).read() + token = urllib2.quote(res) + except HTTPError: + raise SimplenoteLoginFailed('Login to Simplenote API failed!') + except IOError: # no connection exception + token = None + return token + + def get_token(self): + """ Method to retrieve an auth token. + + The cached global token is looked up and returned if it exists. If it + is `None` a new one is requested and returned. + + Returns: + Simplenote API token as string + + """ + if self.token == None: + self.token = self.authenticate(self.username, self.password) + return self.token + + + def get_note(self, noteid, version=None): + """ method to get a specific note + + Arguments: + - noteid (string): ID of the note to get + - version (int): optional version of the note to get + + Returns: + A tuple `(note, status)` + + - note (dict): note object + - status (int): 0 on sucesss and -1 otherwise + + """ + # request note + params_version = "" + if version is not None: + params_version = '/' + str(version) + + params = '/%s%s?auth=%s&email=%s' % (str(noteid), params_version, self.get_token(), self.username) + #logging.debug('REQUEST: ' + DATA_URL+params) + request = Request(DATA_URL+params) + try: + response = urllib2.urlopen(request) + except HTTPError, e: + #logging.debug('RESPONSE ERROR: ' + str(e)) + return e, -1 + except IOError, e: + #logging.debug('RESPONSE ERROR: ' + str(e)) + return e, -1 + note = json.loads(response.read()) + # use UTF-8 encoding + note["content"] = note["content"].encode('utf-8') + # For early versions of notes, tags not always available + if note.has_key("tags"): + note["tags"] = [t.encode('utf-8') for t in note["tags"]] + #logging.debug('RESPONSE OK: ' + str(note)) + return note, 0 + + def update_note(self, note): + """ function to update a specific note object, if the note object does not + have a "key" field, a new note is created + + Arguments + - note (dict): note object to update + + Returns: + A tuple `(note, status)` + + - note (dict): note object + - status (int): 0 on sucesss and -1 otherwise + + """ + # use UTF-8 encoding + # cpbotha: in both cases check if it's not unicode already + # otherwise you get "TypeError: decoding Unicode is not supported" + if note.has_key("content"): + if isinstance(note["content"], str): + note["content"] = unicode(note["content"], 'utf-8') + + if note.has_key("tags"): + # if a tag is a string, unicode it, otherwise pass it through + # unchanged (it's unicode already) + # using the ternary operator, because I like it: a if test else b + note["tags"] = [unicode(t, 'utf-8') if isinstance(t, str) else t for t in note["tags"]] + + # determine whether to create a new note or updated an existing one + if "key" in note: + # set modification timestamp if not set by client + if 'modifydate' not in note: + note["modifydate"] = time.time() + + url = '%s/%s?auth=%s&email=%s' % (DATA_URL, note["key"], + self.get_token(), self.username) + else: + url = '%s?auth=%s&email=%s' % (DATA_URL, self.get_token(), self.username) + #logging.debug('REQUEST: ' + url + ' - ' + str(note)) + request = Request(url, urllib.quote(json.dumps(note))) + response = "" + try: + response = urllib2.urlopen(request) + except IOError, e: + #logging.debug('RESPONSE ERROR: ' + str(e)) + return e, -1 + note = json.loads(response.read()) + if note.has_key("content"): + # use UTF-8 encoding + note["content"] = note["content"].encode('utf-8') + if note.has_key("tags"): + note["tags"] = [t.encode('utf-8') for t in note["tags"]] + #logging.debug('RESPONSE OK: ' + str(note)) + return note, 0 + + def add_note(self, note): + """wrapper function to add a note + + The function can be passed the note as a dict with the `content` + property set, which is then directly send to the web service for + creation. Alternatively, only the body as string can also be passed. In + this case the parameter is used as `content` for the new note. + + Arguments: + - note (dict or string): the note to add + + Returns: + A tuple `(note, status)` + + - note (dict): the newly created note + - status (int): 0 on sucesss and -1 otherwise + + """ + if type(note) == str: + return self.update_note({"content": note}) + elif (type(note) == dict) and "content" in note: + return self.update_note(note) + else: + return "No string or valid note.", -1 + + def get_note_list(self, since=None, tags=[]): + """ function to get the note list + + The function can be passed optional arguments to limit the + date range of the list returned and/or limit the list to notes + containing a certain tag. If omitted a list of all notes + is returned. + + Arguments: + - since=time.time() epoch stamp: only return notes modified + since this date + - tags=[] list of tags as string: return notes that have + at least one of these tags + + Returns: + A tuple `(notes, status)` + + - notes (list): A list of note objects with all properties set except + `content`. + - status (int): 0 on sucesss and -1 otherwise + + """ + # initialize data + status = 0 + ret = [] + response = {} + notes = { "data" : [] } + + # get the note index + params = 'auth=%s&email=%s&length=%s' % (self.get_token(), self.username, + NOTE_FETCH_LENGTH) + if since is not None: + params += '&since=%s' % since + + # perform initial HTTP request + try: + #logging.debug('REQUEST: ' + INDX_URL+params) + request = Request(INDX_URL+params) + response = json.loads(urllib2.urlopen(request).read()) + #logging.debug('RESPONSE OK: ' + str(response)) + notes["data"].extend(response["data"]) + except IOError: + status = -1 + + # get additional notes if bookmark was set in response + while "mark" in response: + vals = (self.get_token(), self.username, response["mark"], NOTE_FETCH_LENGTH) + params = 'auth=%s&email=%s&mark=%s&length=%s' % vals + if since is not None: + params += '&since=%s' % since + + # perform the actual HTTP request + try: + #logging.debug('REQUEST: ' + INDX_URL+params) + request = Request(INDX_URL+params) + response = json.loads(urllib2.urlopen(request).read()) + #logging.debug('RESPONSE OK: ' + str(response)) + notes["data"].extend(response["data"]) + except IOError: + status = -1 + + # parse data fields in response + note_list = notes["data"] + + # Can only filter for tags at end, once all notes have been retrieved. + #Below based on simplenote.vim, except we return deleted notes as well + if (len(tags) > 0): + note_list = [n for n in note_list if (len(set(n["tags"]).intersection(tags)) > 0)] + + return note_list, status + + def trash_note(self, note_id): + """ method to move a note to the trash + + Arguments: + - note_id (string): key of the note to trash + + Returns: + A tuple `(note, status)` + + - note (dict): the newly created note or an error message + - status (int): 0 on sucesss and -1 otherwise + + """ + # get note + note, status = self.get_note(note_id) + if (status == -1): + return note, status + # set deleted property + note["deleted"] = 1 + # update note + return self.update_note(note) + + def delete_note(self, note_id): + """ method to permanently delete a note + + Arguments: + - note_id (string): key of the note to trash + + Returns: + A tuple `(note, status)` + + - note (dict): an empty dict or an error message + - status (int): 0 on sucesss and -1 otherwise + + """ + # notes have to be trashed before deletion + note, status = self.trash_note(note_id) + if (status == -1): + return note, status + + params = '/%s?auth=%s&email=%s' % (str(note_id), self.get_token(), + self.username) + #logging.debug('REQUEST DELETE: ' + DATA_URL+params) + request = Request(url=DATA_URL+params, method='DELETE') + try: + urllib2.urlopen(request) + except IOError, e: + return e, -1 + return {}, 0 + + +class Request(urllib2.Request): + """ monkey patched version of urllib2's Request to support HTTP DELETE + Taken from http://python-requests.org, thanks @kennethreitz + """ + + def __init__(self, url, data=None, headers={}, origin_req_host=None, + unverifiable=False, method=None): + urllib2.Request.__init__(self, url, data, headers, origin_req_host, unverifiable) + self.method = method + + def get_method(self): + if self.method: + return self.method + + return urllib2.Request.get_method(self) + diff --git a/simplenote_cli/sncli.py b/simplenote_cli/sncli.py @@ -0,0 +1,1198 @@ + +# Copyright (c) 2014 Eric Davis +# Licensed under the MIT License + +import os, sys, getopt, re, signal, time, datetime, shlex, md5 +import subprocess, thread, threading, logging +import copy, json, urwid, datetime +import view_titles, view_note, view_help, view_log, user_input +import utils, temp +from config import Config +from simplenote import Simplenote +from notes_db import NotesDB, ReadError, WriteError +from logging.handlers import RotatingFileHandler + +class sncli: + + def __init__(self, do_server_sync, verbose=False): + self.config = Config() + self.do_server_sync = do_server_sync + self.verbose = verbose + self.do_gui = False + + if not os.path.exists(self.config.get_config('db_path')): + os.mkdir(self.config.get_config('db_path')) + + # configure the logging module + self.logfile = os.path.join(self.config.get_config('db_path'), 'sncli.log') + self.loghandler = RotatingFileHandler(self.logfile, maxBytes=100000, backupCount=1) + self.loghandler.setLevel(logging.DEBUG) + self.loghandler.setFormatter(logging.Formatter(fmt='%(asctime)s [%(levelname)s] %(message)s')) + self.logger = logging.getLogger() + self.logger.setLevel(logging.DEBUG) + self.logger.addHandler(self.loghandler) + self.config.logfile = self.logfile + + logging.debug('sncli logging initialized') + + self.logs = [] + + try: + self.ndb = NotesDB(self.config, self.log, self.gui_update_view) + except Exception, e: + self.log(str(e)) + sys.exit(1) + + def sync_notes(self): + self.ndb.sync_now(self.do_server_sync) + + def get_editor(self): + editor = self.config.get_config('editor') + if not editor and os.environ['EDITOR']: + editor = os.environ['EDITOR'] + if not editor: + self.log(u'No editor configured!') + return None + return editor + + def get_pager(self): + pager = self.config.get_config('pager') + if not pager and os.environ['PAGER']: + pager = os.environ['PAGER'] + if not pager: + self.log(u'No pager configured!') + return None + return pager + + def get_diff(self): + diff = self.config.get_config('diff') + if not diff: + self.log(u'No diff command configured!') + return None + return diff + + def exec_cmd_on_note(self, note, cmd=None, raw=False): + + if not cmd: + cmd = self.get_editor() + if not cmd: + return None + + tf = temp.tempfile_create(note if note else None, raw=raw) + + try: + subprocess.check_call(cmd + u' ' + temp.tempfile_name(tf), shell=True) + except Exception, e: + self.log(u'Command error: ' + str(e)) + temp.tempfile_delete(tf) + return None + + content = None + if not raw: + content = ''.join(temp.tempfile_content(tf)) + if not content or content == u'\n': + content = None + + temp.tempfile_delete(tf) + return content + + def exec_diff_on_note(self, note, old_note): + + diff = self.get_diff() + if not diff: + return None + + pager = self.get_pager() + if not pager: + return None + + ltf = temp.tempfile_create(note) + otf = temp.tempfile_create(old_note) + out = temp.tempfile_create(None) + + try: + subprocess.call(diff + u' ' + + temp.tempfile_name(ltf) + u' ' + + temp.tempfile_name(otf) + u' > ' + + temp.tempfile_name(out), + shell=True) + subprocess.check_call(pager + u' ' + + temp.tempfile_name(out), + shell=True) + except Exception, e: + self.log(u'Command error: ' + str(e)) + temp.tempfile_delete(ltf) + temp.tempfile_delete(otf) + temp.tempfile_delete(out) + return None + + temp.tempfile_delete(ltf) + temp.tempfile_delete(otf) + temp.tempfile_delete(out) + return None + + def gui_header_clear(self): + self.master_frame.contents['header'] = ( None, None ) + self.sncli_loop.draw_screen() + + def gui_header_set(self, w): + self.master_frame.contents['header'] = ( w, None ) + self.sncli_loop.draw_screen() + + def gui_header_get(self): + return self.master_frame.contents['header'][0] + + def gui_header_focus(self): + self.master_frame.focus_position = 'header' + + def gui_footer_log_clear(self): + ui = self.gui_footer_input_get() + self.master_frame.contents['footer'] = \ + (urwid.Pile([ urwid.Pile([]), urwid.Pile([ui]) ]), None) + self.sncli_loop.draw_screen() + + def gui_footer_log_set(self, pl): + ui = self.gui_footer_input_get() + self.master_frame.contents['footer'] = \ + (urwid.Pile([ urwid.Pile(pl), urwid.Pile([ui]) ]), None) + self.sncli_loop.draw_screen() + + def gui_footer_log_get(self): + return self.master_frame.contents['footer'][0].contents[0][0] + + def gui_footer_input_clear(self): + pl = self.gui_footer_log_get() + self.master_frame.contents['footer'] = \ + (urwid.Pile([ urwid.Pile([pl]), urwid.Pile([]) ]), None) + self.sncli_loop.draw_screen() + + def gui_footer_input_set(self, ui): + pl = self.gui_footer_log_get() + self.master_frame.contents['footer'] = \ + (urwid.Pile([ urwid.Pile([pl]), urwid.Pile([ui]) ]), None) + self.sncli_loop.draw_screen() + + def gui_footer_input_get(self): + return self.master_frame.contents['footer'][0].contents[1][0] + + def gui_footer_focus_input(self): + self.master_frame.focus_position = 'footer' + self.master_frame.contents['footer'][0].focus_position = 1 + + def gui_body_clear(self): + self.master_frame.contents['body'] = ( None, None ) + self.sncli_loop.draw_screen() + + def gui_body_set(self, w): + self.master_frame.contents['body'] = ( w, None ) + self.gui_update_status_bar() + self.sncli_loop.draw_screen() + + def gui_body_get(self): + return self.master_frame.contents['body'][0] + + def gui_body_focus(self): + self.master_frame.focus_position = 'body' + + def log_timeout(self, loop, arg): + self.log_lock.acquire() + + self.log_alarms -= 1 + + if self.log_alarms == 0: + self.gui_footer_log_clear() + self.logs = [] + else: + self.logs.pop(0) + + log_pile = [] + for l in self.logs: + log_pile.append(urwid.AttrMap(urwid.Text(l), 'log')) + + self.gui_footer_log_set(log_pile) + + self.log_lock.release() + + def log(self, msg): + logging.debug(msg) + + if not self.do_gui: + if self.verbose: + print msg + return + + self.log_lock.acquire() + + self.log_alarms += 1 + self.logs.append(msg) + + if len(self.logs) > self.config.get_config('max_logs'): + self.log_alarms -= 1 + self.logs.pop(0) + + log_pile = [] + for l in self.logs: + log_pile.append(urwid.AttrMap(urwid.Text(l), 'log')) + + self.gui_footer_log_set(log_pile) + + self.sncli_loop.set_alarm_in( + int(self.config.get_config('log_timeout')), + self.log_timeout, None) + + self.log_lock.release() + + def gui_update_view(self): + if not self.do_gui: + return + + try: + cur_key = self.view_titles.note_list[self.view_titles.focus_position].note['key'] + except IndexError, e: + cur_key = None + pass + self.view_titles.update_note_list(self.view_titles.search_string) + self.view_titles.focus_note(cur_key) + + if self.gui_body_get().__class__ == view_note.ViewNote: + self.view_note.update_note_view() + + self.gui_update_status_bar() + + def gui_update_status_bar(self): + if self.status_bar != 'yes': + self.gui_header_clear() + else: + self.gui_header_set(self.gui_body_get().get_status_bar()) + + def gui_switch_frame_body(self, new_view, save_current_view=True): + if new_view == None: + if len(self.last_view) == 0: + # XXX verify all notes saved... + self.gui_stop() + else: + self.gui_body_set(self.last_view.pop()) + else: + if self.gui_body_get().__class__ != new_view.__class__: + if save_current_view: + self.last_view.append(self.gui_body_get()) + self.gui_body_set(new_view) + + def trash_note_callback(self, key, yes): + if not yes: + return + + # toggle the deleted flag + note = self.ndb.get_note(key) + self.ndb.set_note_deleted(key, 0 if note['deleted'] else 1) + + if self.gui_body_get().__class__ == view_titles.ViewTitles: + self.view_titles.update_note_title() + + self.gui_update_status_bar() + + def restore_note_callback(self, key, yes): + if not yes: + return + + # restore the contents of the old_note + self.log(u'Restoring version v{0} (key={1})'. + format(self.view_note.old_note['version'], key)) + self.ndb.set_note_content(key, self.view_note.old_note['content']) + + self.view_note.update_note_view() + self.gui_update_status_bar() + + def gui_yes_no_input(self, args, yes_no): + self.gui_footer_input_clear() + self.gui_body_focus() + self.master_frame.keypress = self.gui_frame_keypress + args[0](args[1], + True if yes_no in [ 'YES', 'Yes', 'yes', 'Y', 'y' ] + else False) + + def gui_search_input(self, args, search_string): + self.gui_footer_input_clear() + self.gui_body_focus() + self.master_frame.keypress = self.gui_frame_keypress + if search_string: + self.view_titles.update_note_list(search_string, args[0]) + self.gui_body_set(self.view_titles) + + def gui_version_input(self, args, version): + self.gui_footer_input_clear() + self.gui_body_focus() + self.master_frame.keypress = self.gui_frame_keypress + if version: + try: + # verify input is a number + int(version) + except ValueError, e: + self.log(u'ERROR: Invalid version value') + return + self.view_note.update_note_view(version=version) + self.gui_update_status_bar() + + def gui_tags_input(self, args, tags): + self.gui_footer_input_clear() + self.gui_body_focus() + self.master_frame.keypress = self.gui_frame_keypress + if tags != None: + if self.gui_body_get().__class__ == view_titles.ViewTitles: + note = self.view_titles.note_list[self.view_titles.focus_position].note + else: # self.gui_body_get().__class__ == view_note.ViewNote: + note = self.view_note.note + + self.ndb.set_note_tags(note['key'], tags) + + if self.gui_body_get().__class__ == view_titles.ViewTitles: + self.view_titles.update_note_title() + else: # self.gui_body_get().__class__ == view_note.ViewNote: + self.view_note.update_note_view() + + self.gui_update_status_bar() + + def gui_pipe_input(self, args, cmd): + self.gui_footer_input_clear() + self.gui_body_focus() + self.master_frame.keypress = self.gui_frame_keypress + if cmd != None: + if self.gui_body_get().__class__ == view_titles.ViewTitles: + note = self.view_titles.note_list[self.view_titles.focus_position].note + else: # self.gui_body_get().__class__ == view_note.ViewNote: + note = self.view_note.old_note if self.view_note.old_note \ + else self.view_note.note + args = shlex.split(cmd) + try: + self.gui_clear() + pipe = subprocess.Popen(cmd, stdin=subprocess.PIPE, shell=True) + pipe.communicate(note['content']) + pipe.stdin.close() + pipe.wait() + except OSError, e: + self.log(u'Pipe error: ' + str(e)) + finally: + self.gui_reset() + + def gui_frame_keypress(self, size, key): + + lb = self.gui_body_get() + + if key == self.config.get_keybind('quit'): + self.gui_switch_frame_body(None) + + elif key == self.config.get_keybind('help'): + self.gui_switch_frame_body(self.view_help) + + elif key == self.config.get_keybind('sync'): + self.ndb.last_sync = 0 + + elif key == self.config.get_keybind('view_log'): + self.view_log.update_log() + self.gui_switch_frame_body(self.view_log) + + elif key == self.config.get_keybind('down'): + if len(lb.body.positions()) <= 0: + return None + last = len(lb.body.positions()) + if lb.focus_position == (last - 1): + return None + lb.focus_position += 1 + lb.render(size) + + elif key == self.config.get_keybind('up'): + if len(lb.body.positions()) <= 0: + return None + if lb.focus_position == 0: + return None + lb.focus_position -= 1 + lb.render(size) + + elif key == self.config.get_keybind('page_down'): + if len(lb.body.positions()) <= 0: + return None + last = len(lb.body.positions()) + next_focus = lb.focus_position + size[1] + if next_focus >= last: + next_focus = last - 1 + lb.change_focus(size, next_focus, + offset_inset=0, + coming_from='above') + + elif key == self.config.get_keybind('page_up'): + if len(lb.body.positions()) <= 0: + return None + if 'bottom' in lb.ends_visible(size): + last = len(lb.body.positions()) + next_focus = last - size[1] - size[1] + else: + next_focus = lb.focus_position - size[1] + if next_focus < 0: + next_focus = 0 + lb.change_focus(size, next_focus, + offset_inset=0, + coming_from='below') + + elif key == self.config.get_keybind('half_page_down'): + if len(lb.body.positions()) <= 0: + return None + last = len(lb.body.positions()) + next_focus = lb.focus_position + (size[1] / 2) + if next_focus >= last: + next_focus = last - 1 + lb.change_focus(size, next_focus, + offset_inset=0, + coming_from='above') + + elif key == self.config.get_keybind('half_page_up'): + if len(lb.body.positions()) <= 0: + return None + if 'bottom' in lb.ends_visible(size): + last = len(lb.body.positions()) + next_focus = last - size[1] - (size[1] / 2) + else: + next_focus = lb.focus_position - (size[1] / 2) + if next_focus < 0: + next_focus = 0 + lb.change_focus(size, next_focus, + offset_inset=0, + coming_from='below') + + elif key == self.config.get_keybind('bottom'): + if len(lb.body.positions()) <= 0: + return None + lb.change_focus(size, (len(lb.body.positions()) - 1), + offset_inset=0, + coming_from='above') + + elif key == self.config.get_keybind('top'): + if len(lb.body.positions()) <= 0: + return None + lb.change_focus(size, 0, + offset_inset=0, + coming_from='below') + + elif key == self.config.get_keybind('view_next_note'): + if self.gui_body_get().__class__ != view_note.ViewNote: + return key + + if len(self.view_titles.body.positions()) <= 0: + return None + last = len(self.view_titles.body.positions()) + if self.view_titles.focus_position == (last - 1): + return None + self.view_titles.focus_position += 1 + lb.update_note_view( + 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('view_prev_note'): + if self.gui_body_get().__class__ != view_note.ViewNote: + return key + + if len(self.view_titles.body.positions()) <= 0: + return None + if self.view_titles.focus_position == 0: + return None + self.view_titles.focus_position -= 1 + lb.update_note_view( + 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('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 + + version = diff + (self.view_note.old_note['version'] + if self.view_note.old_note else + self.view_note.note['version']) + + lb.update_note_view(version=version) + + elif key == self.config.get_keybind('diff_version'): + if self.gui_body_get().__class__ != view_note.ViewNote: + return key + + if not self.view_note.old_note: + self.log(u'Already at latest version (key={0})'. + format(self.view_note.key)) + return None + + self.gui_clear() + self.exec_diff_on_note(self.view_note.note, + self.view_note.old_note) + self.gui_reset() + + elif key == self.config.get_keybind('restore_version'): + if self.gui_body_get().__class__ != view_note.ViewNote: + return key + + if not self.view_note.old_note: + self.log(u'Already at latest version (key={0})'. + format(self.view_note.key)) + return None + + self.gui_footer_input_set( + urwid.AttrMap( + user_input.UserInput( + self.config, + 'Restore v{0} (y/n): '.format(self.view_note.old_note['version']), + '', + self.gui_yes_no_input, + [ self.restore_note_callback, self.view_note.key ]), + 'user_input_bar')) + self.gui_footer_focus_input() + self.master_frame.keypress = self.gui_footer_input_get().keypress + + elif key == self.config.get_keybind('latest_version'): + if self.gui_body_get().__class__ != view_note.ViewNote: + return key + + lb.update_note_view(version=None) + + elif key == self.config.get_keybind('select_version'): + if self.gui_body_get().__class__ != view_note.ViewNote: + return key + + self.gui_footer_input_set( + urwid.AttrMap( + user_input.UserInput( + self.config, + key, + '', + self.gui_version_input, + None), + 'user_input_bar')) + self.gui_footer_focus_input() + self.master_frame.keypress = self.gui_footer_input_get().keypress + + elif key == self.config.get_keybind('status'): + if self.status_bar == 'yes': + self.status_bar = 'no' + else: + self.status_bar = self.config.get_config('status_bar') + + elif key == self.config.get_keybind('create_note'): + if self.gui_body_get().__class__ != view_titles.ViewTitles: + return key + + self.gui_clear() + content = self.exec_cmd_on_note(None) + self.gui_reset() + + if content: + self.log(u'New note created') + self.ndb.create_note(content) + self.gui_update_view() + + elif key == self.config.get_keybind('edit_note') or \ + key == self.config.get_keybind('view_note_ext') or \ + key == self.config.get_keybind('view_note_json'): + if self.gui_body_get().__class__ != view_titles.ViewTitles and \ + self.gui_body_get().__class__ != view_note.ViewNote: + return key + + if self.gui_body_get().__class__ == view_titles.ViewTitles: + if len(lb.body.positions()) <= 0: + return None + note = lb.note_list[lb.focus_position].note + else: # self.gui_body_get().__class__ == view_note.ViewNote: + if key == self.config.get_keybind('edit_note'): + note = lb.note + else: + note = lb.old_note if lb.old_note else lb.note + + self.gui_clear() + if key == self.config.get_keybind('edit_note'): + content = self.exec_cmd_on_note(note) + elif key == self.config.get_keybind('view_note_ext'): + content = self.exec_cmd_on_note(note, cmd=self.get_pager()) + else: # key == self.config.get_keybind('view_note_json') + content = self.exec_cmd_on_note(note, cmd=self.get_pager(), raw=True) + + self.gui_reset() + + if not content: + return None + + md5_old = md5.new(note['content']).digest() + md5_new = md5.new(content).digest() + + if md5_old != md5_new: + self.log(u'Note updated') + self.ndb.set_note_content(note['key'], content) + if self.gui_body_get().__class__ == view_titles.ViewTitles: + lb.update_note_title() + else: # self.gui_body_get().__class__ == view_note.ViewNote: + lb.update_note_view() + else: + self.log(u'Note unchanged') + + elif key == self.config.get_keybind('view_note'): + if self.gui_body_get().__class__ != view_titles.ViewTitles: + return key + + if len(lb.body.positions()) <= 0: + return None + self.view_note.update_note_view( + lb.note_list[lb.focus_position].note['key']) + self.gui_switch_frame_body(self.view_note) + + elif key == self.config.get_keybind('pipe_note'): + if self.gui_body_get().__class__ != view_titles.ViewTitles and \ + self.gui_body_get().__class__ != view_note.ViewNote: + return key + + if self.gui_body_get().__class__ == view_titles.ViewTitles: + if len(lb.body.positions()) <= 0: + return None + note = lb.note_list[lb.focus_position].note + else: # self.gui_body_get().__class__ == view_note.ViewNote: + note = lb.old_note if lb.old_note else lb.note + + self.gui_footer_input_set( + urwid.AttrMap( + user_input.UserInput( + self.config, + key, + '', + self.gui_pipe_input, + None), + 'user_input_bar')) + self.gui_footer_focus_input() + self.master_frame.keypress = self.gui_footer_input_get().keypress + + elif key == self.config.get_keybind('note_trash'): + if self.gui_body_get().__class__ != view_titles.ViewTitles and \ + self.gui_body_get().__class__ != view_note.ViewNote: + return key + + if self.gui_body_get().__class__ == view_titles.ViewTitles: + if len(lb.body.positions()) <= 0: + return None + note = lb.note_list[lb.focus_position].note + else: # self.gui_body_get().__class__ == view_note.ViewNote: + note = lb.note + + self.gui_footer_input_set( + urwid.AttrMap( + user_input.UserInput( + self.config, + '{0} (y/n): '.format('Untrash' if note['deleted'] else 'Trash'), + '', + self.gui_yes_no_input, + [ self.trash_note_callback, note['key'] ]), + 'user_input_bar')) + self.gui_footer_focus_input() + self.master_frame.keypress = self.gui_footer_input_get().keypress + + elif key == self.config.get_keybind('note_pin'): + if self.gui_body_get().__class__ != view_titles.ViewTitles and \ + self.gui_body_get().__class__ != view_note.ViewNote: + return key + + if self.gui_body_get().__class__ == view_titles.ViewTitles: + if len(lb.body.positions()) <= 0: + return None + note = lb.note_list[lb.focus_position].note + else: # self.gui_body_get().__class__ == view_note.ViewNote: + note = lb.note + + pin = 1 + if 'systemtags' in note: + if 'pinned' in note['systemtags']: pin = 0 + else: pin = 1 + + self.ndb.set_note_pinned(note['key'], pin) + + if self.gui_body_get().__class__ == view_titles.ViewTitles: + lb.update_note_title() + + elif key == self.config.get_keybind('note_markdown'): + if self.gui_body_get().__class__ != view_titles.ViewTitles and \ + self.gui_body_get().__class__ != view_note.ViewNote: + return key + + if self.gui_body_get().__class__ == view_titles.ViewTitles: + if len(lb.body.positions()) <= 0: + return None + note = lb.note_list[lb.focus_position].note + else: # self.gui_body_get().__class__ == view_note.ViewNote: + note = lb.note + + md = 1 + if 'systemtags' in note: + if 'markdown' in note['systemtags']: md = 0 + else: md = 1 + + self.ndb.set_note_markdown(note['key'], md) + + if self.gui_body_get().__class__ == view_titles.ViewTitles: + lb.update_note_title() + + elif key == self.config.get_keybind('note_tags'): + if self.gui_body_get().__class__ != view_titles.ViewTitles and \ + self.gui_body_get().__class__ != view_note.ViewNote: + return key + + if self.gui_body_get().__class__ == view_titles.ViewTitles: + if len(lb.body.positions()) <= 0: + return None + note = lb.note_list[lb.focus_position].note + else: # self.gui_body_get().__class__ == view_note.ViewNote: + note = lb.note + + self.gui_footer_input_set( + urwid.AttrMap( + user_input.UserInput( + self.config, + 'Tags: ', + '%s' % ','.join(note['tags']), + self.gui_tags_input, + None), + 'user_input_bar')) + self.gui_footer_focus_input() + self.master_frame.keypress = self.gui_footer_input_get().keypress + + elif key == self.config.get_keybind('search_gstyle') or \ + key == self.config.get_keybind('search_regex'): + if self.gui_body_get().__class__ != view_titles.ViewTitles: + return key + + self.gui_footer_input_set( + urwid.AttrMap( + user_input.UserInput( + self.config, + key, + '', + self.gui_search_input, + [ 'gstyle' if key == self.config.get_keybind('search_gstyle') + else 'regex' ]), + 'user_input_bar')) + self.gui_footer_focus_input() + self.master_frame.keypress = self.gui_footer_input_get().keypress + + elif key == self.config.get_keybind('clear_search'): + if self.gui_body_get().__class__ != view_titles.ViewTitles: + return key + + self.view_titles.update_note_list(None) + self.gui_body_set(self.view_titles) + + elif key == self.config.get_keybind('sort_date'): + if self.gui_body_get().__class__ != view_titles.ViewTitles: + return key + + self.view_titles.sort_note_list('date') + + elif key == self.config.get_keybind('sort_alpha'): + if self.gui_body_get().__class__ != view_titles.ViewTitles: + return key + + self.view_titles.sort_note_list('alpha') + + else: + return lb.keypress(size, key) + + self.gui_update_status_bar() + return None + + def gui_init_view(self, loop, view_note): + self.master_frame.keypress = self.gui_frame_keypress + self.gui_body_set(self.view_titles) + + if view_note: + # note that title view set first to prime the view stack + self.gui_switch_frame_body(self.view_note) + + self.thread_sync.start() + + def gui_clear(self): + self.sncli_loop.widget = urwid.Filler(urwid.Text(u'')) + self.sncli_loop.draw_screen() + + def gui_reset(self): + self.sncli_loop.widget = self.master_frame + self.sncli_loop.draw_screen() + + def gui_stop(self): + # don't exit if there are any notes not yet saved to the disk + if self.ndb.verify_all_saved(): + # clear the screen and exit the urwid run loop + self.gui_clear() + raise urwid.ExitMainLoop() + else: + self.log(u'WARNING: Not all notes saved to disk (wait for sync worker)') + + def gui(self, key): + + self.do_gui = True + + self.last_view = [] + self.status_bar = self.config.get_config('status_bar') + + self.log_alarms = 0 + self.log_lock = threading.Lock() + + self.thread_sync = threading.Thread(target=self.ndb.sync_worker, + args=[self.do_server_sync]) + self.thread_sync.setDaemon(True) + + self.view_titles = \ + view_titles.ViewTitles(self.config, + { + 'ndb' : self.ndb, + 'search_string' : None, + 'log' : self.log + }) + self.view_note = \ + view_note.ViewNote(self.config, + { + 'ndb' : self.ndb, + 'key' : key, # initial key to view or None + 'log' : self.log + }) + + self.view_log = view_log.ViewLog(self.config) + self.view_help = view_help.ViewHelp(self.config) + + palette = \ + [ + ('default', + self.config.get_color('default_fg'), + self.config.get_color('default_bg') ), + ('status_bar', + self.config.get_color('status_bar_fg'), + self.config.get_color('status_bar_bg') ), + ('log', + self.config.get_color('log_fg'), + self.config.get_color('log_bg') ), + ('user_input_bar', + self.config.get_color('user_input_bar_fg'), + self.config.get_color('user_input_bar_bg') ), + ('note_focus', + self.config.get_color('note_focus_fg'), + self.config.get_color('note_focus_bg') ), + ('note_title_day', + self.config.get_color('note_title_day_fg'), + self.config.get_color('note_title_day_bg') ), + ('note_title_week', + self.config.get_color('note_title_week_fg'), + self.config.get_color('note_title_week_bg') ), + ('note_title_month', + self.config.get_color('note_title_month_fg'), + self.config.get_color('note_title_month_bg') ), + ('note_title_year', + self.config.get_color('note_title_year_fg'), + self.config.get_color('note_title_year_bg') ), + ('note_title_ancient', + self.config.get_color('note_title_ancient_fg'), + self.config.get_color('note_title_ancient_bg') ), + ('note_date', + self.config.get_color('note_date_fg'), + self.config.get_color('note_date_bg') ), + ('note_flags', + self.config.get_color('note_flags_fg'), + self.config.get_color('note_flags_bg') ), + ('note_tags', + self.config.get_color('note_tags_fg'), + self.config.get_color('note_tags_bg') ), + ('note_content', + self.config.get_color('note_content_fg'), + self.config.get_color('note_content_bg') ), + ('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') ), + ('help_header', + self.config.get_color('help_header_fg'), + self.config.get_color('help_header_bg') ), + ('help_config', + self.config.get_color('help_config_fg'), + self.config.get_color('help_config_bg') ), + ('help_value', + self.config.get_color('help_value_fg'), + self.config.get_color('help_value_bg') ), + ('help_descr', + self.config.get_color('help_descr_fg'), + self.config.get_color('help_descr_bg') ) + ] + + self.master_frame = urwid.Frame(body=urwid.Filler(urwid.Text(u'')), + header=None, + footer=urwid.Pile([ urwid.Pile([]), + urwid.Pile([]) ]), + focus_part='body') + + self.sncli_loop = urwid.MainLoop(self.master_frame, + palette, + handle_mouse=False) + + self.sncli_loop.set_alarm_in(0, self.gui_init_view, + True if key else False) + + self.sncli_loop.run() + + def cli_list_notes(self, regex, search_string): + + note_list, match_regex, all_notes_cnt = \ + self.ndb.filter_notes( + search_string, + search_mode='regex' if regex else 'gstyle') + for n in note_list: + flags = utils.get_note_flags(n.note) + print n.key + \ + u' [' + flags + u'] ' + \ + utils.get_note_title(n.note) + + def cli_note_dump(self, key): + + note = self.ndb.get_note(key) + if not note: + self.log(u'ERROR: Key does not exist') + return + + w = 60 + sep = u'+' + u'-'*(w+2) + u'+' + t = time.localtime(float(note['modifydate'])) + mod_time = time.strftime('%a, %d %b %Y %H:%M:%S', t) + title = utils.get_note_title(note) + flags = utils.get_note_flags(note) + tags = utils.get_note_tags(note) + + print sep + print (u'| {:<' + str(w) + u'} |').format((u' Title: ' + title)[:w]) + print (u'| {:<' + str(w) + u'} |').format((u' Key: ' + note['key'])[:w]) + print (u'| {:<' + str(w) + u'} |').format((u' Date: ' + mod_time)[:w]) + print (u'| {:<' + str(w) + u'} |').format((u' Tags: ' + tags)[:w]) + print (u'| {:<' + str(w) + u'} |').format((u' Version: v' + str(note['version']))[:w]) + print (u'| {:<' + str(w) + u'} |').format((u' Flags: [' + flags + u']')[:w]) + if utils.note_published(note) and 'publishkey' in note: + print (u'| {:<' + str(w) + u'} |').format((u'Published: http://simp.ly/publish/' + note['publishkey'])[:w]) + else: + print (u'| {:<' + str(w) + u'} |').format((u'Published: n/a')[:w]) + print sep + print note['content'] + + def cli_dump_notes(self, regex, search_string): + + note_list, match_regex, all_notes_cnt = \ + self.ndb.filter_notes( + search_string, + search_mode='regex' if regex else 'gstyle') + for n in note_list: + self.cli_note_dump(n.key) + + def cli_note_create(self, from_stdin, title): + + if from_stdin: + content = ''.join(sys.stdin) + else: + content = self.exec_cmd_on_note(None) + + if title: + content = title + '\n\n' + content if content else u'' + + if content: + self.log(u'New note created') + self.ndb.create_note(content) + self.sync_notes() + + def cli_note_edit(self, key): + + note = self.ndb.get_note(key) + if not note: + self.log(u'ERROR: Key does not exist') + return + + content = self.exec_cmd_on_note(note) + if not content: + return + + md5_old = md5.new(note['content']).digest() + md5_new = md5.new(content).digest() + + if md5_old != md5_new: + self.log(u'Note updated') + self.ndb.set_note_content(note['key'], content) + self.sync_notes() + else: + self.log(u'Note unchanged') + + def cli_note_trash(self, key, trash): + + note = self.ndb.get_note(key) + if not note: + self.log(u'ERROR: Key does not exist') + return + + self.ndb.set_note_deleted(key, trash) + self.sync_notes() + + def cli_note_pin(self, key, pin): + + note = self.ndb.get_note(key) + if not note: + self.log(u'ERROR: Key does not exist') + return + + self.ndb.set_note_pinned(key, pin) + self.sync_notes() + + def cli_note_markdown(self, key, markdown): + + note = self.ndb.get_note(key) + if not note: + self.log(u'ERROR: Key does not exist') + return + + self.ndb.set_note_markdown(key, markdown) + self.sync_notes() + + +def SIGINT_handler(signum, frame): + print u'\nSignal caught, bye!' + sys.exit(1) + +signal.signal(signal.SIGINT, SIGINT_handler) + +def usage(): + print u''' +Usage: + sncli [OPTIONS] [COMMAND] [COMMAND_ARGS] + + OPTIONS: + -h, --help - usage help + -v, --verbose - verbose output (cli mode) + -n, --nosync - don't perform a server sync + -r, --regex - search string is a regular expression + -k <key>, --key=<key> - note key + -t <title>, --title=<title> - title of note for create (cli mode) + + COMMANDS: + <none> - console gui mode when no command specified + sync - perform a full sync with the server + list [search_string] - list notes (refined with search string) + dump [search_string] - dump notes (refined with search string) + create [-] - create a note ('-' content from stdin) + dump - dump a note (specified by <key>) + edit - edit a note (specified by <key>) + < trash | untrash > - trash/untrash a note (specified by <key>) + < pin | unpin > - pin/unpin a note (specified by <key>) + < markdown | unmarkdown > - markdown/unmarkdown a note (specified by <key>) +''' + sys.exit(0) + +def main(argv): + verbose = False + sync = True + regex = False + key = None + title = None + + try: + opts, args = getopt.getopt(argv, + 'hvnrk:t:', + [ 'help', 'verbose' 'nosync', 'regex', 'key=', 'title=' ]) + except: + usage() + + for opt, arg in opts: + if opt in [ '-h', '--help']: + usage() + elif opt in [ '-v', '--verbose']: + verbose = True + elif opt in [ '-n', '--nosync']: + sync = False + elif opt in [ '-r', '--regex']: + regex = True + elif opt in [ '-k', '--key']: + key = arg + elif opt in [ '-t', '--title']: + title = arg + else: + print u'ERROR: Unhandled option' + usage() + + if not args: + sncli(sync).gui(key) + return + + def sncli_start(sync, verbose): + sn = sncli(sync, verbose) + if sync: sn.sync_notes() + return sn + + if args[0] == 'sync': + sn = sncli_start(True, verbose) + + elif args[0] == 'list': + + sn = sncli_start(sync, verbose) + sn.cli_list_notes(regex, ' '.join(args[1:])) + + elif args[0] == 'dump': + + sn = sncli_start(sync, verbose) + if key: + sn.cli_note_dump(key) + else: + sn.cli_dump_notes(regex, ' '.join(args[1:])) + + elif args[0] == 'create': + + if len(args) == 1: + sn = sncli_start(sync, verbose) + sn.cli_note_create(False, title) + elif len(args) == 2 and args[1] == '-': + sn = sncli_start(sync, verbose) + sn.cli_note_create(True, title) + else: + usage() + + elif args[0] == 'edit': + + if not key: + usage() + + sn = sncli_start(sync, verbose) + sn.cli_note_edit(key) + + elif args[0] == 'trash' or args[0] == 'untrash': + + if not key: + usage() + + sn = sncli_start(sync, verbose) + sn.cli_note_trash(key, 1 if args[0] == 'trash' else 0) + + elif args[0] == 'pin' or args[0] == 'unpin': + + if not key: + usage() + + sn = sncli_start(sync, verbose) + sn.cli_note_pin(key, 1 if args[0] == 'pin' else 0) + + elif args[0] == 'markdown' or args[0] == 'unmarkdown': + + if not key: + usage() + + sn = sncli_start(sync, verbose) + sn.cli_note_markdown(key, 1 if args[0] == 'markdown' else 0) + + else: + usage() + diff --git a/simplenote_cli/temp.py b/simplenote_cli/temp.py @@ -0,0 +1,40 @@ + +# Copyright (c) 2014 Eric Davis +# Licensed under the MIT License + +import os, json, tempfile + +def tempfile_create(note, raw=False): + if raw: + # dump the raw json of the note + tf = tempfile.NamedTemporaryFile(suffix='.json', delete=False) + json.dump(note, tf, indent=2) + tf.flush() + else: + ext = '.txt' + if note and \ + 'systemtags' in note and \ + 'markdown' in note['systemtags']: + ext = '.mkd' + tf = tempfile.NamedTemporaryFile(suffix=ext, delete=False) + if note: + tf.write(note['content']) + tf.flush() + return tf + +def tempfile_delete(tf): + if tf: + os.unlink(tf.name) + +def tempfile_name(tf): + if tf: + return tf.name + return '' + +def tempfile_content(tf): + tf.seek(0) + lines = [] + for line in tf: + lines.append(line) + return lines + diff --git a/simplenote_cli/user_input.py b/simplenote_cli/user_input.py @@ -0,0 +1,26 @@ + +# Copyright (c) 2014 Eric Davis +# Licensed under the MIT License + +import urwid + +class UserInput(urwid.Edit): + + def __init__(self, config, caption, edit_text, callback_func, args): + self.config = config + self.callback_func = callback_func + self.callback_func_args = args + super(UserInput, self).__init__(caption=caption, + edit_text=edit_text, + wrap='clip') + + def keypress(self, size, key): + size = (size[0],) # if this isn't here then urwid freaks out... + if key == 'esc': + self.callback_func(self.callback_func_args, None) + elif key == 'enter': + self.callback_func(self.callback_func_args, self.edit_text) + else: + return super(UserInput, self).keypress(size, key) + return None + diff --git a/simplenote_cli/utils.py b/simplenote_cli/utils.py @@ -0,0 +1,172 @@ + +# Copyright (c) 2014 Eric Davis +# This file is *heavily* modified from nvpy. + +# nvPY: cross-platform note-taking app with simplenote syncing +# copyright 2012 by Charl P. Botha <cpbotha@vxlabs.com> +# new BSD license + +import datetime, random, re + +# first line with non-whitespace should be the title +note_title_re = re.compile('\s*(.*)\n?') + +def generate_random_key(): + """Generate random 30 digit (15 byte) hex string. + + stackoverflow question 2782229 + """ + return '%030x' % (random.randrange(256**15),) + +def get_note_tags(note): + 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 + +# Returns a fixed length string: +# 'X' - needs sync +# 'T' - trashed +# '*' - pinned +# 'S' - published/shared +# 'm' - markdown +def get_note_flags(note): + flags = '' + flags += u'X' if float(note['modifydate']) > float(note['syncdate']) 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' ' + flags += u'm' if 'markdown' in note['systemtags'] else u' ' + else: + flags += ' ' + return flags + +def get_note_title(note): + mo = note_title_re.match(note.get('content', '')) + if mo: + return mo.groups()[0] + else: + return '' + +def get_note_title_file(note): + mo = note_title_re.match(note.get('content', '')) + if mo: + fn = mo.groups()[0] + fn = fn.replace(' ', '_') + fn = fn.replace('/', '_') + if not fn: + return '' + + if isinstance(fn, str): + fn = unicode(fn, 'utf-8') + else: + fn = unicode(fn) + + if note_markdown(note): + fn += '.mkdn' + else: + fn += '.txt' + + return fn + else: + return '' + +def human_date(timestamp): + """ + Given a timestamp, return pretty human format representation. + + For example, if timestamp is: + * today, then do "15:11" + * else if it is this year, then do "Aug 4" + * else do "Dec 11, 2011" + """ + + # this will also give us timestamp in the local timezone + dt = datetime.datetime.fromtimestamp(timestamp) + # this returns localtime + now = datetime.datetime.now() + + if dt.date() == now.date(): + # today: 15:11 + return dt.strftime('%H:%M') + + elif dt.year == now.year: + # this year: Aug 6 + # format code %d unfortunately 0-pads + return dt.strftime('%b') + ' ' + str(dt.day) + + else: + # not today or this year, so we do "Dec 11, 2011" + return '%s %d, %d' % (dt.strftime('%b'), dt.day, dt.year) + +def note_published(n): + asystags = n.get('systemtags', 0) + if not asystags: + return 0 + return 1 if 'published' in asystags else 0 + +def note_pinned(n): + asystags = n.get('systemtags', 0) + if not asystags: + return 0 + return 1 if 'pinned' in asystags else 0 + +def note_markdown(n): + asystags = n.get('systemtags', 0) + if not asystags: + return 0 + return 1 if 'markdown' in asystags else 0 + +tags_illegal_chars = re.compile(r'[\s]') +def sanitise_tags(tags): + """ + Given a string containing comma-separated tags, sanitise and return a list of string tags. + + The simplenote API doesn't allow for spaces, so we strip those out. + + @param tags: Comma-separated tags, one string. + @returns: List of strings. + """ + + # hack out all kinds of whitespace, then split on , + # if you run into more illegal characters (simplenote does not want to sync them) + # add them to the regular expression above. + illegals_removed = tags_illegal_chars.sub('', tags) + if len(illegals_removed) == 0: + # special case for empty string '' + # split turns that into [''], which is not valid + return [] + + else: + return illegals_removed.split(',') + +def sort_by_title_pinned(a, b): + if note_pinned(a.note) and not note_pinned(b.note): + return -1 + elif not note_pinned(a.note) and note_pinned(b.note): + return 1 + else: + return cmp(get_note_title(a.note), get_note_title(b.note)) + +def sort_by_modify_date_pinned(a, b): + if note_pinned(a.note) and not note_pinned(b.note): + return 1 + elif not note_pinned(a.note) and note_pinned(b.note): + return -1 + else: + return cmp(float(a.note.get('modifydate', 0)), float(b.note.get('modifydate', 0))) + +class KeyValueObject: + """Store key=value pairs in this object and retrieve with o.key. + + You should also be able to do MiscObject(**your_dict) for the same effect. + """ + + def __init__(self, **kwargs): + self.__dict__.update(kwargs) + diff --git a/simplenote_cli/view_help.py b/simplenote_cli/view_help.py @@ -0,0 +1,128 @@ + +# Copyright (c) 2014 Eric Davis +# Licensed under the MIT License + +import re, urwid + +class ViewHelp(urwid.ListBox): + + def __init__(self, config): + self.config = config + + self.descr_width = 26 + self.config_width = 29 + + lines = [] + lines.extend(self.create_kb_help_lines(u'Keybinds Common', 'common')) + lines.extend(self.create_kb_help_lines(u'Keybinds Note List', 'titles')) + lines.extend(self.create_kb_help_lines(u'Keybinds Note Content', 'notes')) + lines.extend(self.create_config_help_lines()) + lines.extend(self.create_color_help_lines()) + lines.append(urwid.Text(('help_header', u''))) + + super(ViewHelp, self).__init__(urwid.SimpleFocusListWalker(lines)) + + def get_status_bar(self): + cur = -1 + total = 0 + if len(self.body.positions()) > 0: + cur = self.focus_position + total = len(self.body.positions()) + + status_title = \ + urwid.AttrMap(urwid.Text(u'Help', + wrap='clip'), + 'status_bar') + status_index = \ + ('pack', urwid.AttrMap(urwid.Text(u' ' + + str(cur + 1) + + u'/' + + str(total)), + 'status_bar')) + return \ + urwid.AttrMap(urwid.Columns([ status_title, status_index ]), + 'status_bar') + + def create_kb_help_lines(self, header, use): + lines = [ urwid.AttrMap(urwid.Text(u''), + 'help_header', + 'help_focus') ] + lines.append(urwid.AttrMap(urwid.Text(u' ' + header), + 'help_header', + 'help_focus')) + for c in self.config.keybinds: + if use not in self.config.get_keybind_use(c): + continue + lines.append( + urwid.AttrMap(urwid.AttrMap( + urwid.Text( + [ + ('help_descr', ('{:>' + str(self.descr_width) + '} ').format(self.config.get_keybind_descr(c))), + ('help_config', ('{:>' + str(self.config_width) + '} ').format(u'kb_' + c)), + ('help_value', u"'" + self.config.get_keybind(c) + u"'") + ] + ), + attr_map = None, + focus_map = { + 'help_value' : 'help_focus', + 'help_config' : 'help_focus', + 'help_descr' : 'help_focus' + } + ), 'default', 'help_focus')) + return lines + + def create_config_help_lines(self): + lines = [ urwid.AttrMap(urwid.Text(u''), + 'help_header', + 'help_focus') ] + lines.append(urwid.AttrMap(urwid.Text(u' Configuration'), + 'help_header', + 'help_focus')) + for c in self.config.configs: + if c in [ 'sn_username', 'sn_password' ]: continue + lines.append( + urwid.AttrMap(urwid.AttrMap( + urwid.Text( + [ + ('help_descr', ('{:>' + str(self.descr_width) + '} ').format(self.config.get_config_descr(c))), + ('help_config', ('{:>' + str(self.config_width) + '} ').format(u'cfg_' + c)), + ('help_value', u"'" + self.config.get_config(c) + u"'") + ] + ), + attr_map = None, + focus_map = { + 'help_value' : 'help_focus', + 'help_config' : 'help_focus', + 'help_descr' : 'help_focus' + } + ), 'default', 'help_focus')) + return lines + + def create_color_help_lines(self): + lines = [ urwid.AttrMap(urwid.Text(u''), + 'help_header', + 'help_focus') ] + lines.append(urwid.AttrMap(urwid.Text(u' Colors'), + 'help_header', + 'help_focus')) + fmap = {} + for c in self.config.colors: + fmap[re.search('^(.*)(_fg|_bg)$', c).group(1)] = 'help_focus' + for c in self.config.colors: + lines.append( + urwid.AttrMap(urwid.AttrMap( + urwid.Text( + [ + ('help_descr', ('{:>' + str(self.descr_width) + '} ').format(self.config.get_color_descr(c))), + ('help_config', ('{:>' + str(self.config_width) + '} ').format(u'clr_' + c)), + (re.search('^(.*)(_fg|_bg)$', c).group(1), u"'" + self.config.get_color(c) + u"'") + ] + ), + attr_map = None, + focus_map = fmap + ), 'default', 'help_focus')) + return lines + + def keypress(self, size, key): + return key + diff --git a/simplenote_cli/view_log.py b/simplenote_cli/view_log.py @@ -0,0 +1,50 @@ + +# Copyright (c) 2014 Eric Davis +# Licensed under the MIT License + +import urwid + +class ViewLog(urwid.ListBox): + + def __init__(self, config): + self.config = config + super(ViewLog, self).__init__(urwid.SimpleFocusListWalker([])) + + def update_log(self): + lines = [] + f = open(self.config.logfile) + for line in f: + lines.append( + urwid.AttrMap(urwid.Text(line.rstrip()), + 'note_content', + 'note_content_focus')) + f.close() + if self.config.get_config('log_reversed') == 'yes': + lines.reverse() + self.body[:] = urwid.SimpleFocusListWalker(lines) + self.focus_position = 0 + + def get_status_bar(self): + cur = -1 + total = 0 + if len(self.body.positions()) > 0: + cur = self.focus_position + total = len(self.body.positions()) + + status_title = \ + urwid.AttrMap(urwid.Text(u'Sync Log', + wrap='clip'), + 'status_bar') + status_index = \ + ('pack', urwid.AttrMap(urwid.Text(u' ' + + str(cur + 1) + + u'/' + + str(total)), + 'status_bar')) + return \ + urwid.AttrMap(urwid.Columns([ status_title, status_index ]), + 'status_bar') + + def keypress(self, size, key): + return key + diff --git a/simplenote_cli/view_note.py b/simplenote_cli/view_note.py @@ -0,0 +1,172 @@ + +# Copyright (c) 2014 Eric Davis +# Licensed under the MIT License + +import time, urwid +import utils + +class ViewNote(urwid.ListBox): + + def __init__(self, config, args): + self.config = config + self.ndb = args['ndb'] + self.key = args['key'] + self.log = args['log'] + self.note = self.ndb.get_note(self.key) if self.key else None + self.old_note = None + self.tabstop = int(self.config.get_config('tabstop')) + super(ViewNote, self).__init__( + urwid.SimpleFocusListWalker(self.get_note_content_as_list())) + + def get_note_content_as_list(self): + lines = [] + if not self.key: + return lines + 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_view(self, key=None, version=None): + if key: # setting a new note + self.key = key + self.note = self.ndb.get_note(self.key) + self.old_note = None + + if self.key and version: + # verify version is within range + if int(version) <= 0 or int(version) >= self.note['version'] + 1: + self.log(u'Version v{0} is unavailable (key={1})'. + format(version, self.key)) + return + + if (not version and self.old_note) or \ + (self.key and version and version == self.note['version']): + self.log(u'Displaying latest version v{0} of note (key={1})'. + format(self.note['version'], self.key)) + self.old_note = None + elif self.key and version: + # get a previous version of the note + self.log(u'Fetching version v{0} of note (key={1})'. + format(version, self.key)) + version_note = self.ndb.get_note_version(self.key, version) + if not version_note: + self.log(u'Failed to get version v{0} of note (key={1})'. + format(version, self.key)) + # don't do anything, keep current note/version + else: + self.old_note = version_note + + self.body[:] = \ + urwid.SimpleFocusListWalker(self.get_note_content_as_list()) + self.focus_position = 0 + + def get_status_bar(self): + if not self.key: + return \ + urwid.AttrMap(urwid.Text(u'No note...'), + 'status_bar') + + cur = -1 + total = 0 + if len(self.body.positions()) > 0: + cur = self.focus_position + total = len(self.body.positions()) + + if self.old_note: + t = time.localtime(float(self.old_note['versiondate'])) + title = utils.get_note_title(self.old_note) + version = self.old_note['version'] + else: + t = time.localtime(float(self.note['modifydate'])) + title = utils.get_note_title(self.note) + flags = utils.get_note_flags(self.note) + tags = utils.get_note_tags(self.note) + version = self.note['version'] + + mod_time = time.strftime(u'Date: %a, %d %b %Y %H:%M:%S', t) + + status_title = \ + urwid.AttrMap(urwid.Text(u'Title: ' + + title, + wrap='clip'), + 'status_bar') + + status_key_index = \ + ('pack', urwid.AttrMap(urwid.Text(u' [' + + self.key + + u'] ' + + str(cur + 1) + + u'/' + + str(total)), + 'status_bar')) + + status_date = \ + urwid.AttrMap(urwid.Text(mod_time, + wrap='clip'), + 'status_bar') + + if self.old_note: + status_tags_flags = \ + ('pack', urwid.AttrMap(urwid.Text(u'[OLD:v' + + str(version) + + u']'), + 'status_bar')) + else: + status_tags_flags = \ + ('pack', urwid.AttrMap(urwid.Text(u'[' + + tags + + u'] [v' + + str(version) + + u'] [' + + flags + + u']'), + 'status_bar')) + + pile_top = urwid.Columns([ status_title, status_key_index ]) + pile_bottom = urwid.Columns([ status_date, status_tags_flags ]) + + if self.old_note or \ + not (utils.note_published(self.note) and 'publishkey' in self.note): + return urwid.AttrMap(urwid.Pile([ pile_top, pile_bottom ]), + 'status_bar') + + pile_publish = \ + urwid.AttrMap(urwid.Text(u'Published: http://simp.ly/publish/' + + self.note['publishkey']), + 'status_bar') + return \ + urwid.AttrMap(urwid.Pile([ pile_top, pile_bottom, pile_publish ]), + 'status_bar') + + def keypress(self, size, key): + if key == self.config.get_keybind('tabstop2'): + self.tabstop = 2 + self.body[:] = \ + urwid.SimpleFocusListWalker(self.get_note_content_as_list()) + + elif key == self.config.get_keybind('tabstop4'): + self.tabstop = 4 + self.body[:] = \ + urwid.SimpleFocusListWalker(self.get_note_content_as_list()) + + elif key == self.config.get_keybind('tabstop8'): + self.tabstop = 8 + self.body[:] = \ + urwid.SimpleFocusListWalker(self.get_note_content_as_list()) + + else: + return key + + return None + diff --git a/simplenote_cli/view_titles.py b/simplenote_cli/view_titles.py @@ -0,0 +1,188 @@ + +# Copyright (c) 2014 Eric Davis +# Licensed under the MIT License + +import re, time, datetime, urwid, subprocess +import utils, view_note + +class ViewTitles(urwid.ListBox): + + def __init__(self, config, args): + self.config = config + self.ndb = args['ndb'] + self.search_string = args['search_string'] + self.log = args['log'] + self.note_list, self.match_regex, self.all_notes_cnt = \ + self.ndb.filter_notes(self.search_string) + super(ViewTitles, self).__init__( + urwid.SimpleFocusListWalker(self.get_note_titles())) + + def update_note_list(self, search_string, search_mode='gstyle'): + self.search_string = search_string + self.note_list, self.match_regex, self.all_notes_cnt = \ + self.ndb.filter_notes(self.search_string, search_mode) + self.body[:] = \ + urwid.SimpleFocusListWalker(self.get_note_titles()) + if len(self.note_list) == 0: + self.log(u'No notes found!') + else: + self.focus_position = 0 + + def sort_note_list(self, sort_mode): + self.ndb.filtered_notes_sort(self.note_list, sort_mode) + self.body[:] = \ + urwid.SimpleFocusListWalker(self.get_note_titles()) + + def format_title(self, note): + """ + Various formatting tags are supported for dynamically building + the title string. Each of these formatting tags supports a width + specifier (decimal) and a left justification (-) like that + supported by printf. + + %F -- flags + %T -- tags + %D -- date + %N -- note title + """ + + t = time.localtime(float(note['modifydate'])) + mod_time = time.strftime(self.config.get_config('format_strftime'), t) + title = utils.get_note_title(note) + flags = utils.get_note_flags(note) + tags = utils.get_note_tags(note) + + # get the age of the note + dt = datetime.datetime.fromtimestamp(time.mktime(t)) + if dt > datetime.datetime.now() - datetime.timedelta(days=1): + note_age = 'd' # less than a day old + elif dt > datetime.datetime.now() - datetime.timedelta(weeks=1): + note_age = 'w' # less than a week old + elif dt > datetime.datetime.now() - datetime.timedelta(weeks=4): + note_age = 'm' # less than a month old + elif dt > datetime.datetime.now() - datetime.timedelta(weeks=52): + note_age = 'y' # less than a year old + else: + note_age = 'a' # ancient + + def recursive_format(title_format): + if not title_format: + return None + fmt = re.search("^(.*)%([-]*)([0-9]*)([FDTN])(.*)$", title_format) + if not fmt: + m = ('pack', urwid.AttrMap(urwid.Text(title_format), + 'default')) + l_fmt = None + r_fmt = None + else: + l = fmt.group(1) if fmt.group(1) else None + m = None + r = fmt.group(5) if fmt.group(5) else None + align = 'left' if fmt.group(2) == '-' else 'right' + width = int(fmt.group(3)) if fmt.group(3) else 'pack' + if fmt.group(4) == 'F': + m = (width, urwid.AttrMap(urwid.Text(flags, + align=align, + wrap='clip'), + 'note_flags')) + elif fmt.group(4) == 'D': + m = (width, urwid.AttrMap(urwid.Text(mod_time, + align=align, + wrap='clip'), + 'note_date')) + elif fmt.group(4) == 'T': + m = (width, urwid.AttrMap(urwid.Text(tags, + align=align, + wrap='clip'), + 'note_tags')) + elif fmt.group(4) == 'N': + if note_age == 'd': attr = 'note_title_day' + elif note_age == 'w': attr = 'note_title_week' + elif note_age == 'm': attr = 'note_title_month' + elif note_age == 'y': attr = 'note_title_year' + elif note_age == 'a': attr = 'note_title_ancient' + if width != 'pack': + m = (width, urwid.AttrMap(urwid.Text(title, + align=align, + wrap='clip'), + attr)) + else: + m = urwid.AttrMap(urwid.Text(title, + align=align, + wrap='clip'), + attr) + l_fmt = recursive_format(l) + r_fmt = recursive_format(r) + + tmp = [] + if l_fmt: tmp.extend(l_fmt) + tmp.append(m) + if r_fmt: tmp.extend(r_fmt) + return tmp + + # convert the format string into the actual note title line + 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.note_list: + lines.append(self.get_note_title(n.note)) + return lines + + def get_status_bar(self): + cur = -1 + total = 0 + if len(self.body.positions()) > 0: + cur = self.focus_position + total = len(self.body.positions()) + + hdr = u'Simplenote' + if self.search_string != None: + hdr += ' - Search: ' + self.search_string + + status_title = \ + urwid.AttrMap(urwid.Text(hdr, + wrap='clip'), + 'status_bar') + status_index = \ + ('pack', urwid.AttrMap(urwid.Text(u' ' + + str(cur + 1) + + u'/' + + str(total)), + 'status_bar')) + return \ + urwid.AttrMap(urwid.Columns([ status_title, status_index ]), + 'status_bar') + + def update_note_title(self, key=None): + if not key: + self.body[self.focus_position] = \ + self.get_note_title(self.note_list[self.focus_position].note) + else: + for i in xrange(len(self.note_list)): + if self.note_list[i].note['key'] == key: + self.body[i] = self.get_note_title(self.note_list[i].note) + + def focus_note(self, key): + for i in xrange(len(self.note_list)): + if 'key' in self.note_list[i].note and \ + self.note_list[i].note['key'] == key: + self.focus_position = i + + def keypress(self, size, key): + return key + diff --git a/sncli b/sncli @@ -1,6 +1,33 @@ #!/usr/bin/env python2 -import sys, sncli +# +# ** The MIT License ** +# +# Copyright (c) 2014 Eric Davis (edavis@insanum.com) +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +# Dude... just buy me a beer. :-) +# + +import sys +from simplenote_cli import sncli if __name__ == '__main__': sncli.main(sys.argv[1:]) diff --git a/sncli.py b/sncli.py @@ -1,1195 +0,0 @@ - -import os, sys, getopt, re, signal, time, datetime, shlex, md5 -import subprocess, thread, threading, logging -import copy, json, urwid, datetime -import view_titles, view_note, view_help, view_log, user_input -import utils, temp -from config import Config -from simplenote import Simplenote -from notes_db import NotesDB, ReadError, WriteError -from logging.handlers import RotatingFileHandler - -class sncli: - - def __init__(self, do_server_sync, verbose=False): - self.config = Config() - self.do_server_sync = do_server_sync - self.verbose = verbose - self.do_gui = False - - if not os.path.exists(self.config.get_config('db_path')): - os.mkdir(self.config.get_config('db_path')) - - # configure the logging module - self.logfile = os.path.join(self.config.get_config('db_path'), 'sncli.log') - self.loghandler = RotatingFileHandler(self.logfile, maxBytes=100000, backupCount=1) - self.loghandler.setLevel(logging.DEBUG) - self.loghandler.setFormatter(logging.Formatter(fmt='%(asctime)s [%(levelname)s] %(message)s')) - self.logger = logging.getLogger() - self.logger.setLevel(logging.DEBUG) - self.logger.addHandler(self.loghandler) - self.config.logfile = self.logfile - - logging.debug('sncli logging initialized') - - self.logs = [] - - try: - self.ndb = NotesDB(self.config, self.log, self.gui_update_view) - except Exception, e: - self.log(str(e)) - sys.exit(1) - - def sync_notes(self): - self.ndb.sync_now(self.do_server_sync) - - def get_editor(self): - editor = self.config.get_config('editor') - if not editor and os.environ['EDITOR']: - editor = os.environ['EDITOR'] - if not editor: - self.log(u'No editor configured!') - return None - return editor - - def get_pager(self): - pager = self.config.get_config('pager') - if not pager and os.environ['PAGER']: - pager = os.environ['PAGER'] - if not pager: - self.log(u'No pager configured!') - return None - return pager - - def get_diff(self): - diff = self.config.get_config('diff') - if not diff: - self.log(u'No diff command configured!') - return None - return diff - - def exec_cmd_on_note(self, note, cmd=None, raw=False): - - if not cmd: - cmd = self.get_editor() - if not cmd: - return None - - tf = temp.tempfile_create(note if note else None, raw=raw) - - try: - subprocess.check_call(cmd + u' ' + temp.tempfile_name(tf), shell=True) - except Exception, e: - self.log(u'Command error: ' + str(e)) - temp.tempfile_delete(tf) - return None - - content = None - if not raw: - content = ''.join(temp.tempfile_content(tf)) - if not content or content == u'\n': - content = None - - temp.tempfile_delete(tf) - return content - - def exec_diff_on_note(self, note, old_note): - - diff = self.get_diff() - if not diff: - return None - - pager = self.get_pager() - if not pager: - return None - - ltf = temp.tempfile_create(note) - otf = temp.tempfile_create(old_note) - out = temp.tempfile_create(None) - - try: - subprocess.call(diff + u' ' + - temp.tempfile_name(ltf) + u' ' + - temp.tempfile_name(otf) + u' > ' + - temp.tempfile_name(out), - shell=True) - subprocess.check_call(pager + u' ' + - temp.tempfile_name(out), - shell=True) - except Exception, e: - self.log(u'Command error: ' + str(e)) - temp.tempfile_delete(ltf) - temp.tempfile_delete(otf) - temp.tempfile_delete(out) - return None - - temp.tempfile_delete(ltf) - temp.tempfile_delete(otf) - temp.tempfile_delete(out) - return None - - def gui_header_clear(self): - self.master_frame.contents['header'] = ( None, None ) - self.sncli_loop.draw_screen() - - def gui_header_set(self, w): - self.master_frame.contents['header'] = ( w, None ) - self.sncli_loop.draw_screen() - - def gui_header_get(self): - return self.master_frame.contents['header'][0] - - def gui_header_focus(self): - self.master_frame.focus_position = 'header' - - def gui_footer_log_clear(self): - ui = self.gui_footer_input_get() - self.master_frame.contents['footer'] = \ - (urwid.Pile([ urwid.Pile([]), urwid.Pile([ui]) ]), None) - self.sncli_loop.draw_screen() - - def gui_footer_log_set(self, pl): - ui = self.gui_footer_input_get() - self.master_frame.contents['footer'] = \ - (urwid.Pile([ urwid.Pile(pl), urwid.Pile([ui]) ]), None) - self.sncli_loop.draw_screen() - - def gui_footer_log_get(self): - return self.master_frame.contents['footer'][0].contents[0][0] - - def gui_footer_input_clear(self): - pl = self.gui_footer_log_get() - self.master_frame.contents['footer'] = \ - (urwid.Pile([ urwid.Pile([pl]), urwid.Pile([]) ]), None) - self.sncli_loop.draw_screen() - - def gui_footer_input_set(self, ui): - pl = self.gui_footer_log_get() - self.master_frame.contents['footer'] = \ - (urwid.Pile([ urwid.Pile([pl]), urwid.Pile([ui]) ]), None) - self.sncli_loop.draw_screen() - - def gui_footer_input_get(self): - return self.master_frame.contents['footer'][0].contents[1][0] - - def gui_footer_focus_input(self): - self.master_frame.focus_position = 'footer' - self.master_frame.contents['footer'][0].focus_position = 1 - - def gui_body_clear(self): - self.master_frame.contents['body'] = ( None, None ) - self.sncli_loop.draw_screen() - - def gui_body_set(self, w): - self.master_frame.contents['body'] = ( w, None ) - self.gui_update_status_bar() - self.sncli_loop.draw_screen() - - def gui_body_get(self): - return self.master_frame.contents['body'][0] - - def gui_body_focus(self): - self.master_frame.focus_position = 'body' - - def log_timeout(self, loop, arg): - self.log_lock.acquire() - - self.log_alarms -= 1 - - if self.log_alarms == 0: - self.gui_footer_log_clear() - self.logs = [] - else: - self.logs.pop(0) - - log_pile = [] - for l in self.logs: - log_pile.append(urwid.AttrMap(urwid.Text(l), 'log')) - - self.gui_footer_log_set(log_pile) - - self.log_lock.release() - - def log(self, msg): - logging.debug(msg) - - if not self.do_gui: - if self.verbose: - print msg - return - - self.log_lock.acquire() - - self.log_alarms += 1 - self.logs.append(msg) - - if len(self.logs) > self.config.get_config('max_logs'): - self.log_alarms -= 1 - self.logs.pop(0) - - log_pile = [] - for l in self.logs: - log_pile.append(urwid.AttrMap(urwid.Text(l), 'log')) - - self.gui_footer_log_set(log_pile) - - self.sncli_loop.set_alarm_in( - int(self.config.get_config('log_timeout')), - self.log_timeout, None) - - self.log_lock.release() - - def gui_update_view(self): - if not self.do_gui: - return - - try: - cur_key = self.view_titles.note_list[self.view_titles.focus_position].note['key'] - except IndexError, e: - cur_key = None - pass - self.view_titles.update_note_list(self.view_titles.search_string) - self.view_titles.focus_note(cur_key) - - if self.gui_body_get().__class__ == view_note.ViewNote: - self.view_note.update_note_view() - - self.gui_update_status_bar() - - def gui_update_status_bar(self): - if self.status_bar != 'yes': - self.gui_header_clear() - else: - self.gui_header_set(self.gui_body_get().get_status_bar()) - - def gui_switch_frame_body(self, new_view, save_current_view=True): - if new_view == None: - if len(self.last_view) == 0: - # XXX verify all notes saved... - self.gui_stop() - else: - self.gui_body_set(self.last_view.pop()) - else: - if self.gui_body_get().__class__ != new_view.__class__: - if save_current_view: - self.last_view.append(self.gui_body_get()) - self.gui_body_set(new_view) - - def trash_note_callback(self, key, yes): - if not yes: - return - - # toggle the deleted flag - note = self.ndb.get_note(key) - self.ndb.set_note_deleted(key, 0 if note['deleted'] else 1) - - if self.gui_body_get().__class__ == view_titles.ViewTitles: - self.view_titles.update_note_title() - - self.gui_update_status_bar() - - def restore_note_callback(self, key, yes): - if not yes: - return - - # restore the contents of the old_note - self.log(u'Restoring version v{0} (key={1})'. - format(self.view_note.old_note['version'], key)) - self.ndb.set_note_content(key, self.view_note.old_note['content']) - - self.view_note.update_note_view() - self.gui_update_status_bar() - - def gui_yes_no_input(self, args, yes_no): - self.gui_footer_input_clear() - self.gui_body_focus() - self.master_frame.keypress = self.gui_frame_keypress - args[0](args[1], - True if yes_no in [ 'YES', 'Yes', 'yes', 'Y', 'y' ] - else False) - - def gui_search_input(self, args, search_string): - self.gui_footer_input_clear() - self.gui_body_focus() - self.master_frame.keypress = self.gui_frame_keypress - if search_string: - self.view_titles.update_note_list(search_string, args[0]) - self.gui_body_set(self.view_titles) - - def gui_version_input(self, args, version): - self.gui_footer_input_clear() - self.gui_body_focus() - self.master_frame.keypress = self.gui_frame_keypress - if version: - try: - # verify input is a number - int(version) - except ValueError, e: - self.log(u'ERROR: Invalid version value') - return - self.view_note.update_note_view(version=version) - self.gui_update_status_bar() - - def gui_tags_input(self, args, tags): - self.gui_footer_input_clear() - self.gui_body_focus() - self.master_frame.keypress = self.gui_frame_keypress - if tags != None: - if self.gui_body_get().__class__ == view_titles.ViewTitles: - note = self.view_titles.note_list[self.view_titles.focus_position].note - else: # self.gui_body_get().__class__ == view_note.ViewNote: - note = self.view_note.note - - self.ndb.set_note_tags(note['key'], tags) - - if self.gui_body_get().__class__ == view_titles.ViewTitles: - self.view_titles.update_note_title() - else: # self.gui_body_get().__class__ == view_note.ViewNote: - self.view_note.update_note_view() - - self.gui_update_status_bar() - - def gui_pipe_input(self, args, cmd): - self.gui_footer_input_clear() - self.gui_body_focus() - self.master_frame.keypress = self.gui_frame_keypress - if cmd != None: - if self.gui_body_get().__class__ == view_titles.ViewTitles: - note = self.view_titles.note_list[self.view_titles.focus_position].note - else: # self.gui_body_get().__class__ == view_note.ViewNote: - note = self.view_note.old_note if self.view_note.old_note \ - else self.view_note.note - args = shlex.split(cmd) - try: - self.gui_clear() - pipe = subprocess.Popen(cmd, stdin=subprocess.PIPE, shell=True) - pipe.communicate(note['content']) - pipe.stdin.close() - pipe.wait() - except OSError, e: - self.log(u'Pipe error: ' + str(e)) - finally: - self.gui_reset() - - def gui_frame_keypress(self, size, key): - - lb = self.gui_body_get() - - if key == self.config.get_keybind('quit'): - self.gui_switch_frame_body(None) - - elif key == self.config.get_keybind('help'): - self.gui_switch_frame_body(self.view_help) - - elif key == self.config.get_keybind('sync'): - self.ndb.last_sync = 0 - - elif key == self.config.get_keybind('view_log'): - self.view_log.update_log() - self.gui_switch_frame_body(self.view_log) - - elif key == self.config.get_keybind('down'): - if len(lb.body.positions()) <= 0: - return None - last = len(lb.body.positions()) - if lb.focus_position == (last - 1): - return None - lb.focus_position += 1 - lb.render(size) - - elif key == self.config.get_keybind('up'): - if len(lb.body.positions()) <= 0: - return None - if lb.focus_position == 0: - return None - lb.focus_position -= 1 - lb.render(size) - - elif key == self.config.get_keybind('page_down'): - if len(lb.body.positions()) <= 0: - return None - last = len(lb.body.positions()) - next_focus = lb.focus_position + size[1] - if next_focus >= last: - next_focus = last - 1 - lb.change_focus(size, next_focus, - offset_inset=0, - coming_from='above') - - elif key == self.config.get_keybind('page_up'): - if len(lb.body.positions()) <= 0: - return None - if 'bottom' in lb.ends_visible(size): - last = len(lb.body.positions()) - next_focus = last - size[1] - size[1] - else: - next_focus = lb.focus_position - size[1] - if next_focus < 0: - next_focus = 0 - lb.change_focus(size, next_focus, - offset_inset=0, - coming_from='below') - - elif key == self.config.get_keybind('half_page_down'): - if len(lb.body.positions()) <= 0: - return None - last = len(lb.body.positions()) - next_focus = lb.focus_position + (size[1] / 2) - if next_focus >= last: - next_focus = last - 1 - lb.change_focus(size, next_focus, - offset_inset=0, - coming_from='above') - - elif key == self.config.get_keybind('half_page_up'): - if len(lb.body.positions()) <= 0: - return None - if 'bottom' in lb.ends_visible(size): - last = len(lb.body.positions()) - next_focus = last - size[1] - (size[1] / 2) - else: - next_focus = lb.focus_position - (size[1] / 2) - if next_focus < 0: - next_focus = 0 - lb.change_focus(size, next_focus, - offset_inset=0, - coming_from='below') - - elif key == self.config.get_keybind('bottom'): - if len(lb.body.positions()) <= 0: - return None - lb.change_focus(size, (len(lb.body.positions()) - 1), - offset_inset=0, - coming_from='above') - - elif key == self.config.get_keybind('top'): - if len(lb.body.positions()) <= 0: - return None - lb.change_focus(size, 0, - offset_inset=0, - coming_from='below') - - elif key == self.config.get_keybind('view_next_note'): - if self.gui_body_get().__class__ != view_note.ViewNote: - return key - - if len(self.view_titles.body.positions()) <= 0: - return None - last = len(self.view_titles.body.positions()) - if self.view_titles.focus_position == (last - 1): - return None - self.view_titles.focus_position += 1 - lb.update_note_view( - 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('view_prev_note'): - if self.gui_body_get().__class__ != view_note.ViewNote: - return key - - if len(self.view_titles.body.positions()) <= 0: - return None - if self.view_titles.focus_position == 0: - return None - self.view_titles.focus_position -= 1 - lb.update_note_view( - 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('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 - - version = diff + (self.view_note.old_note['version'] - if self.view_note.old_note else - self.view_note.note['version']) - - lb.update_note_view(version=version) - - elif key == self.config.get_keybind('diff_version'): - if self.gui_body_get().__class__ != view_note.ViewNote: - return key - - if not self.view_note.old_note: - self.log(u'Already at latest version (key={0})'. - format(self.view_note.key)) - return None - - self.gui_clear() - self.exec_diff_on_note(self.view_note.note, - self.view_note.old_note) - self.gui_reset() - - elif key == self.config.get_keybind('restore_version'): - if self.gui_body_get().__class__ != view_note.ViewNote: - return key - - if not self.view_note.old_note: - self.log(u'Already at latest version (key={0})'. - format(self.view_note.key)) - return None - - self.gui_footer_input_set( - urwid.AttrMap( - user_input.UserInput( - self.config, - 'Restore v{0} (y/n): '.format(self.view_note.old_note['version']), - '', - self.gui_yes_no_input, - [ self.restore_note_callback, self.view_note.key ]), - 'user_input_bar')) - self.gui_footer_focus_input() - self.master_frame.keypress = self.gui_footer_input_get().keypress - - elif key == self.config.get_keybind('latest_version'): - if self.gui_body_get().__class__ != view_note.ViewNote: - return key - - lb.update_note_view(version=None) - - elif key == self.config.get_keybind('select_version'): - if self.gui_body_get().__class__ != view_note.ViewNote: - return key - - self.gui_footer_input_set( - urwid.AttrMap( - user_input.UserInput( - self.config, - key, - '', - self.gui_version_input, - None), - 'user_input_bar')) - self.gui_footer_focus_input() - self.master_frame.keypress = self.gui_footer_input_get().keypress - - elif key == self.config.get_keybind('status'): - if self.status_bar == 'yes': - self.status_bar = 'no' - else: - self.status_bar = self.config.get_config('status_bar') - - elif key == self.config.get_keybind('create_note'): - if self.gui_body_get().__class__ != view_titles.ViewTitles: - return key - - self.gui_clear() - content = self.exec_cmd_on_note(None) - self.gui_reset() - - if content: - self.log(u'New note created') - self.ndb.create_note(content) - self.gui_update_view() - - elif key == self.config.get_keybind('edit_note') or \ - key == self.config.get_keybind('view_note_ext') or \ - key == self.config.get_keybind('view_note_json'): - if self.gui_body_get().__class__ != view_titles.ViewTitles and \ - self.gui_body_get().__class__ != view_note.ViewNote: - return key - - if self.gui_body_get().__class__ == view_titles.ViewTitles: - if len(lb.body.positions()) <= 0: - return None - note = lb.note_list[lb.focus_position].note - else: # self.gui_body_get().__class__ == view_note.ViewNote: - if key == self.config.get_keybind('edit_note'): - note = lb.note - else: - note = lb.old_note if lb.old_note else lb.note - - self.gui_clear() - if key == self.config.get_keybind('edit_note'): - content = self.exec_cmd_on_note(note) - elif key == self.config.get_keybind('view_note_ext'): - content = self.exec_cmd_on_note(note, cmd=self.get_pager()) - else: # key == self.config.get_keybind('view_note_json') - content = self.exec_cmd_on_note(note, cmd=self.get_pager(), raw=True) - - self.gui_reset() - - if not content: - return None - - md5_old = md5.new(note['content']).digest() - md5_new = md5.new(content).digest() - - if md5_old != md5_new: - self.log(u'Note updated') - self.ndb.set_note_content(note['key'], content) - if self.gui_body_get().__class__ == view_titles.ViewTitles: - lb.update_note_title() - else: # self.gui_body_get().__class__ == view_note.ViewNote: - lb.update_note_view() - else: - self.log(u'Note unchanged') - - elif key == self.config.get_keybind('view_note'): - if self.gui_body_get().__class__ != view_titles.ViewTitles: - return key - - if len(lb.body.positions()) <= 0: - return None - self.view_note.update_note_view( - lb.note_list[lb.focus_position].note['key']) - self.gui_switch_frame_body(self.view_note) - - elif key == self.config.get_keybind('pipe_note'): - if self.gui_body_get().__class__ != view_titles.ViewTitles and \ - self.gui_body_get().__class__ != view_note.ViewNote: - return key - - if self.gui_body_get().__class__ == view_titles.ViewTitles: - if len(lb.body.positions()) <= 0: - return None - note = lb.note_list[lb.focus_position].note - else: # self.gui_body_get().__class__ == view_note.ViewNote: - note = lb.old_note if lb.old_note else lb.note - - self.gui_footer_input_set( - urwid.AttrMap( - user_input.UserInput( - self.config, - key, - '', - self.gui_pipe_input, - None), - 'user_input_bar')) - self.gui_footer_focus_input() - self.master_frame.keypress = self.gui_footer_input_get().keypress - - elif key == self.config.get_keybind('note_trash'): - if self.gui_body_get().__class__ != view_titles.ViewTitles and \ - self.gui_body_get().__class__ != view_note.ViewNote: - return key - - if self.gui_body_get().__class__ == view_titles.ViewTitles: - if len(lb.body.positions()) <= 0: - return None - note = lb.note_list[lb.focus_position].note - else: # self.gui_body_get().__class__ == view_note.ViewNote: - note = lb.note - - self.gui_footer_input_set( - urwid.AttrMap( - user_input.UserInput( - self.config, - '{0} (y/n): '.format('Untrash' if note['deleted'] else 'Trash'), - '', - self.gui_yes_no_input, - [ self.trash_note_callback, note['key'] ]), - 'user_input_bar')) - self.gui_footer_focus_input() - self.master_frame.keypress = self.gui_footer_input_get().keypress - - elif key == self.config.get_keybind('note_pin'): - if self.gui_body_get().__class__ != view_titles.ViewTitles and \ - self.gui_body_get().__class__ != view_note.ViewNote: - return key - - if self.gui_body_get().__class__ == view_titles.ViewTitles: - if len(lb.body.positions()) <= 0: - return None - note = lb.note_list[lb.focus_position].note - else: # self.gui_body_get().__class__ == view_note.ViewNote: - note = lb.note - - pin = 1 - if 'systemtags' in note: - if 'pinned' in note['systemtags']: pin = 0 - else: pin = 1 - - self.ndb.set_note_pinned(note['key'], pin) - - if self.gui_body_get().__class__ == view_titles.ViewTitles: - lb.update_note_title() - - elif key == self.config.get_keybind('note_markdown'): - if self.gui_body_get().__class__ != view_titles.ViewTitles and \ - self.gui_body_get().__class__ != view_note.ViewNote: - return key - - if self.gui_body_get().__class__ == view_titles.ViewTitles: - if len(lb.body.positions()) <= 0: - return None - note = lb.note_list[lb.focus_position].note - else: # self.gui_body_get().__class__ == view_note.ViewNote: - note = lb.note - - md = 1 - if 'systemtags' in note: - if 'markdown' in note['systemtags']: md = 0 - else: md = 1 - - self.ndb.set_note_markdown(note['key'], md) - - if self.gui_body_get().__class__ == view_titles.ViewTitles: - lb.update_note_title() - - elif key == self.config.get_keybind('note_tags'): - if self.gui_body_get().__class__ != view_titles.ViewTitles and \ - self.gui_body_get().__class__ != view_note.ViewNote: - return key - - if self.gui_body_get().__class__ == view_titles.ViewTitles: - if len(lb.body.positions()) <= 0: - return None - note = lb.note_list[lb.focus_position].note - else: # self.gui_body_get().__class__ == view_note.ViewNote: - note = lb.note - - self.gui_footer_input_set( - urwid.AttrMap( - user_input.UserInput( - self.config, - 'Tags: ', - '%s' % ','.join(note['tags']), - self.gui_tags_input, - None), - 'user_input_bar')) - self.gui_footer_focus_input() - self.master_frame.keypress = self.gui_footer_input_get().keypress - - elif key == self.config.get_keybind('search_gstyle') or \ - key == self.config.get_keybind('search_regex'): - if self.gui_body_get().__class__ != view_titles.ViewTitles: - return key - - self.gui_footer_input_set( - urwid.AttrMap( - user_input.UserInput( - self.config, - key, - '', - self.gui_search_input, - [ 'gstyle' if key == self.config.get_keybind('search_gstyle') - else 'regex' ]), - 'user_input_bar')) - self.gui_footer_focus_input() - self.master_frame.keypress = self.gui_footer_input_get().keypress - - elif key == self.config.get_keybind('clear_search'): - if self.gui_body_get().__class__ != view_titles.ViewTitles: - return key - - self.view_titles.update_note_list(None) - self.gui_body_set(self.view_titles) - - elif key == self.config.get_keybind('sort_date'): - if self.gui_body_get().__class__ != view_titles.ViewTitles: - return key - - self.view_titles.sort_note_list('date') - - elif key == self.config.get_keybind('sort_alpha'): - if self.gui_body_get().__class__ != view_titles.ViewTitles: - return key - - self.view_titles.sort_note_list('alpha') - - else: - return lb.keypress(size, key) - - self.gui_update_status_bar() - return None - - def gui_init_view(self, loop, view_note): - self.master_frame.keypress = self.gui_frame_keypress - self.gui_body_set(self.view_titles) - - if view_note: - # note that title view set first to prime the view stack - self.gui_switch_frame_body(self.view_note) - - self.thread_sync.start() - - def gui_clear(self): - self.sncli_loop.widget = urwid.Filler(urwid.Text(u'')) - self.sncli_loop.draw_screen() - - def gui_reset(self): - self.sncli_loop.widget = self.master_frame - self.sncli_loop.draw_screen() - - def gui_stop(self): - # don't exit if there are any notes not yet saved to the disk - if self.ndb.verify_all_saved(): - # clear the screen and exit the urwid run loop - self.gui_clear() - raise urwid.ExitMainLoop() - else: - self.log(u'WARNING: Not all notes saved to disk (wait for sync worker)') - - def gui(self, key): - - self.do_gui = True - - self.last_view = [] - self.status_bar = self.config.get_config('status_bar') - - self.log_alarms = 0 - self.log_lock = threading.Lock() - - self.thread_sync = threading.Thread(target=self.ndb.sync_worker, - args=[self.do_server_sync]) - self.thread_sync.setDaemon(True) - - self.view_titles = \ - view_titles.ViewTitles(self.config, - { - 'ndb' : self.ndb, - 'search_string' : None, - 'log' : self.log - }) - self.view_note = \ - view_note.ViewNote(self.config, - { - 'ndb' : self.ndb, - 'key' : key, # initial key to view or None - 'log' : self.log - }) - - self.view_log = view_log.ViewLog(self.config) - self.view_help = view_help.ViewHelp(self.config) - - palette = \ - [ - ('default', - self.config.get_color('default_fg'), - self.config.get_color('default_bg') ), - ('status_bar', - self.config.get_color('status_bar_fg'), - self.config.get_color('status_bar_bg') ), - ('log', - self.config.get_color('log_fg'), - self.config.get_color('log_bg') ), - ('user_input_bar', - self.config.get_color('user_input_bar_fg'), - self.config.get_color('user_input_bar_bg') ), - ('note_focus', - self.config.get_color('note_focus_fg'), - self.config.get_color('note_focus_bg') ), - ('note_title_day', - self.config.get_color('note_title_day_fg'), - self.config.get_color('note_title_day_bg') ), - ('note_title_week', - self.config.get_color('note_title_week_fg'), - self.config.get_color('note_title_week_bg') ), - ('note_title_month', - self.config.get_color('note_title_month_fg'), - self.config.get_color('note_title_month_bg') ), - ('note_title_year', - self.config.get_color('note_title_year_fg'), - self.config.get_color('note_title_year_bg') ), - ('note_title_ancient', - self.config.get_color('note_title_ancient_fg'), - self.config.get_color('note_title_ancient_bg') ), - ('note_date', - self.config.get_color('note_date_fg'), - self.config.get_color('note_date_bg') ), - ('note_flags', - self.config.get_color('note_flags_fg'), - self.config.get_color('note_flags_bg') ), - ('note_tags', - self.config.get_color('note_tags_fg'), - self.config.get_color('note_tags_bg') ), - ('note_content', - self.config.get_color('note_content_fg'), - self.config.get_color('note_content_bg') ), - ('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') ), - ('help_header', - self.config.get_color('help_header_fg'), - self.config.get_color('help_header_bg') ), - ('help_config', - self.config.get_color('help_config_fg'), - self.config.get_color('help_config_bg') ), - ('help_value', - self.config.get_color('help_value_fg'), - self.config.get_color('help_value_bg') ), - ('help_descr', - self.config.get_color('help_descr_fg'), - self.config.get_color('help_descr_bg') ) - ] - - self.master_frame = urwid.Frame(body=urwid.Filler(urwid.Text(u'')), - header=None, - footer=urwid.Pile([ urwid.Pile([]), - urwid.Pile([]) ]), - focus_part='body') - - self.sncli_loop = urwid.MainLoop(self.master_frame, - palette, - handle_mouse=False) - - self.sncli_loop.set_alarm_in(0, self.gui_init_view, - True if key else False) - - self.sncli_loop.run() - - def cli_list_notes(self, regex, search_string): - - note_list, match_regex, all_notes_cnt = \ - self.ndb.filter_notes( - search_string, - search_mode='regex' if regex else 'gstyle') - for n in note_list: - flags = utils.get_note_flags(n.note) - print n.key + \ - u' [' + flags + u'] ' + \ - utils.get_note_title(n.note) - - def cli_note_dump(self, key): - - note = self.ndb.get_note(key) - if not note: - self.log(u'ERROR: Key does not exist') - return - - w = 60 - sep = u'+' + u'-'*(w+2) + u'+' - t = time.localtime(float(note['modifydate'])) - mod_time = time.strftime('%a, %d %b %Y %H:%M:%S', t) - title = utils.get_note_title(note) - flags = utils.get_note_flags(note) - tags = utils.get_note_tags(note) - - print sep - print (u'| {:<' + str(w) + u'} |').format((u' Title: ' + title)[:w]) - print (u'| {:<' + str(w) + u'} |').format((u' Key: ' + note['key'])[:w]) - print (u'| {:<' + str(w) + u'} |').format((u' Date: ' + mod_time)[:w]) - print (u'| {:<' + str(w) + u'} |').format((u' Tags: ' + tags)[:w]) - print (u'| {:<' + str(w) + u'} |').format((u' Version: v' + str(note['version']))[:w]) - print (u'| {:<' + str(w) + u'} |').format((u' Flags: [' + flags + u']')[:w]) - if utils.note_published(note) and 'publishkey' in note: - print (u'| {:<' + str(w) + u'} |').format((u'Published: http://simp.ly/publish/' + note['publishkey'])[:w]) - else: - print (u'| {:<' + str(w) + u'} |').format((u'Published: n/a')[:w]) - print sep - print note['content'] - - def cli_dump_notes(self, regex, search_string): - - note_list, match_regex, all_notes_cnt = \ - self.ndb.filter_notes( - search_string, - search_mode='regex' if regex else 'gstyle') - for n in note_list: - self.cli_note_dump(n.key) - - def cli_note_create(self, from_stdin, title): - - if from_stdin: - content = ''.join(sys.stdin) - else: - content = self.exec_cmd_on_note(None) - - if title: - content = title + '\n\n' + content if content else u'' - - if content: - self.log(u'New note created') - self.ndb.create_note(content) - self.sync_notes() - - def cli_note_edit(self, key): - - note = self.ndb.get_note(key) - if not note: - self.log(u'ERROR: Key does not exist') - return - - content = self.exec_cmd_on_note(note) - if not content: - return - - md5_old = md5.new(note['content']).digest() - md5_new = md5.new(content).digest() - - if md5_old != md5_new: - self.log(u'Note updated') - self.ndb.set_note_content(note['key'], content) - self.sync_notes() - else: - self.log(u'Note unchanged') - - def cli_note_trash(self, key, trash): - - note = self.ndb.get_note(key) - if not note: - self.log(u'ERROR: Key does not exist') - return - - self.ndb.set_note_deleted(key, trash) - self.sync_notes() - - def cli_note_pin(self, key, pin): - - note = self.ndb.get_note(key) - if not note: - self.log(u'ERROR: Key does not exist') - return - - self.ndb.set_note_pinned(key, pin) - self.sync_notes() - - def cli_note_markdown(self, key, markdown): - - note = self.ndb.get_note(key) - if not note: - self.log(u'ERROR: Key does not exist') - return - - self.ndb.set_note_markdown(key, markdown) - self.sync_notes() - - -def SIGINT_handler(signum, frame): - print u'\nSignal caught, bye!' - sys.exit(1) - -signal.signal(signal.SIGINT, SIGINT_handler) - -def usage(): - print u''' -Usage: - sncli [OPTIONS] [COMMAND] [COMMAND_ARGS] - - OPTIONS: - -h, --help - usage help - -v, --verbose - verbose output (cli mode) - -n, --nosync - don't perform a server sync - -r, --regex - search string is a regular expression - -k <key>, --key=<key> - note key - -t <title>, --title=<title> - title of note for create (cli mode) - - COMMANDS: - <none> - console gui mode when no command specified - sync - perform a full sync with the server - list [search_string] - list notes (refined with search string) - dump [search_string] - dump notes (refined with search string) - create [-] - create a note ('-' content from stdin) - dump - dump a note (specified by <key>) - edit - edit a note (specified by <key>) - < trash | untrash > - trash/untrash a note (specified by <key>) - < pin | unpin > - pin/unpin a note (specified by <key>) - < markdown | unmarkdown > - markdown/unmarkdown a note (specified by <key>) -''' - sys.exit(0) - -def main(argv): - verbose = False - sync = True - regex = False - key = None - title = None - - try: - opts, args = getopt.getopt(argv, - 'hvnrk:t:', - [ 'help', 'verbose' 'nosync', 'regex', 'key=', 'title=' ]) - except: - usage() - - for opt, arg in opts: - if opt in [ '-h', '--help']: - usage() - elif opt in [ '-v', '--verbose']: - verbose = True - elif opt in [ '-n', '--nosync']: - sync = False - elif opt in [ '-r', '--regex']: - regex = True - elif opt in [ '-k', '--key']: - key = arg - elif opt in [ '-t', '--title']: - title = arg - else: - print u'ERROR: Unhandled option' - usage() - - if not args: - sncli(sync).gui(key) - return - - def sncli_start(sync, verbose): - sn = sncli(sync, verbose) - if sync: sn.sync_notes() - return sn - - if args[0] == 'sync': - sn = sncli_start(True, verbose) - - elif args[0] == 'list': - - sn = sncli_start(sync, verbose) - sn.cli_list_notes(regex, ' '.join(args[1:])) - - elif args[0] == 'dump': - - sn = sncli_start(sync, verbose) - if key: - sn.cli_note_dump(key) - else: - sn.cli_dump_notes(regex, ' '.join(args[1:])) - - elif args[0] == 'create': - - if len(args) == 1: - sn = sncli_start(sync, verbose) - sn.cli_note_create(False, title) - elif len(args) == 2 and args[1] == '-': - sn = sncli_start(sync, verbose) - sn.cli_note_create(True, title) - else: - usage() - - elif args[0] == 'edit': - - if not key: - usage() - - sn = sncli_start(sync, verbose) - sn.cli_note_edit(key) - - elif args[0] == 'trash' or args[0] == 'untrash': - - if not key: - usage() - - sn = sncli_start(sync, verbose) - sn.cli_note_trash(key, 1 if args[0] == 'trash' else 0) - - elif args[0] == 'pin' or args[0] == 'unpin': - - if not key: - usage() - - sn = sncli_start(sync, verbose) - sn.cli_note_pin(key, 1 if args[0] == 'pin' else 0) - - elif args[0] == 'markdown' or args[0] == 'unmarkdown': - - if not key: - usage() - - sn = sncli_start(sync, verbose) - sn.cli_note_markdown(key, 1 if args[0] == 'markdown' else 0) - - else: - usage() - diff --git a/temp.py b/temp.py @@ -1,37 +0,0 @@ - -import os, json, tempfile - -def tempfile_create(note, raw=False): - if raw: - # dump the raw json of the note - tf = tempfile.NamedTemporaryFile(suffix='.json', delete=False) - json.dump(note, tf, indent=2) - tf.flush() - else: - ext = '.txt' - if note and \ - 'systemtags' in note and \ - 'markdown' in note['systemtags']: - ext = '.mkd' - tf = tempfile.NamedTemporaryFile(suffix=ext, delete=False) - if note: - tf.write(note['content']) - tf.flush() - return tf - -def tempfile_delete(tf): - if tf: - os.unlink(tf.name) - -def tempfile_name(tf): - if tf: - return tf.name - return '' - -def tempfile_content(tf): - tf.seek(0) - lines = [] - for line in tf: - lines.append(line) - return lines - diff --git a/user_input.py b/user_input.py @@ -1,23 +0,0 @@ - -import urwid - -class UserInput(urwid.Edit): - - def __init__(self, config, caption, edit_text, callback_func, args): - self.config = config - self.callback_func = callback_func - self.callback_func_args = args - super(UserInput, self).__init__(caption=caption, - edit_text=edit_text, - wrap='clip') - - def keypress(self, size, key): - size = (size[0],) # if this isn't here then urwid freaks out... - if key == 'esc': - self.callback_func(self.callback_func_args, None) - elif key == 'enter': - self.callback_func(self.callback_func_args, self.edit_text) - else: - return super(UserInput, self).keypress(size, key) - return None - diff --git a/utils.py b/utils.py @@ -1,168 +0,0 @@ -# nvPY: cross-platform note-taking app with simplenote syncing -# copyright 2012 by Charl P. Botha <cpbotha@vxlabs.com> -# new BSD license - -import datetime, random, re - -# first line with non-whitespace should be the title -note_title_re = re.compile('\s*(.*)\n?') - -def generate_random_key(): - """Generate random 30 digit (15 byte) hex string. - - stackoverflow question 2782229 - """ - return '%030x' % (random.randrange(256**15),) - -def get_note_tags(note): - 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 - -# Returns a fixed length string: -# 'X' - needs sync -# 'T' - trashed -# '*' - pinned -# 'S' - published/shared -# 'm' - markdown -def get_note_flags(note): - flags = '' - flags += u'X' if float(note['modifydate']) > float(note['syncdate']) 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' ' - flags += u'm' if 'markdown' in note['systemtags'] else u' ' - else: - flags += ' ' - return flags - -def get_note_title(note): - mo = note_title_re.match(note.get('content', '')) - if mo: - return mo.groups()[0] - else: - return '' - -def get_note_title_file(note): - mo = note_title_re.match(note.get('content', '')) - if mo: - fn = mo.groups()[0] - fn = fn.replace(' ', '_') - fn = fn.replace('/', '_') - if not fn: - return '' - - if isinstance(fn, str): - fn = unicode(fn, 'utf-8') - else: - fn = unicode(fn) - - if note_markdown(note): - fn += '.mkdn' - else: - fn += '.txt' - - return fn - else: - return '' - -def human_date(timestamp): - """ - Given a timestamp, return pretty human format representation. - - For example, if timestamp is: - * today, then do "15:11" - * else if it is this year, then do "Aug 4" - * else do "Dec 11, 2011" - """ - - # this will also give us timestamp in the local timezone - dt = datetime.datetime.fromtimestamp(timestamp) - # this returns localtime - now = datetime.datetime.now() - - if dt.date() == now.date(): - # today: 15:11 - return dt.strftime('%H:%M') - - elif dt.year == now.year: - # this year: Aug 6 - # format code %d unfortunately 0-pads - return dt.strftime('%b') + ' ' + str(dt.day) - - else: - # not today or this year, so we do "Dec 11, 2011" - return '%s %d, %d' % (dt.strftime('%b'), dt.day, dt.year) - -def note_published(n): - asystags = n.get('systemtags', 0) - if not asystags: - return 0 - return 1 if 'published' in asystags else 0 - -def note_pinned(n): - asystags = n.get('systemtags', 0) - if not asystags: - return 0 - return 1 if 'pinned' in asystags else 0 - -def note_markdown(n): - asystags = n.get('systemtags', 0) - if not asystags: - return 0 - return 1 if 'markdown' in asystags else 0 - -tags_illegal_chars = re.compile(r'[\s]') -def sanitise_tags(tags): - """ - Given a string containing comma-separated tags, sanitise and return a list of string tags. - - The simplenote API doesn't allow for spaces, so we strip those out. - - @param tags: Comma-separated tags, one string. - @returns: List of strings. - """ - - # hack out all kinds of whitespace, then split on , - # if you run into more illegal characters (simplenote does not want to sync them) - # add them to the regular expression above. - illegals_removed = tags_illegal_chars.sub('', tags) - if len(illegals_removed) == 0: - # special case for empty string '' - # split turns that into [''], which is not valid - return [] - - else: - return illegals_removed.split(',') - -def sort_by_title_pinned(a, b): - if note_pinned(a.note) and not note_pinned(b.note): - return -1 - elif not note_pinned(a.note) and note_pinned(b.note): - return 1 - else: - return cmp(get_note_title(a.note), get_note_title(b.note)) - -def sort_by_modify_date_pinned(a, b): - if note_pinned(a.note) and not note_pinned(b.note): - return 1 - elif not note_pinned(a.note) and note_pinned(b.note): - return -1 - else: - return cmp(float(a.note.get('modifydate', 0)), float(b.note.get('modifydate', 0))) - -class KeyValueObject: - """Store key=value pairs in this object and retrieve with o.key. - - You should also be able to do MiscObject(**your_dict) for the same effect. - """ - - def __init__(self, **kwargs): - self.__dict__.update(kwargs) - diff --git a/view_help.py b/view_help.py @@ -1,125 +0,0 @@ - -import re, urwid - -class ViewHelp(urwid.ListBox): - - def __init__(self, config): - self.config = config - - self.descr_width = 26 - self.config_width = 29 - - lines = [] - lines.extend(self.create_kb_help_lines(u'Keybinds Common', 'common')) - lines.extend(self.create_kb_help_lines(u'Keybinds Note List', 'titles')) - lines.extend(self.create_kb_help_lines(u'Keybinds Note Content', 'notes')) - lines.extend(self.create_config_help_lines()) - lines.extend(self.create_color_help_lines()) - lines.append(urwid.Text(('help_header', u''))) - - super(ViewHelp, self).__init__(urwid.SimpleFocusListWalker(lines)) - - def get_status_bar(self): - cur = -1 - total = 0 - if len(self.body.positions()) > 0: - cur = self.focus_position - total = len(self.body.positions()) - - status_title = \ - urwid.AttrMap(urwid.Text(u'Help', - wrap='clip'), - 'status_bar') - status_index = \ - ('pack', urwid.AttrMap(urwid.Text(u' ' + - str(cur + 1) + - u'/' + - str(total)), - 'status_bar')) - return \ - urwid.AttrMap(urwid.Columns([ status_title, status_index ]), - 'status_bar') - - def create_kb_help_lines(self, header, use): - lines = [ urwid.AttrMap(urwid.Text(u''), - 'help_header', - 'help_focus') ] - lines.append(urwid.AttrMap(urwid.Text(u' ' + header), - 'help_header', - 'help_focus')) - for c in self.config.keybinds: - if use not in self.config.get_keybind_use(c): - continue - lines.append( - urwid.AttrMap(urwid.AttrMap( - urwid.Text( - [ - ('help_descr', ('{:>' + str(self.descr_width) + '} ').format(self.config.get_keybind_descr(c))), - ('help_config', ('{:>' + str(self.config_width) + '} ').format(u'kb_' + c)), - ('help_value', u"'" + self.config.get_keybind(c) + u"'") - ] - ), - attr_map = None, - focus_map = { - 'help_value' : 'help_focus', - 'help_config' : 'help_focus', - 'help_descr' : 'help_focus' - } - ), 'default', 'help_focus')) - return lines - - def create_config_help_lines(self): - lines = [ urwid.AttrMap(urwid.Text(u''), - 'help_header', - 'help_focus') ] - lines.append(urwid.AttrMap(urwid.Text(u' Configuration'), - 'help_header', - 'help_focus')) - for c in self.config.configs: - if c in [ 'sn_username', 'sn_password' ]: continue - lines.append( - urwid.AttrMap(urwid.AttrMap( - urwid.Text( - [ - ('help_descr', ('{:>' + str(self.descr_width) + '} ').format(self.config.get_config_descr(c))), - ('help_config', ('{:>' + str(self.config_width) + '} ').format(u'cfg_' + c)), - ('help_value', u"'" + self.config.get_config(c) + u"'") - ] - ), - attr_map = None, - focus_map = { - 'help_value' : 'help_focus', - 'help_config' : 'help_focus', - 'help_descr' : 'help_focus' - } - ), 'default', 'help_focus')) - return lines - - def create_color_help_lines(self): - lines = [ urwid.AttrMap(urwid.Text(u''), - 'help_header', - 'help_focus') ] - lines.append(urwid.AttrMap(urwid.Text(u' Colors'), - 'help_header', - 'help_focus')) - fmap = {} - for c in self.config.colors: - fmap[re.search('^(.*)(_fg|_bg)$', c).group(1)] = 'help_focus' - for c in self.config.colors: - lines.append( - urwid.AttrMap(urwid.AttrMap( - urwid.Text( - [ - ('help_descr', ('{:>' + str(self.descr_width) + '} ').format(self.config.get_color_descr(c))), - ('help_config', ('{:>' + str(self.config_width) + '} ').format(u'clr_' + c)), - (re.search('^(.*)(_fg|_bg)$', c).group(1), u"'" + self.config.get_color(c) + u"'") - ] - ), - attr_map = None, - focus_map = fmap - ), 'default', 'help_focus')) - return lines - - def keypress(self, size, key): - return key - diff --git a/view_log.py b/view_log.py @@ -1,47 +0,0 @@ - -import urwid - -class ViewLog(urwid.ListBox): - - def __init__(self, config): - self.config = config - super(ViewLog, self).__init__(urwid.SimpleFocusListWalker([])) - - def update_log(self): - lines = [] - f = open(self.config.logfile) - for line in f: - lines.append( - urwid.AttrMap(urwid.Text(line.rstrip()), - 'note_content', - 'note_content_focus')) - f.close() - if self.config.get_config('log_reversed') == 'yes': - lines.reverse() - self.body[:] = urwid.SimpleFocusListWalker(lines) - self.focus_position = 0 - - def get_status_bar(self): - cur = -1 - total = 0 - if len(self.body.positions()) > 0: - cur = self.focus_position - total = len(self.body.positions()) - - status_title = \ - urwid.AttrMap(urwid.Text(u'Sync Log', - wrap='clip'), - 'status_bar') - status_index = \ - ('pack', urwid.AttrMap(urwid.Text(u' ' + - str(cur + 1) + - u'/' + - str(total)), - 'status_bar')) - return \ - urwid.AttrMap(urwid.Columns([ status_title, status_index ]), - 'status_bar') - - def keypress(self, size, key): - return key - diff --git a/view_note.py b/view_note.py @@ -1,169 +0,0 @@ - -import time, urwid -import utils - -class ViewNote(urwid.ListBox): - - def __init__(self, config, args): - self.config = config - self.ndb = args['ndb'] - self.key = args['key'] - self.log = args['log'] - self.note = self.ndb.get_note(self.key) if self.key else None - self.old_note = None - self.tabstop = int(self.config.get_config('tabstop')) - super(ViewNote, self).__init__( - urwid.SimpleFocusListWalker(self.get_note_content_as_list())) - - def get_note_content_as_list(self): - lines = [] - if not self.key: - return lines - 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_view(self, key=None, version=None): - if key: # setting a new note - self.key = key - self.note = self.ndb.get_note(self.key) - self.old_note = None - - if self.key and version: - # verify version is within range - if int(version) <= 0 or int(version) >= self.note['version'] + 1: - self.log(u'Version v{0} is unavailable (key={1})'. - format(version, self.key)) - return - - if (not version and self.old_note) or \ - (self.key and version and version == self.note['version']): - self.log(u'Displaying latest version v{0} of note (key={1})'. - format(self.note['version'], self.key)) - self.old_note = None - elif self.key and version: - # get a previous version of the note - self.log(u'Fetching version v{0} of note (key={1})'. - format(version, self.key)) - version_note = self.ndb.get_note_version(self.key, version) - if not version_note: - self.log(u'Failed to get version v{0} of note (key={1})'. - format(version, self.key)) - # don't do anything, keep current note/version - else: - self.old_note = version_note - - self.body[:] = \ - urwid.SimpleFocusListWalker(self.get_note_content_as_list()) - self.focus_position = 0 - - def get_status_bar(self): - if not self.key: - return \ - urwid.AttrMap(urwid.Text(u'No note...'), - 'status_bar') - - cur = -1 - total = 0 - if len(self.body.positions()) > 0: - cur = self.focus_position - total = len(self.body.positions()) - - if self.old_note: - t = time.localtime(float(self.old_note['versiondate'])) - title = utils.get_note_title(self.old_note) - version = self.old_note['version'] - else: - t = time.localtime(float(self.note['modifydate'])) - title = utils.get_note_title(self.note) - flags = utils.get_note_flags(self.note) - tags = utils.get_note_tags(self.note) - version = self.note['version'] - - mod_time = time.strftime(u'Date: %a, %d %b %Y %H:%M:%S', t) - - status_title = \ - urwid.AttrMap(urwid.Text(u'Title: ' + - title, - wrap='clip'), - 'status_bar') - - status_key_index = \ - ('pack', urwid.AttrMap(urwid.Text(u' [' + - self.key + - u'] ' + - str(cur + 1) + - u'/' + - str(total)), - 'status_bar')) - - status_date = \ - urwid.AttrMap(urwid.Text(mod_time, - wrap='clip'), - 'status_bar') - - if self.old_note: - status_tags_flags = \ - ('pack', urwid.AttrMap(urwid.Text(u'[OLD:v' + - str(version) + - u']'), - 'status_bar')) - else: - status_tags_flags = \ - ('pack', urwid.AttrMap(urwid.Text(u'[' + - tags + - u'] [v' + - str(version) + - u'] [' + - flags + - u']'), - 'status_bar')) - - pile_top = urwid.Columns([ status_title, status_key_index ]) - pile_bottom = urwid.Columns([ status_date, status_tags_flags ]) - - if self.old_note or \ - not (utils.note_published(self.note) and 'publishkey' in self.note): - return urwid.AttrMap(urwid.Pile([ pile_top, pile_bottom ]), - 'status_bar') - - pile_publish = \ - urwid.AttrMap(urwid.Text(u'Published: http://simp.ly/publish/' + - self.note['publishkey']), - 'status_bar') - return \ - urwid.AttrMap(urwid.Pile([ pile_top, pile_bottom, pile_publish ]), - 'status_bar') - - def keypress(self, size, key): - if key == self.config.get_keybind('tabstop2'): - self.tabstop = 2 - self.body[:] = \ - urwid.SimpleFocusListWalker(self.get_note_content_as_list()) - - elif key == self.config.get_keybind('tabstop4'): - self.tabstop = 4 - self.body[:] = \ - urwid.SimpleFocusListWalker(self.get_note_content_as_list()) - - elif key == self.config.get_keybind('tabstop8'): - self.tabstop = 8 - self.body[:] = \ - urwid.SimpleFocusListWalker(self.get_note_content_as_list()) - - else: - return key - - return None - diff --git a/view_titles.py b/view_titles.py @@ -1,185 +0,0 @@ - -import re, time, datetime, urwid, subprocess -import utils, view_note - -class ViewTitles(urwid.ListBox): - - def __init__(self, config, args): - self.config = config - self.ndb = args['ndb'] - self.search_string = args['search_string'] - self.log = args['log'] - self.note_list, self.match_regex, self.all_notes_cnt = \ - self.ndb.filter_notes(self.search_string) - super(ViewTitles, self).__init__( - urwid.SimpleFocusListWalker(self.get_note_titles())) - - def update_note_list(self, search_string, search_mode='gstyle'): - self.search_string = search_string - self.note_list, self.match_regex, self.all_notes_cnt = \ - self.ndb.filter_notes(self.search_string, search_mode) - self.body[:] = \ - urwid.SimpleFocusListWalker(self.get_note_titles()) - if len(self.note_list) == 0: - self.log(u'No notes found!') - else: - self.focus_position = 0 - - def sort_note_list(self, sort_mode): - self.ndb.filtered_notes_sort(self.note_list, sort_mode) - self.body[:] = \ - urwid.SimpleFocusListWalker(self.get_note_titles()) - - def format_title(self, note): - """ - Various formatting tags are supported for dynamically building - the title string. Each of these formatting tags supports a width - specifier (decimal) and a left justification (-) like that - supported by printf. - - %F -- flags - %T -- tags - %D -- date - %N -- note title - """ - - t = time.localtime(float(note['modifydate'])) - mod_time = time.strftime(self.config.get_config('format_strftime'), t) - title = utils.get_note_title(note) - flags = utils.get_note_flags(note) - tags = utils.get_note_tags(note) - - # get the age of the note - dt = datetime.datetime.fromtimestamp(time.mktime(t)) - if dt > datetime.datetime.now() - datetime.timedelta(days=1): - note_age = 'd' # less than a day old - elif dt > datetime.datetime.now() - datetime.timedelta(weeks=1): - note_age = 'w' # less than a week old - elif dt > datetime.datetime.now() - datetime.timedelta(weeks=4): - note_age = 'm' # less than a month old - elif dt > datetime.datetime.now() - datetime.timedelta(weeks=52): - note_age = 'y' # less than a year old - else: - note_age = 'a' # ancient - - def recursive_format(title_format): - if not title_format: - return None - fmt = re.search("^(.*)%([-]*)([0-9]*)([FDTN])(.*)$", title_format) - if not fmt: - m = ('pack', urwid.AttrMap(urwid.Text(title_format), - 'default')) - l_fmt = None - r_fmt = None - else: - l = fmt.group(1) if fmt.group(1) else None - m = None - r = fmt.group(5) if fmt.group(5) else None - align = 'left' if fmt.group(2) == '-' else 'right' - width = int(fmt.group(3)) if fmt.group(3) else 'pack' - if fmt.group(4) == 'F': - m = (width, urwid.AttrMap(urwid.Text(flags, - align=align, - wrap='clip'), - 'note_flags')) - elif fmt.group(4) == 'D': - m = (width, urwid.AttrMap(urwid.Text(mod_time, - align=align, - wrap='clip'), - 'note_date')) - elif fmt.group(4) == 'T': - m = (width, urwid.AttrMap(urwid.Text(tags, - align=align, - wrap='clip'), - 'note_tags')) - elif fmt.group(4) == 'N': - if note_age == 'd': attr = 'note_title_day' - elif note_age == 'w': attr = 'note_title_week' - elif note_age == 'm': attr = 'note_title_month' - elif note_age == 'y': attr = 'note_title_year' - elif note_age == 'a': attr = 'note_title_ancient' - if width != 'pack': - m = (width, urwid.AttrMap(urwid.Text(title, - align=align, - wrap='clip'), - attr)) - else: - m = urwid.AttrMap(urwid.Text(title, - align=align, - wrap='clip'), - attr) - l_fmt = recursive_format(l) - r_fmt = recursive_format(r) - - tmp = [] - if l_fmt: tmp.extend(l_fmt) - tmp.append(m) - if r_fmt: tmp.extend(r_fmt) - return tmp - - # convert the format string into the actual note title line - 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.note_list: - lines.append(self.get_note_title(n.note)) - return lines - - def get_status_bar(self): - cur = -1 - total = 0 - if len(self.body.positions()) > 0: - cur = self.focus_position - total = len(self.body.positions()) - - hdr = u'Simplenote' - if self.search_string != None: - hdr += ' - Search: ' + self.search_string - - status_title = \ - urwid.AttrMap(urwid.Text(hdr, - wrap='clip'), - 'status_bar') - status_index = \ - ('pack', urwid.AttrMap(urwid.Text(u' ' + - str(cur + 1) + - u'/' + - str(total)), - 'status_bar')) - return \ - urwid.AttrMap(urwid.Columns([ status_title, status_index ]), - 'status_bar') - - def update_note_title(self, key=None): - if not key: - self.body[self.focus_position] = \ - self.get_note_title(self.note_list[self.focus_position].note) - else: - for i in xrange(len(self.note_list)): - if self.note_list[i].note['key'] == key: - self.body[i] = self.get_note_title(self.note_list[i].note) - - def focus_note(self, key): - for i in xrange(len(self.note_list)): - if 'key' in self.note_list[i].note and \ - self.note_list[i].note['key'] == key: - self.focus_position = i - - def keypress(self, size, key): - return key -