Esempio n. 1
0
 def select(self):
     """Toggles selection of the current label."""
     LOGGER.debug('Select command triggered.')
     # get current label
     label, cur_y = self.get_current_label()
     # toggle selection
     if label not in self.selection:
         LOGGER.info("Adding '%s' to the selection.", label)
         self.selection.add(label)
         # Note, that we use an additional two spaces to attempt to uniquely identify the label
         # in the list mode. Otherwise it might be possible that the same text (as used for the
         # label) can occur elsewhere in the buffer.
         # We do not need this outside of the list view because then the line indexed by `cur_y`
         # will surely only include the one label which we actually want to operate on.
         offset = '  ' if self.list_mode == -1 else ''
         self.buffer.replace(
             cur_y, label + offset,
             CONFIG.get_ansi_color('selection') + label + '\x1b[0m' +
             offset)
     else:
         LOGGER.info("Removing '%s' from the selection.", label)
         self.selection.remove(label)
         self.buffer.replace(
             cur_y,
             CONFIG.get_ansi_color('selection') + label + '\x1b[0m', label)
     # update buffer view
     self.buffer.view(self.viewport,
                      self.visible,
                      self.width - 1,
                      ansi_map=self.ANSI_MAP)
Esempio n. 2
0
def test_add_overwrite_label():
    """Test add command while specifying a label manually.

    Regression test against #4.
    """
    # use temporary config
    tmp_config = "[DATABASE]\nfile=/tmp/cobib_test_database.yaml\n"
    with open('/tmp/cobib_test_config.ini', 'w') as file:
        file.write(tmp_config)
    CONFIG.set_config(Path('/tmp/cobib_test_config.ini'))
    # ensure database file exists and is empty
    open('/tmp/cobib_test_database.yaml', 'w').close()
    # freshly read in database to overwrite anything that was read in during setup()
    read_database(fresh=True)
    # add some data
    commands.AddCommand().execute(['-b', './test/example_literature.bib'])
    # add potentially duplicate entry
    commands.AddCommand().execute([
        '-b', './test/example_duplicate_entry.bib', '--label',
        'duplicate_resolver'
    ])
    # compare with reference file
    with open('./test/example_literature.yaml', 'r') as expected:
        true_lines = expected.readlines()
    with open('./test/example_duplicate_entry.yaml', 'r') as extra:
        true_lines += extra.readlines()
    with open('/tmp/cobib_test_database.yaml', 'r') as file:
        for line, truth in zip_longest(file, true_lines):
            assert line == truth
    # clean up file system
    os.remove('/tmp/cobib_test_database.yaml')
    os.remove('/tmp/cobib_test_config.ini')
Esempio n. 3
0
def test_tui_quit_prompt(setting, keys):
    """Test the prompt_before_quit setting of the TUI."""
    # ensure configuration is empty
    CONFIG.config = {}
    root = os.path.abspath(os.path.dirname(__file__))
    CONFIG.set_config(Path(root + '/../cobib/docs/debug.ini'))
    # set prompt_before_quit setting
    CONFIG.config['TUI']['prompt_before_quit'] = setting
    read_database()
    test_tui(None, keys, assert_quit, {'prompt': setting})
Esempio n. 4
0
def test_tui_scrolling(keys, assertion, assertion_kwargs):
    """Test TUI scrolling behavior."""
    # ensure configuration is empty
    CONFIG.config = {}
    root = os.path.abspath(os.path.dirname(__file__))
    CONFIG.set_config(Path(root + '/../cobib/docs/debug.ini'))
    # overwrite database file
    CONFIG.config['DATABASE']['file'] = './test/scrolling_database.yaml'
    read_database()
    test_tui(None, keys, assertion, assertion_kwargs)
Esempio n. 5
0
def setup():
    """Setup."""
    # ensure configuration is empty
    CONFIG.config = {}
    root = os.path.abspath(os.path.dirname(__file__))
    CONFIG.set_config(Path(root + '/../cobib/docs/debug.ini'))
    # NOTE: normally you would never trigger an Add command before reading the database but in this
    # controlled testing scenario we can be certain that this is fine
    AddCommand().execute(['-b', './test/dummy_scrolling_entry.bib'])
    read_database()
    yield setup
    DeleteCommand().execute(['dummy_entry_for_scroll_testing'])
Esempio n. 6
0
def test_set_config(setup):
    """Test config setting.

    Args:
        setup: runs pytest fixture.
    """
    # from setup
    assert CONFIG.config['DATABASE'][
        'file'] == './test/example_literature.yaml'
    # change back to default
    CONFIG.set_config()
    assert CONFIG.config['DATABASE']['file'] == \
        os.path.expanduser('~/.local/share/cobib/literature.yaml')
Esempio n. 7
0
def test_tui_open_menu():
    """Test the open prompt menu for multiple associated files."""
    # ensure configuration is empty
    CONFIG.config = {}
    root = os.path.abspath(os.path.dirname(__file__))
    CONFIG.set_config(Path(root + '/../cobib/docs/debug.ini'))
    # NOTE: normally you would never trigger an Add command before reading the database but in this
    # controlled testing scenario we can be certain that this is fine
    AddCommand().execute(['-b', './test/dummy_multi_file_entry.bib'])
    read_database()
    try:
        test_tui(None, 'o', assert_open, {})
    finally:
        DeleteCommand().execute(['dummy_multi_file_entry'])
Esempio n. 8
0
def test_tui_config_color():
    """Test TUI color configuration."""
    # ensure configuration is empty
    CONFIG.config = {}
    root = os.path.abspath(os.path.dirname(__file__))
    CONFIG.set_config(Path(root + '/../cobib/docs/debug.ini'))
    # overwrite color configuration
    CONFIG.config['COLORS']['top_statusbar_bg'] = 'red'
    CONFIG.config['COLORS']['top_statusbar_fg'] = 'blue'
    CONFIG.config['COLORS']['bottom_statusbar_bg'] = 'green'
    CONFIG.config['COLORS']['bottom_statusbar_fg'] = 'magenta'
    CONFIG.config['COLORS']['cursor_line_bg'] = 'white'
    CONFIG.config['COLORS']['cursor_line_fg'] = 'black'
    read_database()
    test_tui(None, '', assert_config_color, {'colors': CONFIG.config['COLORS']})
Esempio n. 9
0
def list_tags(args=None):
    """List all tags.

    Args:
        args (dict, optional): dictionary of keyword arguments.

    Returns:
        A list of all available tags in the database.
    """
    if not args:
        args = {}
    CONFIG.set_config(args.get('config', None))
    read_database()
    tags = list(CONFIG.config['BIB_DATA'].keys())
    return tags
Esempio n. 10
0
File: show.py Progetto: dinosv/cobib
    def tui(tui):
        """See base class."""
        LOGGER.debug('Show command triggered from TUI.')
        # get current label
        label, cur_y = tui.get_current_label()
        # populate buffer with entry data
        LOGGER.debug('Clearing current buffer contents.')
        tui.buffer.clear()
        ShowCommand().execute([label], out=tui.buffer)
        tui.buffer.split()
        if label in tui.selection:
            LOGGER.debug('Current entry is selected. Applying highlighting.')
            tui.buffer.replace(0, label, CONFIG.get_ansi_color('selection') + label + '\x1b[0m')
        LOGGER.debug('Populating buffer with ShowCommand result.')
        tui.buffer.view(tui.viewport, tui.visible, tui.width-1, ansi_map=tui.ANSI_MAP)

        # reset current cursor position
        tui.top_line = 0
        tui.current_line = 0
        # update top statusbar
        tui.topstatus = "CoBib v{} - {}".format(__version__, label)
        tui.statusbar(tui.topbar, tui.topstatus)
        # enter show menu
        tui.list_mode = cur_y
        tui.inactive_commands = ['Add', 'Filter', 'Search', 'Show', 'Sort']
Esempio n. 11
0
def test_tui_config_keys(command, key):
    """Test TUI key binding configuration."""
    # ensure configuration is empty
    CONFIG.config = {}
    root = os.path.abspath(os.path.dirname(__file__))
    CONFIG.set_config(Path(root + '/../cobib/docs/debug.ini'))
    # overwrite key binding configuration
    CONFIG.config['KEY_BINDINGS'][command] = key
    # NOTE: normally you would never trigger an Add command before reading the database but in this
    # controlled testing scenario we can be certain that this is fine
    AddCommand().execute(['-b', './test/dummy_scrolling_entry.bib'])
    read_database()
    try:
        test_tui(None, key, assert_show, {})
    finally:
        DeleteCommand().execute(['dummy_entry_for_scroll_testing'])
Esempio n. 12
0
def test_init():
    """Test init command."""
    # use temporary config
    tmp_config = "[DATABASE]\nfile=/tmp/cobib_test_database.yaml\n"
    with open('/tmp/cobib_test_config.ini', 'w') as file:
        file.write(tmp_config)
    CONFIG.set_config(Path('/tmp/cobib_test_config.ini'))
    # store current time
    now = float(datetime.now().timestamp())
    commands.InitCommand().execute({})
    # check creation time of temporary database file
    ctime = os.stat('/tmp/cobib_test_database.yaml').st_ctime
    # assert these times are close
    assert ctime - now < 0.1 or now - ctime < 0.1
    # clean up file system
    os.remove('/tmp/cobib_test_database.yaml')
    os.remove('/tmp/cobib_test_config.ini')
Esempio n. 13
0
def test_init_force():
    """Test init can be forced when database file exists."""
    # use temporary config
    tmp_config = "[DATABASE]\nfile=/tmp/cobib_test_database.yaml\n"
    with open('/tmp/cobib_test_config.ini', 'w') as file:
        file.write(tmp_config)
    CONFIG.set_config(Path('/tmp/cobib_test_config.ini'))
    # fill database file
    with open('/tmp/cobib_test_database.yaml', 'w') as file:
        file.write('test')
    # try running init
    commands.InitCommand().execute(['-f'])
    # check init was forced and database file was overwritten
    assert os.stat('/tmp/cobib_test_database.yaml').st_size == 0
    # clean up file system
    os.remove('/tmp/cobib_test_database.yaml')
    os.remove('/tmp/cobib_test_config.ini')
Esempio n. 14
0
def list_filters(args=None):
    """List all filters.

    Args:
        args (dict, optional): dictionary of keyword arguments.

    Returns:
        A list of all field names available for filtering.
    """
    if not args:
        args = {}
    CONFIG.set_config(args.get('config', None))
    read_database()
    filters = set()
    for entry in CONFIG.config['BIB_DATA'].values():
        filters.update(entry.data.keys())
    return filters
Esempio n. 15
0
def test_init_safe():
    """Test init aborts when database file exists."""
    # use temporary config
    tmp_config = "[DATABASE]\nfile=/tmp/cobib_test_database.yaml\n"
    with open('/tmp/cobib_test_config.ini', 'w') as file:
        file.write(tmp_config)
    CONFIG.set_config(Path('/tmp/cobib_test_config.ini'))
    # fill database file
    with open('/tmp/cobib_test_database.yaml', 'w') as file:
        file.write('test')
    # try running init
    commands.InitCommand().execute({})
    # check init aborted and database file still contains 'test'
    with open('/tmp/cobib_test_database.yaml', 'r') as file:
        assert file.read() == 'test'
    # clean up file system
    os.remove('/tmp/cobib_test_database.yaml')
    os.remove('/tmp/cobib_test_config.ini')
Esempio n. 16
0
def test_add():
    """Test add command."""
    # use temporary config
    tmp_config = "[DATABASE]\nfile=/tmp/cobib_test_database.yaml\n"
    with open('/tmp/cobib_test_config.ini', 'w') as file:
        file.write(tmp_config)
    CONFIG.set_config(Path('/tmp/cobib_test_config.ini'))
    # ensure database file exists and is empty
    open('/tmp/cobib_test_database.yaml', 'w').close()
    # freshly read in database to overwrite anything that was read in during setup()
    read_database(fresh=True)
    # add some data
    commands.AddCommand().execute(['-b', './test/example_literature.bib'])
    # compare with reference file
    with open('/tmp/cobib_test_database.yaml', 'r') as file:
        with open('./test/example_literature.yaml', 'r') as expected:
            for line, truth in zip_longest(file, expected):
                assert line == truth
    # clean up file system
    os.remove('/tmp/cobib_test_database.yaml')
    os.remove('/tmp/cobib_test_config.ini')
Esempio n. 17
0
def test_delete(labels):
    """Test delete command."""
    # use temporary config
    tmp_config = "[DATABASE]\nfile=/tmp/cobib_test_database.yaml\n"
    with open('/tmp/cobib_test_config.ini', 'w') as file:
        file.write(tmp_config)
    CONFIG.set_config(Path('/tmp/cobib_test_config.ini'))
    # copy example database to configured location
    copyfile(Path('./test/example_literature.yaml'),
             Path('/tmp/cobib_test_database.yaml'))
    # delete some data
    # NOTE: for testing simplicity we delete the last entry
    commands.DeleteCommand().execute(labels)
    with open('/tmp/cobib_test_database.yaml', 'r') as file:
        with open('./test/example_literature.yaml', 'r') as expected:
            # NOTE: do NOT use zip_longest to omit last entry (thus, we deleted the last one)
            for line, truth in zip(file, expected):
                assert line == truth
            with pytest.raises(StopIteration):
                file.__next__()
    # clean up file system
    os.remove('/tmp/cobib_test_database.yaml')
    os.remove('/tmp/cobib_test_config.ini')
Esempio n. 18
0
 def tui(tui):
     """See base class."""
     LOGGER.debug('Search command triggered from TUI.')
     tui.buffer.clear()
     # handle input via prompt
     command, results = tui.execute_command('search', out=tui.buffer)
     if tui.buffer.lines and results is not None:
         hits, labels = results
         tui.list_mode, _ = tui.viewport.getyx()
         tui.buffer.split()
         LOGGER.debug('Applying selection highlighting in search results.')
         for label in labels:
             if label not in tui.selection:
                 continue
             # we match the label including its 'search_label' highlight to ensure that we really
             # only match this specific occurrence of whatever the label may be
             tui.buffer.replace(range(tui.buffer.height),
                                CONFIG.get_ansi_color('search_label') + label + '\x1b[0m',
                                CONFIG.get_ansi_color('search_label') +
                                CONFIG.get_ansi_color('selection') + label + '\x1b[0m\x1b[0m')
         LOGGER.debug('Populating viewport with search results.')
         tui.buffer.view(tui.viewport, tui.visible, tui.width-1, ansi_map=tui.ANSI_MAP)
         # reset current cursor position
         LOGGER.debug('Resetting cursor position to top.')
         tui.top_line = 0
         tui.current_line = 0
         # update top statusbar
         tui.topstatus = "CoBib v{} - {} hit{}".format(__version__, hits,
                                                       "s" if hits > 1 else "")
         tui.statusbar(tui.topbar, tui.topstatus)
         tui.inactive_commands = ['Add', 'Filter', 'Sort']
     elif command[1:]:
         msg = f"No search hits for '{shlex.join(command[1:])}'!"
         LOGGER.info(msg)
         tui.prompt_print(msg)
         tui.update_list()
Esempio n. 19
0
    def colors():
        """Initialize the color pairs for the curses TUI."""
        # Start colors in curses
        curses.start_color()
        # parse user color configuration
        color_cfg = CONFIG.config['COLORS']
        colors = {col: {} for col in TUI.COLOR_NAMES}
        for attr, col in color_cfg.items():
            if attr in TUI.COLOR_VALUES.keys():
                if not curses.can_change_color():
                    # cannot change curses default colors
                    LOGGER.warning(
                        'Curses cannot change the default colors. Skipping color setup.'
                    )
                    continue
                # update curses-internal color with HEX-color
                rgb_color = tuple(
                    int(col.strip('#')[i:i + 2], 16) for i in (0, 2, 4))
                # curses colors range from 0 to 1000
                curses_color = tuple(col * 1000 // 255 for col in rgb_color)
                curses.init_color(TUI.COLOR_VALUES[attr], *curses_color)
            else:
                if attr[:-3] not in TUI.COLOR_NAMES:
                    LOGGER.warning(
                        'Detected unknown TUI color name specification: %s',
                        attr[:-3])
                    continue
                colors[attr[:-3]][attr[-2:]] = col

        # initialize color pairs for TUI elements
        for idx, attr in enumerate(TUI.COLOR_NAMES):
            foreground = colors[attr].get('fg', 'white')
            background = colors[attr].get('bg', 'black')
            LOGGER.debug('Initiliazing color pair %d for %s', idx + 1, attr)
            curses.init_pair(idx + 1, TUI.COLOR_VALUES[foreground],
                             TUI.COLOR_VALUES[background])
            LOGGER.debug('Adding ANSI color code for %s', attr)
            TUI.ANSI_MAP[CONFIG.get_ansi_color(
                attr)] = TUI.COLOR_NAMES.index(attr) + 1
Esempio n. 20
0
 def update_list(self):
     """Updates the default list view."""
     LOGGER.debug('Re-populating the viewport with the list command.')
     self.buffer.clear()
     labels = commands.ListCommand().execute(self.list_args,
                                             out=self.buffer)
     labels = labels or []  # convert to empty list if labels is None
     # populate buffer with the list
     if self.list_mode >= 0:
         self.current_line = self.list_mode
         self.list_mode = -1
     # reset viewport
     self.top_line = 0
     self.left_edge = 0
     self.inactive_commands = []
     # highlight current selection
     for label in self.selection:
         # Note: the two spaces are explained in the `select()` method.
         # Also: this step may become a performance bottleneck because we replace inside the
         # whole buffer for each selected label!
         self.buffer.replace(
             range(self.buffer.height), label + '  ',
             CONFIG.get_ansi_color('selection') + label + '\x1b[0m  ')
     # display buffer in viewport
     self.buffer.view(self.viewport,
                      self.visible,
                      self.width - 1,
                      ansi_map=self.ANSI_MAP)
     # update top statusbar
     self.topstatus = "CoBib v{} - {} Entries".format(
         __version__, len(labels))
     self.statusbar(self.topbar, self.topstatus)
     # if cursor position is out-of-view (due to e.g. top-line reset in Show command), reset the
     # top-line such that the current line becomes visible again
     if self.current_line > self.top_line + self.visible:
         self.top_line = min(self.current_line,
                             self.buffer.height - self.visible)
Esempio n. 21
0
    def execute(self, args, out=sys.stdout):
        """Search database.

        Searches the database recursively (i.e. including any associated files) using `grep` for a
        query string.

        Args: See base class.
        """
        LOGGER.debug('Starting Search command.')
        parser = ArgumentParser(prog="search", description="Search subcommand parser.")
        parser.add_argument("query", type=str, help="text to search for")
        parser.add_argument("-c", "--context", type=int, default=1,
                            help="number of context lines to provide for each match")
        parser.add_argument("-i", "--ignore-case", action="store_true",
                            help="ignore case for searching")
        parser.add_argument('list_arg', nargs='*',
                            help="Any arguments for the List subcommand." +
                            "\nUse this to add filters to specify a subset of searched entries." +
                            "\nYou can add a '--' before the List arguments to ensure separation." +
                            "\nSee also `list --help` for more information on the List arguments.")

        if not args:
            parser.print_usage(sys.stderr)
            sys.exit(1)

        try:
            largs = parser.parse_intermixed_args(args)
        except argparse.ArgumentError as exc:
            print("{}: {}".format(exc.argument_name, exc.message), file=sys.stderr)
            return None

        labels = ListCommand().execute(largs.list_arg, out=open(os.devnull, 'w'))
        LOGGER.debug('Available entries to search: %s', labels)

        ignore_case = CONFIG.config['DATABASE'].getboolean('search_ignore_case', False) or \
            largs.ignore_case
        re_flags = re.IGNORECASE if ignore_case else 0
        LOGGER.debug('The search will be performed case %ssensitive', 'in' if ignore_case else '')

        hits = 0
        output = []
        for label in labels.copy():
            entry = CONFIG.config['BIB_DATA'][label]
            matches = entry.search(largs.query, largs.context, ignore_case)
            if not matches:
                labels.remove(label)
                continue

            hits += len(matches)
            LOGGER.debug('Entry "%s" includes %d hits.', label, hits)
            title = f"{label} - {len(matches)} match" + ("es" if len(matches) > 1 else "")
            title = title.replace(label, CONFIG.get_ansi_color('search_label') + label + '\x1b[0m')
            output.append(title)

            for idx, match in enumerate(matches):
                for line in match:
                    line = re.sub(rf'({largs.query})',
                                  CONFIG.get_ansi_color('search_query') + r'\1' + '\x1b[0m',
                                  line, flags=re_flags)
                    output.append(f"[{idx+1}]\t".expandtabs(8) + line)

        print('\n'.join(output), file=out)
        return (hits, labels)
Esempio n. 22
0
def main():
    """Main executable.

    CoBib's main function used to parse optional keyword arguments and subcommands.
    """
    if len(sys.argv) > 1 and any([a[0] == '_' for a in sys.argv]):
        # zsh helper function called
        zsh_main()
        sys.exit()

    # initialize logging
    log_to_stream()

    subcommands = [cmd.split(':')[0] for cmd in zsh_helper.list_commands()]
    parser = argparse.ArgumentParser(prog='CoBib',
                                     description="""
                                     Cobib input arguments.
                                     If no arguments are given, the TUI will start as a default.
                                     """)
    parser.add_argument("--version",
                        action="version",
                        version="%(prog)s v{}".format(__version__))
    parser.add_argument('--verbose', '-v', action='count', default=0)
    parser.add_argument("-l",
                        "--logfile",
                        type=argparse.FileType('w'),
                        help="Alternative log file")
    parser.add_argument("-c",
                        "--config",
                        type=argparse.FileType('r'),
                        help="Alternative config file")
    parser.add_argument('command',
                        help="subcommand to be called",
                        choices=subcommands,
                        nargs='?')
    parser.add_argument('args', nargs=argparse.REMAINDER)

    args = parser.parse_args()

    if args.logfile:
        LOGGER.info('Switching to FileHandler logger in %s', args.logfile.name)
        log_to_file('DEBUG' if args.verbose > 1 else 'INFO',
                    logfile=args.logfile.name)

    # set logging verbosity level
    if args.verbose == 1:
        logging.getLogger().setLevel(logging.INFO)
        LOGGER.info('Logging level set to INFO.')
    elif args.verbose > 1:
        logging.getLogger().setLevel(logging.DEBUG)
        LOGGER.info('Logging level set to DEBUG.')

    CONFIG.set_config(args.config)
    try:
        CONFIG.validate()
    except RuntimeError as exc:
        LOGGER.error(exc)
        sys.exit(1)

    if args.command == 'init':
        # the database file may not exist yet, thus we ensure to execute the command before trying
        # to read the database file
        subcmd = getattr(commands, 'InitCommand')()
        subcmd.execute(args.args)
        return

    read_database()
    if not args.command:
        if args.logfile is None:
            LOGGER.info('Switching to FileHandler logger in %s',
                        '/tmp/cobib.log')
            log_to_file('DEBUG' if args.verbose > 1 else 'INFO')
        else:
            LOGGER.info(
                'Already logging to %s. NOT switching to "/tmp/cobib.log"',
                args.logfile)
        tui()
    else:
        subcmd = getattr(commands, args.command.title() + 'Command')()
        subcmd.execute(args.args)
Esempio n. 23
0
def test_valid_tui_colors(setup, color):
    """Test curses color specification validation."""
    with pytest.raises(RuntimeError) as exc_info:
        CONFIG.config.get('COLORS', {})[color] = 'test'
        CONFIG.validate()
    assert str(exc_info.value) == 'Unknown color specification: test'
Esempio n. 24
0
def test_database_section(setup, section, field):
    """Test raised RuntimeError for missing config fields."""
    with pytest.raises(RuntimeError) as exc_info:
        del CONFIG.config.get(section, {})[field]
        CONFIG.validate()
    assert f'{section}/{field}' in str(exc_info.value)
Esempio n. 25
0
def test_missing_section(setup, section):
    """Test raised RuntimeError for missing configuration section."""
    with pytest.raises(RuntimeError) as exc_info:
        del CONFIG.config[section]
        CONFIG.validate()
    assert section in str(exc_info.value)
Esempio n. 26
0
def test_base_config(setup):
    """Test the initial configuration passes all validation checks."""
    CONFIG.validate()
Esempio n. 27
0
def setup():
    """Setup."""
    # ensure configuration is empty
    CONFIG.config = {}
    root = os.path.abspath(os.path.dirname(__file__))
    CONFIG.set_config(Path(root + '/../cobib/docs/debug.ini'))
Esempio n. 28
0
def setup():
    """Setup."""
    root = os.path.abspath(os.path.dirname(__file__))
    CONFIG.set_config(Path(root + '/../cobib/docs/debug.ini'))
    read_database()