From fb1dccc0e64d79eb8084c2c8fd8fc94d6453a2c3 Mon Sep 17 00:00:00 2001 From: Kevin Stadler Date: Sun, 28 Mar 2021 12:43:32 +0800 Subject: [PATCH 01/10] Add TaskListModel.active_report_filter(). --- vit/task.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/vit/task.py b/vit/task.py index c3e8206..050454f 100644 --- a/vit/task.py +++ b/vit/task.py @@ -33,8 +33,7 @@ def parse_error(self, err): def update_report(self, report, context_filters=[], extra_filters=[]): self.report = report - active_report = self.active_report() - report_filters = active_report['filter'] if 'filter' in active_report else [] + report_filters = self.active_report_filter() filters = self.build_task_filters(context_filters, report_filters, extra_filters) try: self.tasks = self.tw.tasks.filter(filters) if filters else self.tw.tasks.all() @@ -45,6 +44,10 @@ def update_report(self, report, context_filters=[], extra_filters=[]): except TaskWarriorException as err: raise VitException(self.parse_error(err)) + def active_report_filter(self): + active_report = self.active_report() + return active_report['filter'] if 'filter' in active_report else [] + def build_task_filters(self, *all_filters): def reducer(accum, filters): if filters: From be2ddbbd5f5ab3e1233000acf51459459934a0f5 Mon Sep 17 00:00:00 2001 From: Kevin Stadler Date: Sun, 28 Mar 2021 12:45:02 +0800 Subject: [PATCH 02/10] Add Application.active_context_filter(). --- vit/application.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/vit/application.py b/vit/application.py index 2521661..f2de922 100644 --- a/vit/application.py +++ b/vit/application.py @@ -97,6 +97,9 @@ def setup_main_loop(self): def set_active_context(self): self.context = self.task_config.get_active_context() + def active_context_filter(self): + return self.contexts[self.context]['filter'] if self.context else [] + def load_contexts(self): self.contexts = self.task_config.get_contexts() @@ -195,6 +198,7 @@ def _task_attribute_replace(task, attribute): else: return str(task[attribute]) return '' + replacements = [ { 'match_callback': _task_attribute_match, @@ -909,7 +913,7 @@ def update_report(self, report=None): self.task_config.get_projects() self.refresh_blocking_task_uuids() self.formatter.recalculate_due_datetimes() - context_filters = self.contexts[self.context]['filter'] if self.context else [] + context_filters = self.active_context_filter() try: self.model.update_report(self.report, context_filters=context_filters, extra_filters=self.extra_filters) except VitException as err: From 2bfa0233d073c43995ac5aeb7862619cc5a77c4b Mon Sep 17 00:00:00 2001 From: Kevin Stadler Date: Wed, 7 Apr 2021 13:29:22 +0800 Subject: [PATCH 03/10] Add get_n_tasks(filterexpression) method to TaskListModel --- vit/task.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/vit/task.py b/vit/task.py index 050454f..b3188f2 100644 --- a/vit/task.py +++ b/vit/task.py @@ -56,6 +56,9 @@ def reducer(accum, filters): filter_parts = reduce(reducer, all_filters, []) return ' '.join(filter_parts) if filter_parts else '' + def get_n_tasks(self, filter): + return len(self.tw.tasks.filter(filter)) + def get_task(self, uuid): try: return self.tw.tasks.get(uuid=uuid) From 3fed3146a853c801b2f2c61f16b33f66e47c1f66 Mon Sep 17 00:00:00 2001 From: Kevin Stadler Date: Wed, 7 Apr 2021 13:46:03 +0800 Subject: [PATCH 04/10] Add support for modify_multiple operation to Application --- vit/application.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/vit/application.py b/vit/application.py index f2de922..56e1a52 100644 --- a/vit/application.py +++ b/vit/application.py @@ -364,6 +364,21 @@ def command_bar_keypress(self, data): # before hitting enter? if self.execute_command(['task', metadata['uuid'], 'modify'] + args, wait=self.wait): self.activate_message_bar('Task %s modified' % self.model.task_id(metadata['uuid'])) + elif op == 'modify_multiple': + # same underlying command is the modify command above, only the results is parsed + # differently and the message bar set accordingly + + # to be absolutely safe, double-check whether the number of tasks matched by + # the filter is still the same (or has changed because of some other operations, + # due/schedule/wait timeouts etc) + ntasks = self.model.get_n_tasks(metadata['target']) + if ntasks != metadata['ntasks']: + self.activate_message_bar('Not applying the modification because the number of tasks has changed (was %s now %s)' % (metadata['ntasks'], ntasks)) + elif self.execute_command(['task', metadata['target'], 'modify'] + args, wait=self.wait): + # TODO depending on interactive confirmation prompts, not all tasks + # might actually have been modified. for completeness one would need to extract + # the number of modified tasks from the stdout of the task command above! + self.activate_message_bar('Modified %s tasks' % ntasks) elif op == 'annotate': task = self.model.task_annotate(metadata['uuid'], data['text']) if task: From b72569a48d5dbd873b5799a16aa16ea3a250e906 Mon Sep 17 00:00:00 2001 From: Kevin Stadler Date: Wed, 7 Apr 2021 13:48:57 +0800 Subject: [PATCH 05/10] Add Application.active_view_filters(). --- vit/application.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/vit/application.py b/vit/application.py index 56e1a52..137b799 100644 --- a/vit/application.py +++ b/vit/application.py @@ -100,6 +100,10 @@ def set_active_context(self): def active_context_filter(self): return self.contexts[self.context]['filter'] if self.context else [] + def active_view_filters(self): + # precedence-preserving concatenation of context, report and extra filters + return self.model.build_task_filters(self.active_context_filter(), self.model.active_report_filter(), self.extra_filters) + def load_contexts(self): self.contexts = self.task_config.get_contexts() From 6c4d35f8980f44f8373366c7658423b33a136fb8 Mon Sep 17 00:00:00 2001 From: Kevin Stadler Date: Wed, 7 Apr 2021 13:50:47 +0800 Subject: [PATCH 06/10] Add TASK_MODIFY_ALL action (addresses vit-project/vit#240) This action is a first use case (but really just one special case) of bulk editing, which makes use of the new 'modify_multiple' operation by passing the filter expression that matches all tasks visible in the current view. --- vit/actions.py | 1 + vit/application.py | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/vit/actions.py b/vit/actions.py index 6a30453..67dfbfb 100644 --- a/vit/actions.py +++ b/vit/actions.py @@ -38,6 +38,7 @@ def register(self): self.action_registrar.register('TASK_DELETE', 'Delete task') self.action_registrar.register('TASK_DENOTATE', 'Denotate a task') self.action_registrar.register('TASK_MODIFY', 'Modify task (supports tab completion)') + self.action_registrar.register('TASK_MODIFY_ALL', 'Modify all tasks currently in view') self.action_registrar.register('TASK_START_STOP', 'Start/stop task') self.action_registrar.register('TASK_DONE', 'Mark task done') self.action_registrar.register('TASK_PRIORITY', 'Modify task priority') diff --git a/vit/application.py b/vit/application.py index 137b799..35d0a90 100644 --- a/vit/application.py +++ b/vit/application.py @@ -171,6 +171,7 @@ def register_managed_actions(self): self.action_manager_registrar.register('TASK_DELETE', self.task_action_delete) self.action_manager_registrar.register('TASK_DENOTATE', self.task_action_denotate) self.action_manager_registrar.register('TASK_MODIFY', self.task_action_modify) + self.action_manager_registrar.register('TASK_MODIFY_ALL', self.task_action_modify_all) self.action_manager_registrar.register('TASK_START_STOP', self.task_action_start_stop) self.action_manager_registrar.register('TASK_DONE', self.task_action_done) self.action_manager_registrar.register('TASK_PRIORITY', self.task_action_priority) @@ -763,6 +764,11 @@ def task_action_modify(self): self.activate_command_bar('modify', 'Modify: ', {'uuid': uuid}) self.task_list.focus_by_task_uuid(uuid, self.previous_focus_position) + def task_action_modify_all(self): + currentviewfilter = self.active_view_filters() + ntasks = self.model.get_n_tasks(currentviewfilter) + self.activate_command_bar('modify_multiple', 'Modify all (%s tasks): ' % ntasks, {'target': currentviewfilter, 'ntasks': ntasks}) + def task_action_start_stop(self): uuid, task = self.get_focused_task() if task: From 73d509f33d234962fffdb76df5361bc288115ad6 Mon Sep 17 00:00:00 2001 From: Kevin Stadler Date: Fri, 9 Apr 2021 13:40:09 +0800 Subject: [PATCH 07/10] Address PR vit-project/vit#290 code review comments --- vit/application.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/vit/application.py b/vit/application.py index 35d0a90..f27af36 100644 --- a/vit/application.py +++ b/vit/application.py @@ -370,8 +370,9 @@ def command_bar_keypress(self, data): if self.execute_command(['task', metadata['uuid'], 'modify'] + args, wait=self.wait): self.activate_message_bar('Task %s modified' % self.model.task_id(metadata['uuid'])) elif op == 'modify_multiple': - # same underlying command is the modify command above, only the results is parsed - # differently and the message bar set accordingly + # same underlying command as the modify command above, only + # the message bar is set to information about the number of + # tasks modified, instead of a single task info # to be absolutely safe, double-check whether the number of tasks matched by # the filter is still the same (or has changed because of some other operations, @@ -380,10 +381,16 @@ def command_bar_keypress(self, data): if ntasks != metadata['ntasks']: self.activate_message_bar('Not applying the modification because the number of tasks has changed (was %s now %s)' % (metadata['ntasks'], ntasks)) elif self.execute_command(['task', metadata['target'], 'modify'] + args, wait=self.wait): - # TODO depending on interactive confirmation prompts, not all tasks - # might actually have been modified. for completeness one would need to extract - # the number of modified tasks from the stdout of the task command above! - self.activate_message_bar('Modified %s tasks' % ntasks) + # TODO depending on interactive confirmation prompts, + # not all tasks might actually have been modified. for + # completeness one would need to extract the number of + # modified tasks from the stdout of the task command above. + # this is currently not possible because execute_command(), + # even when called with capture_output=True, doesn't return + # the captured output back to here. An easier method might + # be to go through tasklib and simply check the number of + # modified tasks based on their modified time + self.activate_message_bar('Attempted to modify %s tasks, mileage may vary' % ntasks) elif op == 'annotate': task = self.model.task_annotate(metadata['uuid'], data['text']) if task: @@ -765,9 +772,9 @@ def task_action_modify(self): self.task_list.focus_by_task_uuid(uuid, self.previous_focus_position) def task_action_modify_all(self): - currentviewfilter = self.active_view_filters() - ntasks = self.model.get_n_tasks(currentviewfilter) - self.activate_command_bar('modify_multiple', 'Modify all (%s tasks): ' % ntasks, {'target': currentviewfilter, 'ntasks': ntasks}) + current_view_filter = self.active_view_filters() + ntasks = self.model.get_n_tasks(current_view_filter) + self.activate_command_bar('modify_multiple', 'Modify all (%s tasks): ' % ntasks, {'target': current_view_filter, 'ntasks': ntasks}) def task_action_start_stop(self): uuid, task = self.get_focused_task() From a1d51c3287359530aae64a87963d9d2a1b7d8091 Mon Sep 17 00:00:00 2001 From: Kevin Stadler Date: Sun, 11 Apr 2021 18:51:35 +0800 Subject: [PATCH 08/10] Add ACTION_TASK_MODIFY_ALL binding to config.sample.ini. --- vit/config/config.sample.ini | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/vit/config/config.sample.ini b/vit/config/config.sample.ini index fdd23f2..aa0a405 100644 --- a/vit/config/config.sample.ini +++ b/vit/config/config.sample.ini @@ -194,6 +194,11 @@ # For capital letter keybindings, use the letter directly: # D = {ACTION_TASK_DONE} +# Modify all tasks currently listed in vit. If the number of affected tasks +# exceeds your .taskrc's 'rc.bulk' setting (default is 3), taskwarrior's +# interactive prompt will ask you to confirm the modifications. +# M = {ACTION_TASK_MODIFY_ALL} + # For a list of available actions, run 'vit --list-actions'. # A great reference for many of the available meta keys, and understanding the # default keybindings is the 'keybinding/vi.ini' file. From 9fdb6adee03999d29d86ea221449a41137c09f7f Mon Sep 17 00:00:00 2001 From: Kevin Stadler Date: Thu, 15 Apr 2021 22:27:10 +0800 Subject: [PATCH 09/10] Rename 'modify_multiple' operation to 'modify_bulk' and enable autocompletion --- vit/application.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vit/application.py b/vit/application.py index f27af36..96fa21e 100644 --- a/vit/application.py +++ b/vit/application.py @@ -369,7 +369,7 @@ def command_bar_keypress(self, data): # before hitting enter? if self.execute_command(['task', metadata['uuid'], 'modify'] + args, wait=self.wait): self.activate_message_bar('Task %s modified' % self.model.task_id(metadata['uuid'])) - elif op == 'modify_multiple': + elif op == 'modify_bulk': # same underlying command as the modify command above, only # the message bar is set to information about the number of # tasks modified, instead of a single task info @@ -774,7 +774,7 @@ def task_action_modify(self): def task_action_modify_all(self): current_view_filter = self.active_view_filters() ntasks = self.model.get_n_tasks(current_view_filter) - self.activate_command_bar('modify_multiple', 'Modify all (%s tasks): ' % ntasks, {'target': current_view_filter, 'ntasks': ntasks}) + self.activate_command_bar('modify_bulk', 'Modify all (%s tasks): ' % ntasks, {'target': current_view_filter, 'ntasks': ntasks}) def task_action_start_stop(self): uuid, task = self.get_focused_task() @@ -846,7 +846,7 @@ def refresh_blocking_task_uuids(self): def setup_autocomplete(self, op): callback = self.command_bar.set_edit_text_callback() - if op in ('filter', 'add', 'modify'): + if op in ('filter', 'add', 'modify', 'modify_bulk'): self.autocomplete.setup(callback) elif op in ('ex',): filters = ('report', 'column', 'project', 'tag', 'help') From 9d337580784f4bccd5f6d01d930c1ea87b1b449f Mon Sep 17 00:00:00 2001 From: Kevin Stadler Date: Thu, 15 Apr 2021 22:29:22 +0800 Subject: [PATCH 10/10] Add 'bulk modification' to README features --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 5c996f7..a215e2f 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Visual Interactive Taskwarrior full-screen terminal interface. * Speed * Per-column colorization * Advanced tab completion + * Bulk modification of tasks * Multiple/customizable themes * Override/customize column formatters * Intelligent sub-project indenting