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:
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
-