PKahNI1=\\nncli/__init__.py# -*- coding: utf-8 -*- """NextCloud Notes Command Line Interface""" __version__ = '0.3.4' PKNM$^nncli/__main__.py# -*- coding: utf-8 -*- """nncli main module""" import nncli.cli # pylint: disable=no-value-for-parameter if __name__ == '__main__': nncli.cli.main() PKNMAB nncli/cli.py# -*- coding: utf-8 -*- """Command line interface module""" import click from . import __version__ from .nncli import Nncli # pylint: disable=unnecessary-pass class StdinFlag(click.ParamType): """StdinFlag Click Parameter Type""" name = "stdin_flag" def convert(self, value, param, ctx): if value == '-': return True return self.fail('%s is not a valid stdin_flag') STDIN_FLAG = StdinFlag() @click.command() @click.pass_obj def rm_category(ctx_obj): """Remove note category.""" nncli = ctx_obj['nncli'] key = ctx_obj['key'] nncli.cli_note_category_rm(key) @click.command() @click.argument('category', required=True) @click.pass_obj def set_category(ctx_obj, category): """Set the note category.""" nncli = ctx_obj['nncli'] key = ctx_obj['key'] nncli.cli_note_category_set(key, category) @click.command(short_help="Print the note category.") @click.pass_obj def get_category(ctx_obj): """Print the category for the given note on stdout.""" nncli = ctx_obj['nncli'] key = ctx_obj['key'] category = nncli.cli_note_category_get(key) if category: print(category) @click.group() @click.option( '-k', '--key', required=True, type=click.INT, help="Specify the note key." ) @click.pass_context def cat(ctx, key): """Operate on the note category.""" nncli = ctx.obj ctx.obj = {} ctx.obj['nncli'] = nncli ctx.obj['key'] = key cat.add_command(get_category, 'get') cat.add_command(set_category, 'set') cat.add_command(rm_category, 'rm') @click.command() @click.option( '-k', '--key', required=True, type=click.INT, help="Specify the note key.") @click.pass_obj def favorite(nncli, key): """Mark as note as a favorite.""" nncli.cli_note_favorite(key, 1) @click.command() @click.option( '-k', '--key', required=True, type=click.INT, help="Specify the note key." ) @click.pass_obj def unfavorite(nncli, key): """Remove favorite flag from a note.""" nncli.cli_note_favorite(key, 0) @click.command(short_help="Print JSON-formatted note to stdout.") @click.option('-k', '--key', type=click.INT, help="Specify the note key.") @click.option( '-r', '--regex', is_flag=True, help="Treat search term(s) as regular expressions." ) @click.argument('search_terms', nargs=-1) @click.pass_obj def export(nncli, key, regex, search_terms): """ Print JSON-formatted note to stdout. If a key is specified, then regex and search_terms are ignored. """ if key: nncli.cli_note_export(key) else: nncli.cli_export_notes(regex, ' '.join(search_terms)) @click.command(short_help="Print note contents to stdout.") @click.option('-k', '--key', type=click.INT, help="Specify the note key.") @click.option( '-r', '--regex', is_flag=True, help="Treat search term(s) as regular expressions." ) @click.argument('search_terms', nargs=-1) @click.pass_obj def dump(nncli, key, regex, search_terms): """ Print note contents to stdout. If a key is specified, then regex and search_terms are ignored. """ if key: nncli.cli_note_dump(key) else: nncli.cli_dump_notes(regex, ' '.join(search_terms)) @click.command(short_help="List notes.") @click.option( '-r', '--regex', is_flag=True, help="Treat search term(s) as regular expressions." ) @click.argument('search_terms', nargs=-1) @click.pass_obj def list_notes(nncli, regex, search_terms): """ List notes, optionally providing search terms to narrow the results. """ nncli.cli_list_notes(regex, ' '.join(search_terms)) @click.command(short_help="Sync notes to server.") def sync(): """ Perform a full, bi-directional sync of your notes between the server and the local cache. """ pass @click.command() @click.option( '-k', '--key', required=True, type=click.INT, help="Specify the note key." ) @click.pass_obj def delete(nncli, key): """Delete an existing note.""" nncli.cli_note_delete(key, True) @click.command() @click.option( '-k', '--key', required=True, type=click.INT, help="Specify the note key." ) @click.pass_obj def edit(nncli, key): """Edit an existing note.""" nncli.cli_note_edit(key) @click.command(short_help="Import a JSON note.") @click.argument('from_stdin', metavar='[-]', type=STDIN_FLAG) @click.pass_obj def json_import(nncli, from_stdin): """ Import a JSON-formatted note file into your account. The expected JSON format is the same format used internally by nncli. If - is specified, the note is read from stdin, otherwise the editor will open. """ nncli.cli_note_import(from_stdin) @click.command(short_help="Add a new note.") @click.option('-t', '--title', help="Specify the title of note for create.") @click.argument('from_stdin', metavar='[-]', type=STDIN_FLAG) @click.pass_obj def create(nncli, title, from_stdin): """ Create a new note, either opening the editor or, if - is specified, reading from stdin. """ nncli.cli_note_create(from_stdin, title) @click.group(invoke_without_command=True) @click.option( '-n', '--nosync', is_flag=True, help="Don't perform a server sync." ) @click.option('-v', '--verbose', is_flag=True, help="Print verbose output.") @click.option( '-c', '--config', type=click.Path(exists=True), help="Specify the config file to read from." ) @click.option('-k', '--key', type=click.INT, help="Specify the note key.") @click.version_option(version=__version__, message='%(prog)s %(version)s') @click.pass_context def main(ctx, nosync, verbose, config, key): """ Run the NextClound Note Command Line Interface. No COMMAND means to open the console GUI. """ ctx.obj = Nncli(not nosync, verbose, config) if ctx.invoked_subcommand is None: ctx.obj.gui(key) elif not nosync: ctx.obj.ndb.sync_notes() main.add_command(create) main.add_command(edit) main.add_command(delete) main.add_command(sync) main.add_command(json_import, name='import') main.add_command(list_notes, name='list') main.add_command(dump) main.add_command(export) main.add_command(favorite) main.add_command(unfavorite) main.add_command(cat) PKNMnncli/clipboard.py# -*- coding: utf-8 -*- """clipboard module""" import os import subprocess from subprocess import CalledProcessError class Clipboard: """Class implements copying note content to the clipboard""" def __init__(self): self.copy_command = self.get_copy_command() @staticmethod def get_copy_command(): """Defines the copy command based on the contents of $PATH""" try: subprocess.check_output(['which', 'xsel']) return 'echo "%s" | xsel -ib' except CalledProcessError: pass try: subprocess.check_output(['which', 'pbcopy']) return 'echo "%s" | pbcopy' except CalledProcessError: pass return None def copy(self, text): """Copies text to the system clipboard""" if self.copy_command: os.system(self.copy_command % text) PKNM6FQhQhnncli/config.py# -*- coding: utf-8 -*- """config module""" import collections import configparser import os import subprocess import sys from appdirs import user_cache_dir, user_config_dir # pylint: disable=too-few-public-methods class Config: """A class to contain all configuration data for nncli""" class State: """A container class for state information""" def __init__(self, **kwargs): self.__dict__.update(kwargs) def __init__(self, custom_file=None): self.state = Config.State(do_server_sync=True, verbose=False, do_gui=False, search_direction=None) self.config_home = user_config_dir('nncli', 'djmoch') self.cache_home = user_cache_dir('nncli', 'djmoch') defaults = \ { 'cfg_nn_username' : '', 'cfg_nn_password' : '', 'cfg_nn_password_eval' : '', 'cfg_db_path' : self.cache_home, 'cfg_search_categories' : 'yes', # with regex searches 'cfg_sort_mode' : 'date', # 'alpha' or 'date' 'cfg_favorite_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' : os.environ['EDITOR'] \ if 'EDITOR' in os.environ else 'vim {fname} +{line}', 'cfg_pager' : os.environ['PAGER'] \ if 'PAGER' in os.environ else 'less -c', 'cfg_max_logs' : '5', 'cfg_log_timeout' : '5', 'cfg_log_reversed' : 'yes', 'cfg_nn_host' : '', 'cfg_tempdir' : '', 'kb_help' : 'h', 'kb_quit' : 'q', 'kb_sync' : 'S', 'kb_down' : 'j', 'kb_up' : 'k', 'kb_page_down' : 'space', '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_search_gstyle' : '/', 'kb_search_regex' : 'meta /', 'kb_search_prev_gstyle' : '?', 'kb_search_prev_regex' : 'meta ?', 'kb_search_next' : 'n', 'kb_search_prev' : 'N', 'kb_clear_search' : 'A', 'kb_sort_date' : 'd', 'kb_sort_alpha' : 'a', 'kb_sort_categories' : 'ctrl t', 'kb_note_delete' : 'D', 'kb_note_favorite' : 'p', 'kb_note_category' : 't', 'kb_copy_note_text' : 'y', '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_category_fg' : 'dark red', 'clr_note_category_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' } parser = configparser.ConfigParser(defaults) if custom_file is not None: parser.read([custom_file]) else: parser.read([os.path.join(self.config_home, 'config')]) cfg_sec = 'nncli' if not parser.has_section(cfg_sec): parser.add_section(cfg_sec) # ordered dicts used to ease help self._create_configs_dict(parser, cfg_sec) self._create_keybinds_dict(parser, cfg_sec) self._create_colors_dict(parser, cfg_sec) def _create_keybinds_dict(self, parser, cfg_sec): """Create an OrderedDict object with the keybinds""" self.keybinds = collections.OrderedDict() self.keybinds['help'] = \ [parser.get(cfg_sec, 'kb_help'), ['common'], 'Help'] self.keybinds['quit'] = \ [parser.get(cfg_sec, 'kb_quit'), ['common'], 'Quit'] self.keybinds['sync'] = \ [parser.get(cfg_sec, 'kb_sync'), ['common'], 'Full sync'] self.keybinds['down'] = \ [ parser.get(cfg_sec, 'kb_down'), ['common'], 'Scroll down one line' ] self.keybinds['up'] = \ [ parser.get(cfg_sec, 'kb_up'), ['common'], 'Scroll up one line' ] self.keybinds['page_down'] = \ [ parser.get(cfg_sec, 'kb_page_down'), ['common'], 'Page down' ] self.keybinds['page_up'] = \ [parser.get(cfg_sec, 'kb_page_up'), ['common'], 'Page up'] self.keybinds['half_page_down'] = \ [ parser.get(cfg_sec, 'kb_half_page_down'), ['common'], 'Half page down' ] self.keybinds['half_page_up'] = \ [ parser.get(cfg_sec, 'kb_half_page_up'), ['common'], 'Half page up' ] self.keybinds['bottom'] = \ [ parser.get(cfg_sec, 'kb_bottom'), ['common'], 'Goto bottom' ] self.keybinds['top'] = \ [parser.get(cfg_sec, 'kb_top'), ['common'], 'Goto top'] self.keybinds['status'] = \ [ parser.get(cfg_sec, 'kb_status'), ['common'], 'Toggle status bar' ] self.keybinds['view_log'] = \ [ parser.get(cfg_sec, 'kb_view_log'), ['common'], 'View log' ] self.keybinds['create_note'] = \ [ parser.get(cfg_sec, 'kb_create_note'), ['titles'], 'Create a new note' ] self.keybinds['edit_note'] = \ [ parser.get(cfg_sec, 'kb_edit_note'), ['titles', 'notes'], 'Edit note' ] self.keybinds['view_note'] = \ [ parser.get(cfg_sec, 'kb_view_note'), ['titles'], 'View note' ] self.keybinds['view_note_ext'] = \ [ parser.get(cfg_sec, 'kb_view_note_ext'), ['titles', 'notes'], 'View note with pager' ] self.keybinds['view_note_json'] = \ [ parser.get(cfg_sec, 'kb_view_note_json'), ['titles', 'notes'], 'View note raw json' ] self.keybinds['pipe_note'] = \ [ parser.get(cfg_sec, 'kb_pipe_note'), ['titles', 'notes'], 'Pipe note contents' ] self.keybinds['view_next_note'] = \ [ parser.get(cfg_sec, 'kb_view_next_note'), ['notes'], 'View next note' ] self.keybinds['view_prev_note'] = \ [ parser.get(cfg_sec, 'kb_view_prev_note'), ['notes'], 'View previous note' ] self.keybinds['tabstop2'] = \ [ parser.get(cfg_sec, 'kb_tabstop2'), ['notes'], 'View with tabstop=2' ] self.keybinds['tabstop4'] = \ [ parser.get(cfg_sec, 'kb_tabstop4'), ['notes'], 'View with tabstop=4' ] self.keybinds['tabstop8'] = \ [ parser.get(cfg_sec, 'kb_tabstop8'), ['notes'], 'View with tabstop=8' ] self.keybinds['search_gstyle'] = \ [ parser.get(cfg_sec, 'kb_search_gstyle'), ['titles', 'notes'], 'Search using gstyle' ] self.keybinds['search_prev_gstyle'] = \ [ parser.get(cfg_sec, 'kb_search_prev_gstyle'), ['notes'], 'Search backwards using gstyle' ] self.keybinds['search_regex'] = \ [ parser.get(cfg_sec, 'kb_search_regex'), ['titles', 'notes'], 'Search using regex' ] self.keybinds['search_prev_regex'] = \ [ parser.get(cfg_sec, 'kb_search_prev_regex'), ['notes'], 'Search backwards using regex' ] self.keybinds['search_next'] = \ [ parser.get(cfg_sec, 'kb_search_next'), ['notes'], 'Go to next search result' ] self.keybinds['search_prev'] = \ [ parser.get(cfg_sec, 'kb_search_prev'), ['notes'], 'Go to previous search result' ] self.keybinds['clear_search'] = \ [ parser.get(cfg_sec, 'kb_clear_search'), ['titles'], 'Show all notes' ] self.keybinds['sort_date'] = \ [ parser.get(cfg_sec, 'kb_sort_date'), ['titles'], 'Sort notes by date' ] self.keybinds['sort_alpha'] = \ [ parser.get(cfg_sec, 'kb_sort_alpha'), ['titles'], 'Sort notes by alpha' ] self.keybinds['sort_categories'] = \ [ parser.get(cfg_sec, 'kb_sort_categories'), ['titles'], 'Sort notes by categories' ] self.keybinds['note_delete'] = \ [ parser.get(cfg_sec, 'kb_note_delete'), ['titles', 'notes'], 'Delete a note' ] self.keybinds['note_favorite'] = \ [ parser.get(cfg_sec, 'kb_note_favorite'), ['titles', 'notes'], 'Favorite note' ] self.keybinds['note_category'] = \ [ parser.get(cfg_sec, 'kb_note_category'), ['titles', 'notes'], 'Edit note category' ] self.keybinds['copy_note_text'] = \ [ parser.get(cfg_sec, 'kb_copy_note_text'), ['notes'], 'Copy line (xsel/pbcopy)' ] def _create_colors_dict(self, parser, cfg_sec): """Create an OrderedDict object with the colors""" self.colors = collections.OrderedDict() self.colors['default_fg'] = \ [parser.get(cfg_sec, 'clr_default_fg'), 'Default fg'] self.colors['default_bg'] = \ [parser.get(cfg_sec, 'clr_default_bg'), 'Default bg'] self.colors['status_bar_fg'] = \ [parser.get(cfg_sec, 'clr_status_bar_fg'), 'Status bar fg'] self.colors['status_bar_bg'] = \ [parser.get(cfg_sec, 'clr_status_bar_bg'), 'Status bar bg'] self.colors['log_fg'] = \ [parser.get(cfg_sec, 'clr_log_fg'), 'Log message fg'] self.colors['log_bg'] = \ [parser.get(cfg_sec, 'clr_log_bg'), 'Log message bg'] self.colors['user_input_bar_fg'] = \ [ parser.get(cfg_sec, 'clr_user_input_bar_fg'), 'User input bar fg' ] self.colors['user_input_bar_bg'] = \ [ parser.get(cfg_sec, 'clr_user_input_bar_bg'), 'User input bar bg' ] self.colors['note_focus_fg'] = \ [ parser.get(cfg_sec, 'clr_note_focus_fg'), 'Note title focus fg' ] self.colors['note_focus_bg'] = \ [ parser.get(cfg_sec, 'clr_note_focus_bg'), 'Note title focus bg' ] self.colors['note_title_day_fg'] = \ [ parser.get(cfg_sec, 'clr_note_title_day_fg'), 'Day old note title fg' ] self.colors['note_title_day_bg'] = \ [ parser.get(cfg_sec, 'clr_note_title_day_bg'), 'Day old note title bg' ] self.colors['note_title_week_fg'] = \ [ parser.get(cfg_sec, 'clr_note_title_week_fg'), 'Week old note title fg' ] self.colors['note_title_week_bg'] = \ [ parser.get(cfg_sec, 'clr_note_title_week_bg'), 'Week old note title bg' ] self.colors['note_title_month_fg'] = \ [ parser.get(cfg_sec, 'clr_note_title_month_fg'), 'Month old note title fg' ] self.colors['note_title_month_bg'] = \ [ parser.get(cfg_sec, 'clr_note_title_month_bg'), 'Month old note title bg' ] self.colors['note_title_year_fg'] = \ [ parser.get(cfg_sec, 'clr_note_title_year_fg'), 'Year old note title fg' ] self.colors['note_title_year_bg'] = \ [ parser.get(cfg_sec, 'clr_note_title_year_bg'), 'Year old note title bg' ] self.colors['note_title_ancient_fg'] = \ [ parser.get(cfg_sec, 'clr_note_title_ancient_fg'), 'Ancient note title fg' ] self.colors['note_title_ancient_bg'] = \ [ parser.get(cfg_sec, 'clr_note_title_ancient_bg'), 'Ancient note title bg' ] self.colors['note_date_fg'] = \ [parser.get(cfg_sec, 'clr_note_date_fg'), 'Note date fg'] self.colors['note_date_bg'] = \ [parser.get(cfg_sec, 'clr_note_date_bg'), 'Note date bg'] self.colors['note_flags_fg'] = \ [parser.get(cfg_sec, 'clr_note_flags_fg'), 'Note flags fg'] self.colors['note_flags_bg'] = \ [parser.get(cfg_sec, 'clr_note_flags_bg'), 'Note flags bg'] self.colors['note_category_fg'] = \ [ parser.get(cfg_sec, 'clr_note_category_fg'), 'Note category fg' ] self.colors['note_category_bg'] = \ [ parser.get(cfg_sec, 'clr_note_category_bg'), 'Note category bg' ] self.colors['note_content_fg'] = \ [parser.get(cfg_sec, 'clr_note_content_fg'), 'Note content fg'] self.colors['note_content_bg'] = \ [parser.get(cfg_sec, 'clr_note_content_bg'), 'Note content bg'] self.colors['note_content_focus_fg'] = \ [ parser.get(cfg_sec, 'clr_note_content_focus_fg'), 'Note content focus fg' ] self.colors['note_content_focus_bg'] = \ [ parser.get(cfg_sec, 'clr_note_content_focus_bg'), 'Note content focus bg' ] self.colors['note_content_old_fg'] = \ [ parser.get(cfg_sec, 'clr_note_content_old_fg'), 'Old note content fg' ] self.colors['note_content_old_bg'] = \ [ parser.get(cfg_sec, 'clr_note_content_old_bg'), 'Old note content bg' ] self.colors['note_content_old_focus_fg'] = \ [ parser.get(cfg_sec, 'clr_note_content_old_focus_fg'), 'Old note content focus fg' ] self.colors['note_content_old_focus_bg'] = \ [ parser.get(cfg_sec, 'clr_note_content_old_focus_bg'), 'Old note content focus bg' ] self.colors['help_focus_fg'] = \ [parser.get(cfg_sec, 'clr_help_focus_fg'), 'Help focus fg'] self.colors['help_focus_bg'] = \ [parser.get(cfg_sec, 'clr_help_focus_bg'), 'Help focus bg'] self.colors['help_header_fg'] = \ [parser.get(cfg_sec, 'clr_help_header_fg'), 'Help header fg'] self.colors['help_header_bg'] = \ [parser.get(cfg_sec, 'clr_help_header_bg'), 'Help header bg'] self.colors['help_config_fg'] = \ [parser.get(cfg_sec, 'clr_help_config_fg'), 'Help config fg'] self.colors['help_config_bg'] = \ [parser.get(cfg_sec, 'clr_help_config_bg'), 'Help config bg'] self.colors['help_value_fg'] = \ [parser.get(cfg_sec, 'clr_help_value_fg'), 'Help value fg'] self.colors['help_value_bg'] = \ [parser.get(cfg_sec, 'clr_help_value_bg'), 'Help value bg'] self.colors['help_descr_fg'] = \ [ parser.get(cfg_sec, 'clr_help_descr_fg'), 'Help description fg' ] self.colors['help_descr_bg'] = \ [ parser.get(cfg_sec, 'clr_help_descr_bg'), 'Help description bg' ] def _create_configs_dict(self, parser, cfg_sec): """Create an OrderedDict object with the configs""" # special handling for password so we can retrieve it by # running a command nn_password = parser.get(cfg_sec, 'cfg_nn_password', raw=True) if not nn_password: command = parser.get(cfg_sec, 'cfg_nn_password_eval', raw=True) if command: try: nn_password = subprocess.check_output( command, shell=True, universal_newlines=True ) # remove trailing newlines to avoid requiring # butchering shell commands (they can't usually be # in passwords anyway) nn_password = nn_password.rstrip('\n') except subprocess.CalledProcessError as ex: print('Error evaluating command for password: %s' % ex) sys.exit(1) self.configs = collections.OrderedDict() self.configs['nn_username'] = \ [ parser.get(cfg_sec, 'cfg_nn_username', raw=True), 'NextCloud Username' ] self.configs['nn_password'] = [nn_password, 'NextCloud Password'] self.configs['nn_host'] = \ [ parser.get(cfg_sec, 'cfg_nn_host', raw=True), 'NextCloud server hostname' ] self.configs['db_path'] = \ [parser.get(cfg_sec, 'cfg_db_path'), 'Note storage path'] self.configs['search_categories'] = \ [ parser.get(cfg_sec, 'cfg_search_categories'), 'Search categories as well' ] self.configs['sort_mode'] = \ [parser.get(cfg_sec, 'cfg_sort_mode'), 'Sort mode'] self.configs['favorite_ontop'] = \ [ parser.get(cfg_sec, 'cfg_favorite_ontop'), 'Favorite at top of list' ] self.configs['tabstop'] = \ [parser.get(cfg_sec, 'cfg_tabstop'), 'Tabstop spaces'] self.configs['format_strftime'] = \ [ parser.get(cfg_sec, 'cfg_format_strftime', raw=True), 'Date strftime format' ] self.configs['format_note_title'] = \ [ parser.get(cfg_sec, 'cfg_format_note_title', raw=True), 'Note title format' ] self.configs['status_bar'] = \ [parser.get(cfg_sec, 'cfg_status_bar'), 'Show the status bar'] self.configs['editor'] = \ [parser.get(cfg_sec, 'cfg_editor'), 'Editor command'] self.configs['pager'] = \ [parser.get(cfg_sec, 'cfg_pager'), 'External pager command'] self.configs['max_logs'] = \ [parser.get(cfg_sec, 'cfg_max_logs'), 'Max logs in footer'] self.configs['log_timeout'] = \ [parser.get(cfg_sec, 'cfg_log_timeout'), 'Log timeout'] self.configs['log_reversed'] = \ [parser.get(cfg_sec, 'cfg_log_reversed'), 'Log file reversed'] self.configs['tempdir'] = \ [ None if parser.get(cfg_sec, 'cfg_tempdir') == '' \ else parser.get(cfg_sec, 'cfg_tempdir'), 'Temporary directory for note storage' ] def get_config(self, name): """Get a config value""" return self.configs[name][0] def get_config_descr(self, name): """Get a config description""" return self.configs[name][1] def get_keybind(self, name): """Get a keybinding value""" return self.keybinds[name][0] def get_keybind_use(self, name): """Get the context(s) where a keybinding is valid""" return self.keybinds[name][1] def get_keybind_descr(self, name): """Get a keybinding description""" return self.keybinds[name][2] def get_color(self, name): """Get a color value""" return self.colors[name][0] def get_color_descr(self, name): """Get a color description""" return self.colors[name][1] PKt]hN/.__ nncli/gui.py# -*- coding: utf-8 -*- """nncli_gui module""" import hashlib import subprocess import threading import urwid from . import view_titles, view_note, view_help, view_log, user_input from .utils import exec_cmd_on_note, get_pager # pylint: disable=too-many-instance-attributes, unused-argument class NncliGui: """NncliGui class. Responsible for the console GUI view logic.""" def __init__(self, config, logger, ndb, key=None): self.ndb = ndb self.logger = logger self.config = config self.last_view = [] self.status_bar = self.config.get_config('status_bar') self.config.state.current_sort_mode = \ self.config.get_config('sort_mode') self.log_lock = threading.Lock() self.log_alarms = 0 self.logs = [] self.thread_sync = threading.Thread( target=self.ndb.sync_worker, args=[self.config.state.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, 'id' : key, # initial key to view or None 'log' : self.log } ) self.view_log = view_log.ViewLog(self.config, self.logger) 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_category', self.config.get_color('note_category_fg'), self.config.get_color('note_category_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('')), header=None, footer=urwid.Pile([urwid.Pile([]), urwid.Pile([])]), focus_part='body') self.nncli_loop = urwid.MainLoop(self.master_frame, palette, handle_mouse=False) self.nncli_loop.set_alarm_in(0, self._gui_init_view, \ bool(key)) def run(self): """Run the GUI""" self.nncli_loop.run() def _gui_header_clear(self): """Clear the console GUI header row""" self.master_frame.contents['header'] = (None, None) self.nncli_loop.draw_screen() def _gui_header_set(self, widget): """Set the content of the console GUI header row""" self.master_frame.contents['header'] = (widget, None) self.nncli_loop.draw_screen() def _gui_footer_log_clear(self): """Clear the log at the bottom of the GUI""" gui = self._gui_footer_input_get() self.master_frame.contents['footer'] = \ (urwid.Pile([urwid.Pile([]), urwid.Pile([gui])]), None) self.nncli_loop.draw_screen() def _gui_footer_log_set(self, pile): """Set the log at the bottom of the GUI""" gui = self._gui_footer_input_get() self.master_frame.contents['footer'] = \ (urwid.Pile([urwid.Pile(pile), urwid.Pile([gui])]), None) self.nncli_loop.draw_screen() def _gui_footer_log_get(self): """Get the log at the bottom of the GUI""" return self.master_frame.contents['footer'][0].contents[0][0] def _gui_footer_input_clear(self): """Clear the input at the bottom of the GUI""" pile = self._gui_footer_log_get() self.master_frame.contents['footer'] = \ (urwid.Pile([urwid.Pile([pile]), urwid.Pile([])]), None) self.nncli_loop.draw_screen() def _gui_footer_input_set(self, gui): """Set the input at the bottom of the GUI""" pile = self._gui_footer_log_get() self.master_frame.contents['footer'] = \ (urwid.Pile([urwid.Pile([pile]), urwid.Pile([gui])]), None) self.nncli_loop.draw_screen() def _gui_footer_input_get(self): """Get the input at the bottom of the GUI""" return self.master_frame.contents['footer'][0].contents[1][0] def _gui_footer_focus_input(self): """Set the GUI focus to the input at the bottom of the GUI""" self.master_frame.focus_position = 'footer' self.master_frame.contents['footer'][0].focus_position = 1 def _gui_body_set(self, widget): """Set the GUI body""" self.master_frame.contents['body'] = (widget, None) self._gui_update_status_bar() self.nncli_loop.draw_screen() def gui_body_get(self): """Get the GUI body""" return self.master_frame.contents['body'][0] def _gui_body_focus(self): """Set the GUI focus to the body""" self.master_frame.focus_position = 'body' def gui_update_view(self): """Update the GUI""" if not self.config.state.do_gui: return try: cur_key = self.view_titles.note_list \ [self.view_titles.focus_position].note['localkey'] except IndexError: cur_key = None self.view_titles.update_note_list( self.view_titles.search_string, sort_mode=self.config.state.current_sort_mode ) 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): """Update the GUI status bar""" 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): """ Switch the body frame of the GUI. Used to switch to a new view """ if new_view is None: if not self.last_view: 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 _delete_note_callback(self, key, delete): """Update the GUI after deleting a note""" if not delete: return self.ndb.set_note_deleted(key, True) if self.gui_body_get().__class__ == view_titles.ViewTitles: self.view_titles.update_note_title() self._gui_update_status_bar() self.ndb.sync_worker_go() def _gui_yes_no_input(self, args, yes_no): """Create a yes/no input dialog at the GUI footer""" self._gui_footer_input_clear() self._gui_body_focus() self.master_frame.keypress = self._gui_frame_keypress args[0](args[1], yes_no in ['YES', 'Yes', 'yes', 'Y', 'y'] ) def _gui_search_input(self, args, search_string): """Create a search input dialog at the GUI footer""" self._gui_footer_input_clear() self._gui_body_focus() self.master_frame.keypress = self._gui_frame_keypress if search_string: if self.gui_body_get() == self.view_note: self.config.state.search_direction = args[1] self.view_note.search_note_view_next( search_string=search_string, search_mode=args[0] ) else: self.view_titles.update_note_list( search_string, args[0], sort_mode=self.config.state.current_sort_mode ) self._gui_body_set(self.view_titles) def _gui_category_input(self, args, category): """Create a category input at the GUI footer""" self._gui_footer_input_clear() self._gui_body_focus() self.master_frame.keypress = self._gui_frame_keypress if category is not 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_category(note['localkey'], category) 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() self.ndb.sync_worker_go() def _gui_pipe_input(self, args, cmd): """Create a pipe input dialog at the GUI footoer""" self._gui_footer_input_clear() self._gui_body_focus() self.master_frame.keypress = self._gui_frame_keypress if cmd is not 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 try: self._gui_clear() pipe = subprocess.Popen(cmd, stdin=subprocess.PIPE, shell=True) pipe.communicate(note['content'].encode('utf-8')) pipe.stdin.close() pipe.wait() except OSError as ex: self.log('Pipe error: %s' % ex) finally: self._gui_reset() # pylint: disable=too-many-return-statements, too-many-branches # pylint: disable=too-many-statements def _gui_frame_keypress(self, size, key): """Keypress handler for the GUI""" # convert space character into name if key == ' ': key = 'space' contents = 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 self.ndb.sync_worker_go() 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 not contents.body.positions(): return None last = len(contents.body.positions()) if contents.focus_position == (last - 1): return None contents.focus_position += 1 contents.render(size) elif key == self.config.get_keybind('up'): if not contents.body.positions(): return None if contents.focus_position == 0: return None contents.focus_position -= 1 contents.render(size) elif key == self.config.get_keybind('page_down'): if not contents.body.positions(): return None last = len(contents.body.positions()) next_focus = contents.focus_position + size[1] if next_focus >= last: next_focus = last - 1 contents.change_focus(size, next_focus, offset_inset=0, coming_from='above') elif key == self.config.get_keybind('page_up'): if not contents.body.positions(): return None if 'bottom' in contents.ends_visible(size): last = len(contents.body.positions()) next_focus = last - size[1] - size[1] else: next_focus = contents.focus_position - size[1] if next_focus < 0: next_focus = 0 contents.change_focus(size, next_focus, offset_inset=0, coming_from='below') elif key == self.config.get_keybind('half_page_down'): if not contents.body.positions(): return None last = len(contents.body.positions()) next_focus = contents.focus_position + (size[1] // 2) if next_focus >= last: next_focus = last - 1 contents.change_focus(size, next_focus, offset_inset=0, coming_from='above') elif key == self.config.get_keybind('half_page_up'): if not contents.body.positions(): return None if 'bottom' in contents.ends_visible(size): last = len(contents.body.positions()) next_focus = last - size[1] - (size[1] // 2) else: next_focus = contents.focus_position - (size[1] // 2) if next_focus < 0: next_focus = 0 contents.change_focus(size, next_focus, offset_inset=0, coming_from='below') elif key == self.config.get_keybind('bottom'): if not contents.body.positions(): return None contents.change_focus(size, (len(contents.body.positions()) - 1), offset_inset=0, coming_from='above') elif key == self.config.get_keybind('top'): if not contents.body.positions(): return None contents.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 not self.view_titles.body.positions(): 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 contents.update_note_view( self.view_titles. \ note_list[self.view_titles. \ focus_position].note['localkey'] ) 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 not self.view_titles.body.positions(): return None if self.view_titles.focus_position == 0: return None self.view_titles.focus_position -= 1 contents.update_note_view( self.view_titles. \ note_list[self.view_titles. \ focus_position].note['localkey'] ) self._gui_switch_frame_body(self.view_note) 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 = exec_cmd_on_note(None, self.config, self, self.logger) self._gui_reset() if content: self.log('New note created') self.ndb.create_note(content) self.gui_update_view() self.ndb.sync_worker_go() 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 not contents.body.positions(): return None note = contents.note_list[contents.focus_position].note else: # self.gui_body_get().__class__ == view_note.ViewNote: if key == self.config.get_keybind('edit_note'): note = contents.note else: note = contents.old_note if contents.old_note \ else contents.note self._gui_clear() if key == self.config.get_keybind('edit_note'): content = exec_cmd_on_note(note, self.config, self, self.logger) elif key == self.config.get_keybind('view_note_ext'): content = exec_cmd_on_note( note, self.config, self, self.logger, cmd=get_pager(self.config, self.logger)) else: # key == self.config.get_keybind('view_note_json') content = exec_cmd_on_note( note, self.config, self, self.logger, cmd=get_pager(self.config, self.logger), raw=True ) self._gui_reset() if not content: return None md5_old = hashlib.md5(note['content'].encode('utf-8')).digest() md5_new = hashlib.md5(content.encode('utf-8')).digest() if md5_old != md5_new: self.log('Note updated') self.ndb.set_note_content(note['localkey'], content) if self.gui_body_get().__class__ == view_titles.ViewTitles: contents.update_note_title() else: # self.gui_body_get().__class__ == view_note.ViewNote: contents.update_note_view() self.ndb.sync_worker_go() else: self.log('Note unchanged') elif key == self.config.get_keybind('view_note'): if self.gui_body_get().__class__ != view_titles.ViewTitles: return key if not contents.body.positions(): return None self.view_note.update_note_view( contents.note_list[contents.focus_position]. \ note['localkey']) 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 not contents.body.positions(): return None note = contents.note_list[contents.focus_position].note else: # self.gui_body_get().__class__ == view_note.ViewNote: note = contents.old_note if contents.old_note else contents.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_delete'): 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 not contents.body.positions(): return None note = contents.note_list[contents.focus_position].note else: # self.gui_body_get().__class__ == view_note.ViewNote: note = contents.note self._gui_footer_input_set( urwid.AttrMap( user_input.UserInput( self.config, 'Delete (y/n): ', '', self._gui_yes_no_input, [ self._delete_note_callback, note['localkey'] ] ), '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_favorite'): 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 not contents.body.positions(): return None note = contents.note_list[contents.focus_position].note else: # self.gui_body_get().__class__ == view_note.ViewNote: note = contents.note favorite = not note['favorite'] self.ndb.set_note_favorite(note['localkey'], favorite) if self.gui_body_get().__class__ == view_titles.ViewTitles: contents.update_note_title() self.ndb.sync_worker_go() elif key == self.config.get_keybind('note_category'): 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 not contents.body.positions(): return None note = contents.note_list[contents.focus_position].note else: # self.gui_body_get().__class__ == view_note.ViewNote: note = contents.note self._gui_footer_input_set( urwid.AttrMap( user_input.UserInput( self.config, 'Category: ', note['category'], self._gui_category_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') or \ key == self.config.get_keybind('search_prev_gstyle') or \ key == self.config.get_keybind('search_prev_regex'): 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_note.ViewNote: if key == self.config.get_keybind('search_prev_gstyle') or \ key == self.config.get_keybind('search_prev_regex'): self.view_note.search_direction = 'backward' else: self.view_note.search_direction = 'forward' options = [ 'gstyle' if key == self.config.get_keybind('search_gstyle') or key == self.config.get_keybind('search_prev_gstyle') else 'regex', 'backward' if key == self.config.get_keybind('search_prev_gstyle') or key == self.config.get_keybind('search_prev_regex') else 'forward' ] caption = '{}{}'.format('(regex) ' if options[0] == 'regex' else '', '/' if options[1] == 'forward' else '?') self._gui_footer_input_set( urwid.AttrMap( user_input.UserInput( self.config, caption, '', self._gui_search_input, options ), '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_next'): if self.gui_body_get().__class__ != view_note.ViewNote: return key self.view_note.search_note_view_next() elif key == self.config.get_keybind('search_prev'): if self.gui_body_get().__class__ != view_note.ViewNote: return key self.view_note.search_note_view_prev() 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, sort_mode=self.config.state.current_sort_mode ) 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.config.state.current_sort_mode = 'date' 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.config.state.current_sort_mode = 'alpha' self.view_titles.sort_note_list('alpha') elif key == self.config.get_keybind('sort_categories'): if self.gui_body_get().__class__ != view_titles.ViewTitles: return key self.config.state.current_sort_mode = 'categories' self.view_titles.sort_note_list('categories') elif key == self.config.get_keybind('copy_note_text'): if self.gui_body_get().__class__ != view_note.ViewNote: return key self.view_note.copy_note_text() else: return contents.keypress(size, key) self._gui_update_status_bar() return None def _gui_init_view(self, loop, show_note): """Initialize the GUI""" self.master_frame.keypress = self._gui_frame_keypress self._gui_body_set(self.view_titles) if show_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): """Clear the GUI""" self.nncli_loop.widget = urwid.Filler(urwid.Text('')) self.nncli_loop.draw_screen() def _gui_reset(self): """Reset the GUI""" self.nncli_loop.widget = self.master_frame self.nncli_loop.draw_screen() def _gui_stop(self): """Stop the GUI""" # don't exit if there are any notes not yet saved to the disk # NOTE: this was originally causing hangs on exit with urllib2 # should not be a problem now since using the requests library # ref https://github.com/insanum/sncli/issues/18#issuecomment-105517773 if self.ndb.verify_all_saved(): # clear the screen and exit the urwid run loop self._gui_clear() raise urwid.ExitMainLoop() self.log('WARNING: Not all notes saved' 'to disk (wait for sync worker)') def log(self, msg): """Log as message, displaying to the user as appropriate""" self.logger.log(msg) self.log_lock.acquire() self.log_alarms += 1 self.logs.append(msg) if len(self.logs) > int(self.config.get_config('max_logs')): self.log_alarms -= 1 self.logs.pop(0) log_pile = [] for log in self.logs: log_pile.append(urwid.AttrMap(urwid.Text(log), 'log')) if self.config.state.verbose: self._gui_footer_log_set(log_pile) self.nncli_loop.set_alarm_in( int(self.config.get_config('log_timeout')), self._log_timeout, None) self.log_lock.release() def _log_timeout(self, loop, arg): """ Run periodically to check for new log entries to append to the GUI footer """ self.log_lock.acquire() self.log_alarms -= 1 if self.log_alarms == 0: self._gui_footer_log_clear() self.logs = [] else: if self.logs: self.logs.pop(0) log_pile = [] for log in self.logs: log_pile.append(urwid.AttrMap(urwid.Text(log), 'log')) if self.config.state.verbose: self._gui_footer_log_set(log_pile) self.log_lock.release() PKNM local modified || 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 = int(time.time()) sync_start_time = int(time.time()) sync_errors = 0 skip_remote_syncing = False 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 _, local_key in enumerate(self.notes.keys()): note = self.notes[local_key] if not note.get('id') or \ float(note.get('modified')) > float(note.get('syncdate')): savedate = float(note.get('savedate')) if float(note.get('modified')) > savedate or \ float(note.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 cnote = copy.deepcopy(note) if 'what_changed' in note: del note['what_changed'] if 'localkey' in cnote: del cnote['localkey'] if 'minversion' in cnote: del cnote['minversion'] del cnote['syncdate'] del cnote['savedate'] del cnote['deleted'] if 'etag' in cnote: del cnote['etag'] if 'title' in cnote: del cnote['title'] if 'what_changed' in cnote: if 'content' not in cnote['what_changed'] \ and 'category' not in cnote['what_changed']: del cnote['content'] if 'category' not in cnote['what_changed']: del cnote['category'] if 'favorite' not in cnote['what_changed']: del cnote['favorite'] del cnote['what_changed'] try: if note['deleted']: uret = self.note.delete_note(cnote) else: uret = self.note.update_note(cnote) # 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] key = uret[0].get('id') category = uret[0].get('category') category = category if category is not None else '' note.update(uret[0]) note['syncdate'] = now note['localkey'] = key note['category'] = category self.notes[key] = note local_updates[key] = True if local_key != key: # 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) ) except (ConnectionError, RequestException, ValueError): 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: note_list = [] else: note_list = self.note.get_note_list() if note_list[1] == 0: # success note_list = note_list[0] else: self.log('ERROR: Failed to get note list from server') sync_errors += 1 note_list = [] skip_remote_syncing = True # 3. for each remote note # if remote modified > local modified || # a new note and key is not in local store # retrieve note, update note with response if not skip_remote_syncing: for _, note in enumerate(note_list): key = note.get('id') category = note.get('category') \ if note.get('category') is not None \ else '' server_keys[key] = 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 key in self.notes: # we already have this note # if the server note has a newer syncnum we need to get it if int(note.get('modified')) > \ int(self.notes[key].get('modified')): gret = self.note.get_note(key) if gret[1] == 0: self.notes[key].update(gret[0]) local_updates[key] = True self.notes[key]['syncdate'] = now self.notes[key]['localkey'] = key self.notes[key]['category'] = category self.notes[key]['deleted'] = False self.log( 'Synced newer note from server (key={0})'. format(key) ) else: self.log( 'ERROR: Failed to sync newer note ' 'from server (key={0})'.format(key) ) sync_errors += 1 else: # this is a new note gret = self.note.get_note(key) if gret[1] == 0: self.notes[key] = gret[0] local_updates[key] = True self.notes[key]['syncdate'] = now self.notes[key]['localkey'] = key self.notes[key]['category'] = category self.notes[key]['deleted'] = False self.log( 'Synced new note from server (key={0})'. format(key) ) else: self.log( 'ERROR: Failed syncing new note from' 'server (key={0})'.format(key) ) 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 and not skip_remote_syncing: for local_key in list(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 key in list(local_updates.keys()): try: self._helper_save_note(key, self.notes[key]) except WriteError as ex: raise WriteError(str(ex)) self.log("Saved note to disk (key={0})".format(key)) for key in list(local_deletes.keys()): fnote = self._helper_key_to_fname(key) if os.path.exists(fnote): os.unlink(fnote) self.log("Deleted note from disk (key={0})".format(key)) if not sync_errors: self.last_sync = sync_start_time # if there were any changes then update the current view if local_updates or local_deletes: self.update_view() if server_sync and full_sync: self.log("Full sync completed") return sync_errors def _get_note_status(self, key): """Get the note status""" note = self.notes[key] obj = utils.KeyValueObject(saved=False, synced=False, modified=False) modified = float(note['modified']) savedate = float(note['savedate']) if savedate > modified: obj.saved = True return obj def verify_all_saved(self): """ Verify all notes in the local database are saved to the server """ all_saved = True self.sync_lock.acquire() for k in list(self.notes.keys()): obj = self._get_note_status(k) if not obj.saved: all_saved = False break self.sync_lock.release() return all_saved def sync_now(self, do_server_sync=True): """Sync the notes to the server""" self.sync_lock.acquire() self.sync_notes(server_sync=do_server_sync, full_sync=not bool(self.last_sync)) self.sync_lock.release() def sync_worker(self, do_server_sync): """The sync worker thread""" time.sleep(1) # give some time to wait for GUI initialization self.log('Sync worker: started') self.sync_now(do_server_sync) while True: self.go_cond.acquire() self.go_cond.wait(15) self.sync_now(do_server_sync) self.go_cond.release() def sync_worker_go(self): """Start the sync worker""" self.go_cond.acquire() self.go_cond.notify() self.go_cond.release() PKNM߁ nncli/temp.py# -*- coding: utf-8 -*- """temp module""" import json import os import tempfile def tempfile_create(note, raw=False, tempdir=None): """create a temp file""" if raw: # dump the raw json of the note tfile = tempfile.NamedTemporaryFile(suffix='.json', delete=False, dir=tempdir) contents = json.dumps(note, indent=2) tfile.write(contents.encode('utf-8')) tfile.flush() else: ext = '.mkd' tfile = tempfile.NamedTemporaryFile(suffix=ext, delete=False, dir=tempdir) if note: contents = note['content'] tfile.write(contents.encode('utf-8')) tfile.flush() return tfile def tempfile_delete(tfile): """delete a temp file""" if tfile: tfile.close() os.unlink(tfile.name) def tempfile_name(tfile): """get the name of a temp file""" if tfile: return tfile.name return '' def tempfile_content(tfile): """read the contents of the temp file""" # This 'hack' is needed because some editors use an intermediate temporary # file, and rename it to that of the correct file, overwriting it. This # means that the tf file handle won't be updated with the new contents, and # the tempfile must be re-opened and read if not tfile: return None with open(tfile.name, 'rb') as temp: updated_tf_contents = temp.read() return updated_tf_contents.decode('utf-8') PKNM-Ņnncli/user_input.py# -*- coding: utf-8 -*- """user_input module""" import urwid # pylint: disable=too-many-arguments class UserInput(urwid.Edit): """UserInput class""" 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 PKNM E,nncli/utils.py# -*- coding: utf-8 -*- """utils module""" import random import re import shlex import subprocess from subprocess import CalledProcessError from . import temp # pylint: disable=too-many-arguments,too-few-public-methods def get_editor(config, logger): """Get the editor""" editor = config.get_config('editor') if not editor: logger.log('No editor configured!') return None return editor def get_pager(config, logger): """Get the pager""" pager = config.get_config('pager') if not pager: logger.log('No pager configured!') return None return pager def exec_cmd_on_note(note, config, gui, logger, cmd=None, raw=False): """Execute an external command to operate on the note""" if not cmd: cmd = get_editor(config, logger) if not cmd: return None tfile = temp.tempfile_create( note if note else None, raw=raw, tempdir=config.get_config('tempdir') ) fname = temp.tempfile_name(tfile) if config.state.do_gui: focus_position = 0 try: focus_position = gui.gui_body_get().focus_position except IndexError: pass subs = {'fname': fname, 'line': focus_position + 1} cmd_list = [c.format(**subs) for c in shlex.split(cmd)] # if the filename wasn't able to be subbed, append it # this makes it fully backwards compatible with previous configs if '{fname}' not in cmd: cmd_list.append(fname) logger.log("EXECUTING: {}".format(cmd_list)) try: subprocess.check_call(cmd_list) except CalledProcessError as ex: logger.log('Command error: %s' % ex) temp.tempfile_delete(tfile) return None content = None if not raw: content = temp.tempfile_content(tfile) if not content or content == '\n': content = None temp.tempfile_delete(tfile) if config.state.do_gui: gui.nncli_loop.screen.clear() gui.nncli_loop.draw_screen() return content def generate_random_key(): """Generate random 30 digit (15 byte) hex string. stackoverflow question 2782229 """ return '%030x' % (random.randrange(256**15),) def get_note_category(note): """get a note category""" if 'category' in note: category = note['category'] if note['category'] is not None else '' else: category = '' return category def get_note_flags(note): """ get the note flags Returns a fixed length string: 'X' - needs sync '*' - favorite """ flags = '' flags += 'X' if float(note['modified']) > float(note['syncdate']) else ' ' if 'favorite' in note: flags += '*' if note['favorite'] else ' ' else: flags += ' ' return flags def get_note_title(note): """get the note title""" if 'title' in note: return note['title'] return '' def note_favorite(note): """ get the status of the note as a favorite returns True if the note is marked as a favorite False otherwise """ if 'favorite' in note: return note['favorite'] return False def sort_by_title_favorite(left): """sort notes by title, favorites on top""" return (not note_favorite(left.note), get_note_title(left.note)) def sort_notes_by_categories(notes, favorite_ontop=False): """ sort notes by category, optionally pushing favorites to the top """ notes.sort(key=lambda i: (favorite_ontop and not note_favorite(i.note), i.note.get('category'), get_note_title(i.note))) def sort_by_modify_date_favorite(left): """sort notest by modify date, favorites on top""" if note_favorite(left.note): return 100.0 * float(left.note.get('modified', 0)) return float(left.note.get('modified', 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) def build_regex_search(search_string): """ Build up a compiled regular expression from the search string. Supports the use of flags - ie. search for `nothing/i` will perform a case-insensitive regex for `nothing` """ sspat = None valid_flags = { 'i': re.IGNORECASE } if search_string: try: search_string, flag_letters = \ re.match(r'^(.+?)(?:/([a-z]+))?$', search_string).groups() flags = 0 # if flags are given, OR together all the valid flags # see https://docs.python.org/3/library/re.html#re.compile if flag_letters: for letter in flag_letters: if letter in valid_flags: flags = flags | valid_flags[letter] sspat = re.compile(search_string, flags) except re.error: sspat = None return sspat PKNM'$nncli/view_help.py# -*- coding: utf-8 -*- """view_help module""" import re import urwid class ViewHelp(urwid.ListBox): """ViewHelp class""" def __init__(self, config): self.config = config self.descr_width = 26 self.config_width = 29 lines = [] lines.extend(self.create_kb_help_lines('Keybinds Common', 'common')) lines.extend(self.create_kb_help_lines('Keybinds Note List', 'titles')) lines.extend( self.create_kb_help_lines('Keybinds Note Content', 'notes') ) lines.extend(self.create_config_help_lines()) lines.extend(self.create_color_help_lines()) lines.append(urwid.Text(('help_header', ''))) super(ViewHelp, self).__init__(urwid.SimpleFocusListWalker(lines)) def get_status_bar(self): """get the status bar""" cur = -1 total = 0 if self.body.positions(): cur = self.focus_position total = len(self.body.positions()) status_title = \ urwid.AttrMap(urwid.Text('Help', wrap='clip'), 'status_bar') status_index = \ ('pack', urwid.AttrMap(urwid.Text(' ' + str(cur + 1) + '/' + str(total)), 'status_bar')) return \ urwid.AttrMap(urwid.Columns([status_title, status_index]), 'status_bar') def create_kb_help_lines(self, header, use): """create the help page for the keybindings""" lines = [urwid.AttrMap(urwid.Text(''), 'help_header', 'help_focus')] lines.append(urwid.AttrMap(urwid.Text(' ' + header), 'help_header', 'help_focus')) for config in self.config.keybinds: if use not in self.config.get_keybind_use(config): continue keybinds_text = urwid.Text( [ ( 'help_descr', ( '{:>' + str(self.descr_width) + '} ' ).format( self.config.get_keybind_descr( config ) ) ), ( 'help_config', ( '{:>' + str(self.config_width) \ + '} ' ).format('kb_' + config) ), ( 'help_value', "'" + self.config.get_keybind(config) + "'" ) ]) lines.append( urwid.AttrMap( urwid.AttrMap( keybinds_text, 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): """create the help lines for the general config settings""" lines = [urwid.AttrMap(urwid.Text(''), 'help_header', 'help_focus')] lines.append(urwid.AttrMap(urwid.Text(' Configuration'), 'help_header', 'help_focus')) for config in self.config.configs: if config in ['nn_username', 'nn_password']: continue config_text = urwid.Text( [ ('help_descr', ('{:>' + str(self.descr_width) + '} '). format(self.config.get_config_descr(config))), ('help_config', ('{:>' + str(self.config_width) + '} '). format('cfg_' + config)), ('help_value', "'" + str(self.config.get_config(config)) + "'") ]) lines.append( urwid.AttrMap(urwid.AttrMap( config_text, 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): """create the help lines for the color settings""" lines = [urwid.AttrMap(urwid.Text(''), 'help_header', 'help_focus')] lines.append(urwid.AttrMap(urwid.Text(' Colors'), 'help_header', 'help_focus')) fmap = {} for config in self.config.colors: fmap[re.search('^(.*)(_fg|_bg)$', config).group(1)] = 'help_focus' for color in self.config.colors: colors_text = urwid.Text( [ ('help_descr', ('{:>' + str(self.descr_width) + '} '). format(self.config.get_color_descr(color))), ('help_config', ('{:>' + str(self.config_width) + '} '). format('clr_' + color)), (re.search('^(.*)(_fg|_bg)$', color).group(1), "'" + self.config.get_color(color) + "'") ]) lines.append( urwid.AttrMap(urwid.AttrMap( colors_text, attr_map=None, focus_map=fmap ), 'default', 'help_focus')) return lines def keypress(self, size, key): return key PKC[hN)W  nncli/view_log.py# -*- coding: utf-8 -*- """view_log module""" import urwid class ViewLog(urwid.ListBox): """ ViewLog class This class defines the urwid view class for the log viewer """ def __init__(self, config, logger): self.config = config self.logger = logger super(ViewLog, self).__init__(urwid.SimpleFocusListWalker([])) def update_log(self): """update the log""" lines = [] with open(self.logger.logfile) as logfile: for line in logfile: lines.append( urwid.AttrMap(urwid.Text(line.rstrip()), 'note_content', 'note_content_focus') ) if self.config.get_config('log_reversed') == 'yes': lines.reverse() self.body[:] = urwid.SimpleFocusListWalker(lines) self.focus_position = 0 def get_status_bar(self): """get the log view status bar""" cur = -1 total = 0 if self.body.positions(): cur = self.focus_position total = len(self.body.positions()) status_title = \ urwid.AttrMap(urwid.Text('Sync Log', wrap='clip'), 'status_bar') status_index = \ ('pack', urwid.AttrMap(urwid.Text(' ' + str(cur + 1) + '/' + str(total)), 'status_bar')) return \ urwid.AttrMap(urwid.Columns([status_title, status_index]), 'status_bar') def keypress(self, size, key): return key PKNML]NNnncli/view_note.py# -*- coding: utf-8 -*- """view_note module""" import time import urwid from . import utils from .clipboard import Clipboard # pylint: disable=too-many-instance-attributes class ViewNote(urwid.ListBox): """ ViewNote class This class defines the urwid class responsible for displaying an individual note in an internal pager """ def __init__(self, config, args): self.config = config self.ndb = args['ndb'] self.key = args['id'] self.log = args['log'] self.search_string = '' self.search_mode = 'gstyle' self.search_direction = '' 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')) self.clipboard = Clipboard() super(ViewNote, self).__init__( urwid.SimpleFocusListWalker(self.get_note_content_as_list())) def get_note_content_as_list(self): """return the contents of a note as a list of strings""" lines = [] if not self.key: return lines if self.old_note: for line in self.old_note['content'].split('\n'): lines.append( urwid.AttrMap(urwid.Text( line.replace('\t', ' ' * self.tabstop)), 'note_content_old', 'note_content_old_focus')) else: for line in self.note['content'].split('\n'): lines.append( urwid.AttrMap(urwid.Text( line.replace('\t', ' ' * self.tabstop)), 'note_content', 'note_content_focus')) lines.append(urwid.AttrMap(urwid.Divider('-'), 'default')) return lines def update_note_view(self, key=None): """update the view""" if key: # setting a new note self.key = key self.note = self.ndb.get_note(self.key) self.old_note = None self.body[:] = \ urwid.SimpleFocusListWalker(self.get_note_content_as_list()) if not self.search_string: self.focus_position = 0 def lines_after_current_position(self): """ return the number of lines after the currently-focused line """ lines_after_current_position = \ list(range(self.focus_position + 1, len(self.body.positions()) - 1)) return lines_after_current_position def lines_before_current_position(self): """ return the number of lines before the currently-focused line """ lines_before_current_position = list(range(0, self.focus_position)) lines_before_current_position.reverse() return lines_before_current_position def search_note_view_next(self, search_string=None, search_mode=None): """move to the next match in search mode""" if search_string: self.search_string = search_string if search_mode: self.search_mode = search_mode note_range = self.lines_after_current_position() \ if self.search_direction == 'forward' \ else self.lines_before_current_position() self.search_note_range(note_range) def search_note_view_prev(self, search_string=None, search_mode=None): """move to the previous match in search mode""" if search_string: self.search_string = search_string if search_mode: self.search_mode = search_mode note_range = self.lines_after_current_position() \ if self.search_direction == 'backward' \ else self.lines_before_current_position() self.search_note_range(note_range) def search_note_range(self, note_range): """search within a range of lines""" for line in note_range: line_content = self.note['content'].split('\n')[line] if self.is_match(self.search_string, line_content): self.focus_position = line break self.update_note_view() def is_match(self, term, full_text): """returns True if there is a match, False otherwise""" if self.search_mode == 'gstyle': return term in full_text sspat = utils.build_regex_search(term) return sspat and sspat.search(full_text) def get_status_bar(self): """get the note view status bar""" if not self.key: return \ urwid.AttrMap(urwid.Text('No note...'), 'status_bar') cur = -1 total = 0 if self.body.positions(): cur = self.focus_position total = len(self.body.positions()) localtime = time.localtime(float(self.note['modified'])) title = utils.get_note_title(self.note) flags = utils.get_note_flags(self.note) category = utils.get_note_category(self.note) mod_time = time.strftime('Date: %a, %d %b %Y %H:%M:%S', localtime) status_title = \ urwid.AttrMap(urwid.Text('Title: ' + title, wrap='clip'), 'status_bar') status_key_index = \ ('pack', urwid.AttrMap(urwid.Text(' [' + str(self.key) + '] ' + str(cur + 1) + '/' + str(total)), 'status_bar')) status_date = \ urwid.AttrMap(urwid.Text(mod_time, wrap='clip'), 'status_bar') status_category_flags = \ ('pack', urwid.AttrMap(urwid.Text('[' + category + '] [' + flags + ']'), 'status_bar')) pile_top = urwid.Columns([status_title, status_key_index]) pile_bottom = urwid.Columns([status_date, status_category_flags]) return \ urwid.AttrMap(urwid.Pile([pile_top, pile_bottom]), 'status_bar') def copy_note_text(self): """copy the text of the note to the system clipboard""" line_content = self.note['content'].split('\n')[self.focus_position] self.clipboard.copy(line_content) 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 PKNMz/$$nncli/view_titles.py# -*- coding: utf-8 -*- """view_titles module""" import re import time import datetime import urwid from . import utils # pylint: disable=too-many-instance-attributes, too-many-statements class ViewTitles(urwid.ListBox): """ ViewTitles class Implements the urwid class for the view_titles view """ 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, sort_mode=self.config.get_config('sort_mode') ) super(ViewTitles, self).__init__( urwid.SimpleFocusListWalker(self.get_note_titles())) def update_note_list(self, search_string, search_mode='gstyle', sort_mode='date'): """update the note list""" self.search_string = search_string self.note_list, self.match_regex, self.all_notes_cnt = \ self.ndb.filter_notes( self.search_string, search_mode, sort_mode=sort_mode ) self.body[:] = \ urwid.SimpleFocusListWalker(self.get_note_titles()) if not self.note_list: self.log('No notes found!') else: self.focus_position = 0 def sort_note_list(self, sort_mode): """sort the note list""" 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 -- category %D -- date %N -- note title """ localtime = time.localtime(float(note['modified'])) mod_time = \ time.strftime(self.config.get_config('format_strftime'), localtime) title = utils.get_note_title(note) flags = utils.get_note_flags(note) category = utils.get_note_category(note) # get the age of the note dtime = datetime.datetime.fromtimestamp(time.mktime(localtime)) if dtime > datetime.datetime.now() - datetime.timedelta(days=1): note_age = 'd' # less than a day old elif dtime > datetime.datetime.now() - datetime.timedelta(weeks=1): note_age = 'w' # less than a week old elif dtime > datetime.datetime.now() - datetime.timedelta(weeks=4): note_age = 'm' # less than a month old elif dtime > 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(r'^(.*)%([-]*)([0-9]*)([FDTN])(.*)$', title_format) if not fmt: attr_map = ('pack', urwid.AttrMap(urwid.Text(title_format), 'default')) l_fmt = None r_fmt = None else: left = fmt.group(1) if fmt.group(1) else None attr_map = None right = 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': attr_map = (width, urwid.AttrMap(urwid.Text(flags, align=align, wrap='clip'), 'note_flags')) elif fmt.group(4) == 'D': attr_map = (width, urwid.AttrMap(urwid.Text(mod_time, align=align, wrap='clip'), 'note_date')) elif fmt.group(4) == 'T': attr_map = (width, urwid.AttrMap(urwid.Text(category, align=align, wrap='clip'), 'note_category')) 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': attr_map = (width, urwid.AttrMap( urwid.Text( title, align=align, wrap='clip' ), attr)) else: attr_map = urwid.AttrMap(urwid.Text(title, align=align, wrap='clip'), attr) l_fmt = recursive_format(left) r_fmt = recursive_format(right) tmp = [] if l_fmt: tmp.extend(l_fmt) tmp.append(attr_map) 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): """get the title of a 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_categories' : 'note_focus'}) def get_note_titles(self): """get the titles of all of the notes""" lines = [] for note in self.note_list: lines.append(self.get_note_title(note.note)) return lines def get_status_bar(self): """get the status bar""" cur = -1 total = 0 if self.body.positions(): cur = self.focus_position total = len(self.body.positions()) hdr = 'NextCloud Notes' # include connection status in header hdr += ' (' + self.ndb.note.status + ')' if self.search_string is not None: hdr += ' - Search: ' + self.search_string status_title = \ urwid.AttrMap(urwid.Text(hdr, wrap='clip'), 'status_bar') status_index = \ ('pack', urwid.AttrMap(urwid.Text(' ' + str(cur + 1) + '/' + str(total)), 'status_bar')) return \ urwid.AttrMap(urwid.Columns([status_title, status_index]), 'status_bar') def update_note_title(self, key=None): """update a note title""" if not key: self.body[self.focus_position] = \ self.get_note_title(self.note_list[self.focus_position].note) else: for i in range(len(self.note_list)): if self.note_list[i].note['localkey'] == key: self.body[i] = self.get_note_title(self.note_list[i].note) def focus_note(self, key): """set the focus on a given note""" for i in range(len(self.note_list)): if 'localkey' in self.note_list[i].note and \ self.note_list[i].note['localkey'] == key: self.focus_position = i def keypress(self, size, key): return key PK!H2%(&nncli-0.3.4.dist-info/entry_points.txtN+I/N.,()Kɴz@lPK2|M3֋nncli-0.3.4.dist-info/LICENSE The MIT License (MIT) Copyright (c) 2018 Daniel Moch 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. ------------------------------------------------------------------------------- Portions of this software are adapted from sncli by Eric Davis: ** 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. :-) ------------------------------------------------------------------------------- Portions of nextcloud_note.py are adapted from simplenote.py by Daniel Schauenberg: Copyright (c) 2011 Daniel Schauenberg 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. ------------------------------------------------------------------------------- Portions of notes_db and utils modules are adapted from the corresponding module in nvpy by Charl P. Botha: Copyright (c) 2012, Charl P. Botha All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL CHARL P. BOTHA BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. PK!HPOnncli-0.3.4.dist-info/WHEEL HM K-*ϳR03rOK-J,/RH,szd&Y)r$[)T&UrPK!HM[snncli-0.3.4.dist-info/METADATAX[s}@=&;Vޭkdz^' "!  0H(Qf2|wYcN7KZ}(ҧ NU>7Һ1f+C:{|Λ1 _6j#C gWWc*۬zP$CܥO>٣B\WJ6[g:OPLZwcS1⎕r^hg<υifv,ڧBdlސ7 [jfzX?!>j"WA*%:]/|QXgDY7*$k֢]HUXM0'6P brQNY,K:h! FlE1ʍE!"BꜝkEJ,i;uU|ڤ,FT9tqs/]CnpV-߀e\˕c!@ Hv.-s s ]m&Olޕ|F_ofdpWn>u5ݐ E|$Se ־0g$MNqhJp+I0s\^"3AD|Nw\y aG8]ZqWHlѲWRUGBI@Q ! F4JM U|H%@bH7Z[)^Ei!0 SТ|@f(4ެ03aR%qK_Eb5 x .WwHn借[r vaI0c*͋‚JoH+3K{^9"y.gvG&5Q砑\`mCAi(Ă04}w!eYe=+Vi\\h$+da6$fƅ0Oi a^E4H\M Y\E0B4 I~4n,O22g1( ւR5LlRpk‚O!F!* r&?5൯8+03L KpgCWlDfO2~sib%Ms]GvKY|IQ'[wY3`Jo9!˷C ōlA0[MTC$TG\Q$X$C O:T[M|Kp,a-41QRXe#u5C*g4/,ViP"b8(毤5hXgMڋETg? _}ʽAcApCģ#~KsF&,=;PsuyI 0/"=JTOEcjׂR?1) yW*]Dh =w>h;GZ\'`^B *ð'(IZ/o!Y[L{ PKahNI1=\\nncli/__init__.pyPKNM$^nncli/__main__.pyPKNMAB Unncli/cli.pyPKNM*nncli/clipboard.pyPKNM6FQhQhnncli/config.pyPKt]hN/.__ Wnncli/gui.pyPKNM