def delete_cards(config, force, noninteractive, tags, no_tags, match, listname, attachments, has_due): '''Delete a set of cards specified ''' _, board = BoardTool.start(config) display = Display(config.color) if config.banner and not json: display.banner() cards = BoardTool.filter_cards( board, tags=tags, no_tags=no_tags, title_regex=match, list_regex=listname, has_attachments=attachments, has_due_date=has_due ) method = 'delete' if force else 'archive' if noninteractive: if force: [c.delete() for c in cards] else: [c.set_closed(True) for c in cards] else: for card in cards: display.show_card(card) if prompt_for_confirmation('Delete this card?'): if force: card.delete() else: card.set_closed(True) click.secho('Card {}d!'.format(method), fg='red')
def review(config, tags, no_tags, match, listname, attachments, has_due, by_due): '''show a smart, command-line based menu for each card selected. This menu will prompt you to add tags to untagged cards, to attach the title of cards which have a link in the title, and gives you all the other functionality combined. ''' connection, board = BoardTool.start(config) if by_due: cards = BoardTool.filter_cards(board, has_due_date=True) cards = sorted(cards, key=lambda c: c.due) else: cards = BoardTool.filter_cards( board, tags=tags, no_tags=no_tags, title_regex=match, list_regex=listname, has_attachments=attachments, has_due_date=has_due, ) list_lookup = BoardTool.list_lookup(board) label_lookup = BoardTool.label_lookup(board) display = Display(config.color) if config.banner: display.banner() for card in cards: CardTool.smart_menu(card, display.show_card, list_lookup, label_lookup, color=config.color) click.echo('All done, have a great day!')
def grep(config, pattern, insensitive, count, regexp): '''egrep through titles of cards on this board. This command attemps to replicate a couple of grep flags faithfully, so if you're a power-user of grep this command will feel familiar. ''' if not (pattern or regexp): click.secho('No pattern provided to grep: use either the argument or -e', fg='red') raise GTDException(1) # Merge together the different regex arguments final_pattern = '|'.join(regexp) if regexp else '' if pattern and final_pattern: final_pattern = final_pattern + '|' + pattern elif pattern: final_pattern = pattern flags = re.I if insensitive else 0 connection, board = BoardTool.start(config) cards = BoardTool.filter_cards( board, title_regex=final_pattern, regex_flags=flags ) if count: print(sum(1 for _ in cards)) raise GTDException(0) display = Display(config.color) if config.banner: display.banner() display.show_cards(cards)
def show_tags(config, json): '''Display all tags on this board''' _, board = BoardTool.start(config) display = Display(config.color) if config.banner and not json: display.banner() tag_names = [t.name for t in board.get_labels()] display.show_raw(tag_names, use_json=json)
def show_lists(config, json, show_all): '''Display all lists on this board''' _, board = BoardTool.start(config) display = Display(config.color) if config.banner and not json: display.banner() list_filter = 'all' if show_all else 'open' list_names = [l.name for l in board.get_lists(list_filter)] display.show_raw(list_names, use_json=json)
def __init__(self, config: Configuration): self.config = config self.connection = TrelloConnection(config) self.display = Display(config, self.connection) self.board = self.connection.main_board() # Cached state for card_repl self._list_choices = build_name_lookup( self.connection.main_board().get_lists('open')) self._label_choices = build_name_lookup( self.connection.main_board().get_labels(limit=200))
def batch_attach(config): '''Extract HTTP links from card titles''' connection, board = BoardTool.start(config) cards = BoardTool.filter_cards(board, title_regex=VALID_URL_REGEX) display = Display(config.color) if config.banner: display.banner() for card in cards: display.show_card(card) if prompt_for_confirmation('Attach title?', True): CardTool.title_to_link(card)
def add_card(config, title, message, edit, listname): '''Add a new card. If no title is provided, $EDITOR will be opened so you can write one.''' connection, board = BoardTool.start(config) if listname is None: inbox = BoardTool.get_inbox_list(connection, config) else: pattern = re.compile(listname, flags=re.I) target_lists = filter(lambda x: pattern.search(x.name), board.get_lists('open')) try: inbox = next(target_lists) except StopIteration: click.secho('No list names matched by {}'.format(listname), fg='red') raise GTDException(1) if not title: title = click.edit(require_save=True, text='<Title here>') if title is None: # No changes were made in $EDITOR click.secho('No title entered for the new card!', fg='red') raise GTDException(1) else: title = title.strip() returned = inbox.add_card(name=title, desc=message) if edit: display = Display(config.color) list_lookup = BoardTool.list_lookup(board) label_lookup = BoardTool.label_lookup(board) CardTool.smart_menu(returned, display.show_card, list_lookup, label_lookup, color=config.color) else: click.secho('Successfully added card {0}!'.format(returned), fg='green')
def validate_fields(command_context, param, value): valid = Display.valid_fields() possible = value.split(',') if value else [] for field in possible: if field not in valid: raise click.BadParameter( f'Field {field} is not a valid field! Use {",".join(valid)}') return possible
def batch_tag(config, tags, no_tags, match, listname, attachments, has_due): '''Change tags on each card selected''' connection, board = BoardTool.start(config) cards = BoardTool.filter_cards( board, tags=tags, no_tags=no_tags, title_regex=match, list_regex=listname, has_attachments=attachments, has_due_date=has_due ) display = Display(config.color) if config.banner: display.banner() for card in cards: display.show_card(card) CardTool.add_labels(card)
def validate_fields(ctx, param, value): valid = Display.build_fields().keys() possible = value.split(',') if value else [] for field in possible: if field not in valid: raise click.BadParameter( 'Field {} is not a valid field! Use {}'.format( field, ','.join(valid))) return possible
def batch_move(config, tags, no_tags, match, listname, attachments, has_due): '''Change the list of each card selected''' connection, board = BoardTool.start(config) cards = BoardTool.filter_cards( board, tags=tags, no_tags=no_tags, title_regex=match, list_regex=listname, has_attachments=attachments, has_due_date=has_due ) display = Display(config.color) if config.banner: display.banner() for card in cards: display.show_card(card) if prompt_for_confirmation('Want to move this one?', True): CardTool.move_to_list(card)
def batch_due(config, tags, no_tags, match, listname, attachments, has_due): '''Set due date for all cards selected''' connection, board = BoardTool.start(config) cards = BoardTool.filter_cards( board, tags=tags, no_tags=no_tags, title_regex=match, list_regex=listname, has_attachments=attachments, has_due_date=has_due ) display = Display(config.color) if config.banner: display.banner() for card in cards: display.show_card(card) if prompt_for_confirmation('Set due date?'): CardTool.set_due_date(card)
def show_cards(config, json, tsv, tags, no_tags, match, listname, attachments, has_due): '''Display cards The show command prints a table of all the cards with fields that will fit on the terminal you're using. You can change this formatting by passing one of --tsv or --json, which will output as a tab-separated value sheet or JSON. This command along with the batch & review commands share a flexible argument scheme for getting card information. Mutually exclusive arguments include -t/--tags & --no-tags along with -j/--json & --tsv ''' _, board = BoardTool.start(config) display = Display(config.color) if config.banner and not json: display.banner() cards = BoardTool.filter_cards( board, tags=tags, no_tags=no_tags, title_regex=match, list_regex=listname, has_attachments=attachments, has_due_date=has_due ) display.show_cards(cards, use_json=json, tsv=tsv)
def show_soon(config, json, tsv): _, board = BoardTool.start(config) display = Display(config.color) if config.banner and not json: display.banner() cards = BoardTool.filter_cards(board, has_due_date=True) display.show_cards(cards, use_json=json, tsv=tsv, sort='due')
def show_cards(config, json, tsv, tags, no_tags, match, listname, attachments, has_due, assigned, completed, include_closed): '''Display cards The show command prints a table of all the cards with fields that will fit on the terminal you're using. You can change this formatting by passing one of --tsv or --json, which will output as a tab-separated value sheet or JSON. This command along with the batch & review commands share a flexible argument scheme for getting card information. Mutually exclusive arguments include -t/--tags & --no-tags along with -j/--json & --tsv ''' _, board, current_user = BoardTool.start(config) display = Display(config.color) if config.banner and not json: display.banner() cards = BoardTool.filter_cards( board, tags=tags, no_tags=no_tags, current_user=current_user, title_regex=match, list_regex=listname, has_attachments=attachments, has_due_date=has_due, assigned=assigned, completed=completed, include_closed=include_closed ) display.show_cards(cards, use_json=json, tsv=tsv)
def grep(config, pattern, insensitive, count, regexp, by, fields, json): '''egrep through titles of cards on this board. This command attemps to replicate a couple of grep flags faithfully, so if you're a power-user of grep this command will feel familiar. One deviation from grep is the --json flag, which outputs all matching cards in full JSON format. ''' if not (pattern or regexp): click.secho( 'No pattern provided to grep: use either the argument or -e', fg='red') raise GTDException(1) # Merge together the different regex arguments final_pattern = '|'.join(regexp) if regexp else '' if pattern and final_pattern: final_pattern = final_pattern + '|' + pattern elif pattern: final_pattern = pattern flags = re.I if insensitive else 0 connection, board = BoardTool.start(config) cards = BoardTool.filter_cards(board, title_regex=final_pattern, regex_flags=flags) if count: print(sum(1 for _ in cards)) return display = Display(config.color) if config.banner and not json: display.banner() display.show_cards(cards, use_json=json, sort=by, table_fields=fields)
def delete_cards(config, force, noninteractive, tags, no_tags, match, listname, attachments, has_due): '''Delete a set of cards specified ''' _, board, _ = BoardTool.start(config) display = Display(config.color) if config.banner and not json: display.banner() cards = BoardTool.filter_cards( board, tags=tags, no_tags=no_tags, title_regex=match, list_regex=listname, has_attachments=attachments, has_due_date=has_due ) method = 'delete' if force else 'archive' if noninteractive: if force: [c.delete() for c in cards] else: [c.set_closed(True) for c in cards] else: for card in cards: display.show_card(card) if prompt_for_confirmation('Delete this card?'): if force: card.delete() else: card.set_closed(True) click.secho('Card {}d!'.format(method), fg='red')
def show_unresponded(config, json): '''Display all unresponded comments for current account''' connection, _, current_user = BoardTool.start(config) display = Display(config.color) if config.banner and not json: display.banner() comments = get_unresponded_comments(connection, current_user) display.show_comments(comments, use_json=json)
def show_tags(config, json): '''Display all tags on this board''' _, board, _ = BoardTool.start(config) display = Display(config.color) if config.banner and not json: display.banner() tag_names = [t.name for t in board.get_labels()] display.show_raw(tag_names, use_json=json)
def review(config, tags, no_tags, match, listname, attachments, has_due): '''show a smart, command-line based menu for each card selected. This menu will prompt you to add tags to untagged cards, to attach the title of cards which have a link in the title, and gives you all the other functionality combined. ''' connection, board = BoardTool.start(config) cards = BoardTool.filter_cards( board, tags=tags, no_tags=no_tags, title_regex=match, list_regex=listname, has_attachments=attachments, has_due_date=has_due ) list_lookup = BoardTool.list_lookup(board) label_lookup = BoardTool.label_lookup(board) display = Display(config.color) if config.banner: display.banner() for card in cards: CardTool.smart_menu(card, display.show_card, list_lookup, label_lookup, Colors.yellow) click.echo('All done, have a great day!')
def show_lists(config, json, show_all): '''Display all lists on this board''' _, board, _ = BoardTool.start(config) display = Display(config.color) if config.banner and not json: display.banner() list_filter = 'all' if show_all else 'open' list_names = [l.name for l in board.get_lists(list_filter)] display.show_raw(list_names, use_json=json)
def add_card(config, title, message, edit): '''Add a new card. If no title is provided, $EDITOR will be opened so you can write one.''' connection, board = BoardTool.start(config) inbox = BoardTool.get_inbox_list(connection, config) if not title: title = click.edit(require_save=True, text='<Title here>') if title is None: # No changes were made in $EDITOR click.secho('No title entered for the new card!', fg='red') raise GTDException(1) else: title = title.strip() returned = inbox.add_card(name=title, desc=message) if edit: display = Display(config.color) list_lookup = BoardTool.list_lookup(board) label_lookup = BoardTool.label_lookup(board) CardTool.smart_menu(returned, display.show_card, list_lookup, label_lookup, Colors.yellow) else: click.secho('Successfully added card {0}!'.format(returned), fg='green')
def batch_tag(config, tags, no_tags, match, listname, attachments, has_due): '''Change tags on each card selected''' connection, board = BoardTool.start(config) cards = BoardTool.filter_cards(board, tags=tags, no_tags=no_tags, title_regex=match, list_regex=listname, has_attachments=attachments, has_due_date=has_due) display = Display(config.color) if config.banner: display.banner() for card in cards: display.show_card(card) CardTool.add_labels(card)
def batch_due(config, tags, no_tags, match, listname, attachments, has_due): '''Set due date for all cards selected''' connection, board = BoardTool.start(config) cards = BoardTool.filter_cards(board, tags=tags, no_tags=no_tags, title_regex=match, list_regex=listname, has_attachments=attachments, has_due_date=has_due) display = Display(config.color) if config.banner: display.banner() for card in cards: display.show_card(card) if prompt_for_confirmation('Set due date?'): CardTool.set_due_date(card)
def batch_move(config, tags, no_tags, match, listname, attachments, has_due): '''Change the list of each card selected''' connection, board = BoardTool.start(config) cards = BoardTool.filter_cards(board, tags=tags, no_tags=no_tags, title_regex=match, list_regex=listname, has_attachments=attachments, has_due_date=has_due) display = Display(config.color) if config.banner: display.banner() for card in cards: display.show_card(card) if prompt_for_confirmation('Want to move this one?', True): CardTool.move_to_list(card)
def show_boards(config, json, tsv, by, show_all): '''Show all boards your account can access''' connection, board = BoardTool.start(config) display = Display(config.color) if config.banner and not json: display.banner() if show_all: boards = connection.trello.fetch_json('/members/me/boards/?filter=all') else: boards = connection.boards if json: display.show_raw(boards, use_json=json) return # Set up a table to hold our boards board_columns = ['name', 'activity', 'members', 'permission', 'url'] if by not in board_columns: click.secho('Field {} is not a valid field: {}'.format( by, ','.join(board_columns)), fg='red') raise GTDException(1) table = prettytable.PrettyTable() table.field_names = board_columns table.align = 'l' if tsv: table.set_style(prettytable.PLAIN_COLUMNS) else: table.hrules = prettytable.FRAME for b in boards: table.add_row([ b['name'], b['dateLastActivity'] or '', len(b['memberships']), b['prefs']['permissionLevel'], b['shortUrl'], ]) try: table[0] except IndexError: click.secho('You have no boards!', fg='red') print(table.get_string(sortby=by))
class CLIContext: '''CLIContext is a container for the commonly used objects in each gtd command. It is passed around as an argument injected by click after the cli() function runs. Any reference to "ctx" as a parameter in this file is an instance of CLIContext. config: todo.configuration.Configuration connection: todo.connection.TrelloConnection display: todo.display.Display ''' def __init__(self, config: Configuration): self.config = config self.connection = TrelloConnection(config) self.display = Display(config, self.connection) self.board = self.connection.main_board() # Cached state for card_repl self._list_choices = build_name_lookup( self.connection.main_board().get_lists('open')) self._label_choices = build_name_lookup( self.connection.main_board().get_labels(limit=200)) def card_repl(self, card: dict) -> bool: '''card_repl displays a command-prompt based UI for modifying a card, with tab-completion and suggestions. It is the logic behind "gtd review" and the "-e" flag in "gtd add" It makes assumptions about what a user might want to do with a card: - Are there attachments? Maybe you want to open them. - Does there appear to be a URL in the title? You might want to attach it. - Are there no tags? Maybe you want to add some. Returns: boolean: move forwards or backwards in the deck of cards ''' on = Colors.yellow if self.config.color else '' off = Colors.reset if self.config.color else '' self.display.show_card(card) if self.config.prompt_for_open_attachments and card['badges'][ 'attachments']: if prompt_for_confirmation(f'{on}Open attachments?{off}', False): with DevNullRedirect(): for url in [ a['url'] for a in card.fetch_attachments() if a['url'] ]: webbrowser.open(url) if re.search(VALID_URL_REGEX, card['name']): if prompt_for_confirmation( f'{on}Link in title detected, want to attach it & rename?{off}', True): card.title_to_link() if self.config.prompt_for_untagged_cards and not card['labels']: print(f'{on}No tags on this card yet, want to add some?{off}') card.add_labels(self._label_choices) commands = { 'archive': 'mark this card as closed', 'attach': 'add, delete, or open attachments', 'change-list': 'move this to a different list on the same board', 'comment': 'add a comment to this card', 'delete': 'permanently delete this card', 'duedate': 'add a due date or change the due date', 'description': 'change the description of this card (desc)', 'help': 'display this help output (h)', 'move': 'move to a different board and list (m)', 'next': 'move to the next card (n)', 'open': 'open all links on this card (o)', 'prev': 'go back to the previous card (p)', 'print': 're-display this card', 'rename': 'change title of this card', 'tag': 'add or remove tags on this card (t)', 'unarchive': 'mark this card as open', 'quit': 'exit program', } command_completer = FuzzyWordCompleter(commands.keys()) while True: user_input = prompt('gtd.py > ', completer=command_completer) if user_input in ['q', 'quit']: raise GTDException(0) elif user_input in ['n', 'next']: return True elif user_input in ['p', 'prev']: return False elif user_input == 'print': card.fetch() self.display.show_card(card) elif user_input in ['o', 'open']: with DevNullRedirect(): for url in [ a['url'] for a in card.fetch_attachments() if a['url'] is not None ]: webbrowser.open(url) elif user_input in ['desc', 'description']: card.change_description() elif user_input == 'delete': card.delete() print('Card deleted') return True elif user_input == 'attach': card.manipulate_attachments() elif user_input == 'archive': card.set_closed(True) print('Card archived') return True elif user_input == 'unarchive': card.set_closed(False) print('Card returned to board') elif user_input in ['t', 'tag']: card.add_labels(self._label_choices) elif user_input == 'rename': # TODO optional form 'rename New name of card' card.rename() elif user_input == 'duedate': card.set_due_date() elif user_input in ['h', 'help']: for cname, cdesc in commands.items(): print('{0:<16}| {1}{2}{3}'.format(cname, on, cdesc, off)) elif user_input == 'change-list': if card.move_to_list(self._list_choices): return True elif user_input in ['m', 'move']: self.move_between_boards(card) elif user_input == 'comment': # TODO Optional form 'comment Contents of a comment' new_comment = click.edit(text='<Comment here>', require_save=True) if new_comment: card.comment(new_comment) else: click.secho('Change the text & save to post the comment', fg='red') else: print( f'{on}{user_input}{off} is not a command, type "{on}help{off}" to view available commands' ) @return_on_eof def move_between_boards(self, card: Card) -> None: boards_by_name = self.connection.boards_by_name() board_name = prompt('gtd.py > move > board name? ', completer=FuzzyWordCompleter( boards_by_name.keys())) board_id = boards_by_name[board_name]['id'] lists_json = self.connection.trello.fetch_json( f'/boards/{board_id}/lists?cards=none&filter=open&fields=name') name_to_listid = {l['name']: l['id'] for l in lists_json} list_name = prompt( f'gtd.py > move > {board_name} > list name? ', completer=FuzzyWordCompleter(name_to_listid.keys()), ) card.change_board(board_id, list_id=name_to_listid[list_name]) click.secho(f'Changed list to {list_name} on {board_name}', fg='green')
def validate_sort(ctx, param, value): if value and value not in Display.build_fields().keys(): raise click.BadParameter( 'Sort parameter {} is not a valid field!'.format(value)) return value
def validate_sort(command_context, param, value): if value and value not in Display.valid_fields(): raise click.BadParameter( f'Sort parameter {value} is not a valid field!') return value