Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new action for bulk-modifying all tasks currently in view (addresses #240) #290

Open
wants to merge 11 commits into
base: 2.x
Choose a base branch
from
Open
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions vit/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
40 changes: 38 additions & 2 deletions vit/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,13 @@ 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 and self.reports[self.report].get('context', 1) 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()

Expand Down Expand Up @@ -164,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)
Expand Down Expand Up @@ -195,6 +203,7 @@ def _task_attribute_replace(task, attribute):
else:
return str(task[attribute])
return ''

replacements = [
{
'match_callback': _task_attribute_match,
Expand Down Expand Up @@ -361,6 +370,28 @@ 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_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

# 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.
# 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:
Expand Down Expand Up @@ -754,6 +785,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):
current_view_filter = self.active_view_filters()
ntasks = self.model.get_n_tasks(current_view_filter)
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()
if task:
Expand Down Expand Up @@ -824,7 +860,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')
Expand Down Expand Up @@ -923,7 +959,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 and self.reports[self.report].get('context', 1) 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:
Expand Down
5 changes: 5 additions & 0 deletions vit/config/config.sample.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
10 changes: 8 additions & 2 deletions vit/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand 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:
Expand All @@ -53,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)
Expand Down