Codebase list vit / e7dcc32
New upstream version 2.2.0 Jochen Sprickerhof 2 years ago
20 changed file(s) with 354 addition(s) and 167 deletion(s). Raw diff Collapse all Expand all
0 ##### Sun Apr 17 2022 - released v2.2.0
1
2 * **Sun Apr 17 2022:** bump dependency versions
3 * **Sun Apr 17 2022:** bump minimum Python version to 3.7
4 * **Tue Mar 22 2022:** Simplify timezone handling
5 * **Sat Jul 24 2021:** Replace pytz and tzlocal by zoneinfo
6 * **Sat Mar 05 2022:** Make vit respect taskrc in config.ini
7
8 ##### Fri Nov 26 2021 - released v2.2.0b1
9
10 * **Fri Nov 26 2021:** fix #317: Broken links on PyPi
11 * **Mon Nov 22 2021:** fix #313: ACTION_REFRESH keybind triggers while entering text
12 * **Wed Oct 27 2021:** add `focus_on_add` configuration parameter, allows focusing on newly added task
13 * **Wed Oct 27 2021:** properly escape search terms
14 * **Tue Oct 12 2021:** Include 'report.X.context=0' option of tw 2.6.0
15 * **Sat Oct 09 2021:** fix #305: vit fails when using new context definition
16 * **Wed Oct 06 2021:** bump tasklib min version
17 * **Wed Oct 06 2021:** Support XDG_CONFIG_DIR taskrc location
18 * **Wed Sep 29 2021:** fix #302: display task id of created task in command bar
19 * **Sun Aug 29 2021:** fix #140, fix #230. smarter handling of spaces/quotes in autocomplete
20 * **Sun Aug 29 2021:** clarify doc for finding user config directory
21 * **Sun Aug 11 2019:** Add support for XDG Base Directory
22 * **Thu Jul 15 2021:** set VIT_TASK_UUID environment variable when executing external scripts
23 * **Thu Jul 15 2021:** allow passing custom environment variables to external commands
24 * **Mon Jun 07 2021:** fix #296: AutoComplete space_escape_regex not initialized for 'wait' command
25 * **Tue Mar 16 2021:** fix #287: Incorrect marker width calculation of Unicode symbols can cause markers to not be displayed
26 * **Sun Feb 28 2021:** add support for TaskWarrior >= 2.5.2 to changelog
27
028 ##### Sun Feb 28 2021 - released v2.1.0
29
30 Support for TaskWarrior >= 2.5.2
131
232 This release includes a breaking change to the keybinding parser, and may affect
333 users who have implemented custom keybindings in their configuration.
11
22 ### Configuration
33
4 #### VIT's configuration
5
46 VIT provides a user directory that allows for configuring basic settings *(via ```config.ini```)*, as well as custom themes, formatters, and keybindings.
57
6 By default, the directory is located at ```~/.vit```
8 VIT searches for the user directory in this order of priority:
79
8 To customize the location of the user directory, you can set the ```VIT_DIR``` environment variable.
10 1. The ```VIT_DIR``` environment variable
11 2. ```~/.vit``` (the default location)
12 3. A ```vit``` directory in any valid [XDG base directory](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html)
13
14 #### Taskwarrior configuration
915
1016 By default, VIT uses the default location of the Taskwarrior configuration file to read configuration from Taskwarrior.
1117
0 [build-system]
1 requires = [
2 "setuptools>=42",
3 "wheel"
4 ]
5 build-backend = "setuptools.build_meta"
0 pytz>=2019.3
1 tasklib>=1.3.0
2 tzlocal>=2.0.0
3 urwid>=2.1.0
0 tasklib>=2.4.3
1 urwid>=2.1.2
2 backports.zoneinfo;python_version<"3.9"
0 #!/usr/bin/env bash
1
2 # Convenience script to handle preparing for PyPi release, and printing out the
3 # commands to execute it.
4
5 execute() {
6 local update_build_release_packages="pip install --upgrade wheel build twine"
7 local clean="rm -rfv dist/ build/"
8 local build="python -m build"
9 local test_pypi_upload="python -m twine upload --repository testpypi dist/*"
10 local pypi_upload="python -m twine upload --skip-existing dist/*"
11
12 echo "Updating build and release packages with command:"
13 echo " ${update_build_release_packages}"
14 ${update_build_release_packages}
15
16 if [ $? -eq 0 ]; then
17 echo "Cleaning build environment with command:"
18 echo " ${clean}"
19 ${clean}
20 if [ $? -eq 0 ]; then
21 echo "Building release with command:"
22 echo " ${build}"
23 ${build}
24 if [ $? -eq 0 ]; then
25 echo "Build successful"
26 echo
27 echo "Test release with command:"
28 echo " ${test_pypi_upload}"
29 echo
30 echo "Release with command:"
31 echo " ${pypi_upload}"
32 fi
33 fi
34 fi
35 }
36
37 if [ -d vit ] && [ -r setup.py ]; then
38 execute
39 else
40 echo "ERROR: must run script from VIT repository root"
41 fi
11 from setuptools import setup
22 from os import path
33
4 DEFAULT_BRANCH = "2.x"
5 BASE_GITHUB_URL = "https://github.com/vit-project/vit/blob"
6
7 MARKUP_LINK_REGEX = "\[([^]]+)\]\(([\w]+\.md)\)"
48 FILE_DIR = path.dirname(path.abspath(path.realpath(__file__)))
59
610 with open(path.join(FILE_DIR, 'README.md')) as f:
7 README = f.read()
11 readme_contents = f.read()
12 markup_link_substitution = '[\\1](%s/%s/\\2)' % (BASE_GITHUB_URL, DEFAULT_BRANCH)
13 README = re.sub(MARKUP_LINK_REGEX, markup_link_substitution, readme_contents, flags=re.MULTILINE)
814
915 with open(path.join(FILE_DIR, 'requirements.txt')) as f:
1016 INSTALL_PACKAGES = f.read().splitlines()
4551 ],
4652 },
4753 include_package_data=True,
48 python_requires='>=3.5',
54 python_requires='>=3.7',
4955 zip_safe=False
5056 )
1111 class TestListBatcher(unittest.TestCase):
1212
1313 def test_batch_default_batch(self):
14 batcher, batch_to = default_batcher()
15 complete = batcher.add()
16 self.assertEqual(complete, True)
17 self.assertEqual(batcher.get_last_position(), len(DEFAULT_BATCH_FROM))
18 self.assertEqual(batch_to, DEFAULT_BATCH_FROM)
14 batcher, batch_to = default_batcher()
15 complete = batcher.add()
16 self.assertEqual(complete, True)
17 self.assertEqual(batcher.get_last_position(), len(DEFAULT_BATCH_FROM))
18 self.assertEqual(batch_to, DEFAULT_BATCH_FROM)
1919
2020 def test_batch_1(self):
21 batcher, batch_to = default_batcher()
22 complete = batcher.add(1)
23 self.assertEqual(complete, False)
24 self.assertEqual(batcher.get_last_position(), 1)
25 self.assertEqual(batch_to, ['a'])
21 batcher, batch_to = default_batcher()
22 complete = batcher.add(1)
23 self.assertEqual(complete, False)
24 self.assertEqual(batcher.get_last_position(), 1)
25 self.assertEqual(batch_to, ['a'])
2626
2727 def test_batch_5(self):
28 batcher, batch_to = default_batcher()
29 complete = batcher.add(5)
30 self.assertEqual(complete, False)
31 self.assertEqual(batcher.get_last_position(), 5)
32 self.assertEqual(batch_to, ['a', 'b', 'c', 'd', 'e'])
28 batcher, batch_to = default_batcher()
29 complete = batcher.add(5)
30 self.assertEqual(complete, False)
31 self.assertEqual(batcher.get_last_position(), 5)
32 self.assertEqual(batch_to, ['a', 'b', 'c', 'd', 'e'])
3333
3434 def test_batch_1_and_1(self):
35 batcher, batch_to = default_batcher()
36 batcher.add(1)
37 complete = batcher.add(1)
38 self.assertEqual(complete, False)
39 self.assertEqual(batcher.get_last_position(), 2)
40 self.assertEqual(batch_to, ['a', 'b'])
35 batcher, batch_to = default_batcher()
36 batcher.add(1)
37 complete = batcher.add(1)
38 self.assertEqual(complete, False)
39 self.assertEqual(batcher.get_last_position(), 2)
40 self.assertEqual(batch_to, ['a', 'b'])
4141
4242 def test_batch_1_and_rest(self):
43 batcher, batch_to = default_batcher()
44 batcher.add(1)
45 complete = batcher.add(0)
46 self.assertEqual(complete, True)
47 self.assertEqual(batcher.get_last_position(), len(DEFAULT_BATCH_FROM))
48 self.assertEqual(batch_to, DEFAULT_BATCH_FROM)
43 batcher, batch_to = default_batcher()
44 batcher.add(1)
45 complete = batcher.add(0)
46 self.assertEqual(complete, True)
47 self.assertEqual(batcher.get_last_position(), len(DEFAULT_BATCH_FROM))
48 self.assertEqual(batch_to, DEFAULT_BATCH_FROM)
4949
5050 def test_batch_batch_size_greater_than_default(self):
51 batcher, batch_to = default_batcher()
52 complete = batcher.add(100000)
53 self.assertEqual(complete, True)
54 self.assertEqual(batcher.get_last_position(), len(DEFAULT_BATCH_FROM))
55 self.assertEqual(batch_to, DEFAULT_BATCH_FROM)
51 batcher, batch_to = default_batcher()
52 complete = batcher.add(100000)
53 self.assertEqual(complete, True)
54 self.assertEqual(batcher.get_last_position(), len(DEFAULT_BATCH_FROM))
55 self.assertEqual(batch_to, DEFAULT_BATCH_FROM)
5656
5757 def test_batch_add_on_completed(self):
58 batcher, batch_to = default_batcher()
59 complete = batcher.add()
60 self.assertEqual(complete, True)
61 complete = batcher.add()
62 self.assertEqual(complete, True)
63 self.assertEqual(batcher.get_last_position(), len(DEFAULT_BATCH_FROM))
64 self.assertEqual(batch_to, DEFAULT_BATCH_FROM)
58 batcher, batch_to = default_batcher()
59 complete = batcher.add()
60 self.assertEqual(complete, True)
61 complete = batcher.add()
62 self.assertEqual(complete, True)
63 self.assertEqual(batcher.get_last_position(), len(DEFAULT_BATCH_FROM))
64 self.assertEqual(batch_to, DEFAULT_BATCH_FROM)
6565
6666 def test_batch_5_with_formatter(self):
67 def formatter(partial, start_idx):
68 return ['before'] + [row * 2 for row in partial] + ['after']
69 batch_to = []
70 batcher = ListBatcher(DEFAULT_BATCH_FROM, batch_to, batch_to_formatter=formatter)
71 complete = batcher.add(5)
72 self.assertEqual(complete, False)
73 self.assertEqual(batcher.get_last_position(), 5)
74 self.assertEqual(batch_to, ['before', 'aa', 'bb', 'cc', 'dd', 'ee', 'after'])
67 def formatter(partial, start_idx):
68 return ['before'] + [row * 2 for row in partial] + ['after']
69 batch_to = []
70 batcher = ListBatcher(DEFAULT_BATCH_FROM, batch_to, batch_to_formatter=formatter)
71 complete = batcher.add(5)
72 self.assertEqual(complete, False)
73 self.assertEqual(batcher.get_last_position(), 5)
74 self.assertEqual(batch_to, ['before', 'aa', 'bb', 'cc', 'dd', 'ee', 'after'])
7575
7676 if __name__ == '__main__':
7777 unittest.main()
1818 from vit import event
1919 from vit.loader import Loader
2020 from vit.config_parser import ConfigParser, TaskParser
21 from vit.util import clear_screen, string_to_args, is_mouse_event
21 from vit.util import clear_screen, string_to_args, is_mouse_event, task_id_or_uuid_short
2222 from vit.process import Command
2323 from vit.task import TaskListModel
2424 from vit.autocomplete import AutoComplete
5252 self.action_manager_registrar = self.action_manager.get_registrar()
5353 self.action_manager_registrar.register('REFRESH', self.refresh)
5454
55 def is_default_refresh_key(self, keys):
56 return keys == 'ctrl l'
57
5558 def keypress(self, size, key):
5659 keys = self.key_cache.get(key)
57 if self.action_manager_registrar.handled_action(keys):
60 if self.action_manager_registrar.handled_action(keys) and self.is_default_refresh_key(keys):
5861 # NOTE: Calling refresh directly here to avoid the
5962 # action-manager:action-executed event, which clobbers the load
6063 # time currently.
145148 self.action_manager_registrar = self.action_manager.get_registrar()
146149 self.action_manager_registrar.register('QUIT', self.quit)
147150 self.action_manager_registrar.register('QUIT_WITH_CONFIRM', self.activate_command_bar_quit_with_confirm)
151 # NOTE: This is a no-op for the default refresh keybinding, which is
152 # handled by the MainFrame() class. It's included here in case the user
153 # assigns a non-default key to the REFRESH action.
154 self.action_manager_registrar.register('REFRESH', self.refresh)
148155 self.action_manager_registrar.register('TASK_ADD', self.activate_command_bar_add)
149156 self.action_manager_registrar.register('REPORT_FILTER', self.activate_command_bar_filter)
150157 self.action_manager_registrar.register('TASK_UNDO', self.task_undo)
353360 elif len(args) > 0:
354361 if op == 'add':
355362 if self.execute_command(['task', 'add'] + args, wait=self.wait):
356 self.activate_message_bar('Task added')
363 task = self.task_get_latest()
364 self.activate_message_bar('Task %s added' % task_id_or_uuid_short(task))
365 self.focus_new_task(task)
357366 elif op == 'modify':
358367 # TODO: Will this break if user clicks another list item
359368 # before hitting enter?
379388 if 'uuid' in metadata:
380389 self.task_list.focus_by_task_uuid(metadata['uuid'], self.previous_focus_position)
381390
391 def focus_new_task(self, task):
392 if self.config.get('vit', 'focus_on_add'):
393 self.task_list.focus_by_task_uuid(task['uuid'], self.previous_focus_position)
394
382395 def key_pressed(self, key):
383396 if is_mouse_event(key):
384397 return None
437450 kwargs['wait'] = False
438451 else:
439452 kwargs['wait'] = True
453 uuid, _ = self.get_focused_task()
454 if not uuid:
455 uuid = ""
456 kwargs['custom_env'] = {
457 "VIT_TASK_UUID": uuid,
458 }
440459 self.execute_command(args, **kwargs)
441460 elif command.isdigit():
442461 self.task_list.focus_by_task_id(int(command))
484503 self.task_list.focus_position = new_focus
485504
486505 def search_rows(self, term, start_index=0, reverse=False):
487 search_regex = re.compile(term, re.MULTILINE)
506 escaped_term = re.escape(term)
507 search_regex = re.compile(escaped_term, re.MULTILINE)
488508 rows = self.table.rows
489509 current_index = start_index
490510 last_index = len(rows) - 1
535555 return False
536556
537557 def get_focused_task(self):
538 if self.widget.focus_position == 'body':
539 try:
540 uuid = self.task_list.focus.uuid
541 task = self.model.get_task(uuid)
542 return uuid, task
543 except:
544 pass
558 try:
559 uuid = self.task_list.focus.uuid
560 task = self.model.get_task(uuid)
561 return uuid, task
562 except:
563 pass
545564 return False, False
546565
547566 def quit(self):
636655 self.command_bar.activate(caption, metadata, edit_text)
637656 self.widget.focus_position = 'footer'
638657
639 def activate_command_bar_add(self):
640 self.activate_command_bar('add', 'Add: ')
641658
642659 def activate_command_bar_filter(self):
643660 self.activate_command_bar('filter', 'Filter: ')
647664
648665 def task_sync(self):
649666 self.execute_command(['task', 'sync'])
667
668 def task_get_latest(self):
669 returncode, stdout, stderr = self.command.run(['task', '+LATEST', 'uuids'], capture_output=True)
670 if returncode == 0:
671 return self.model.get_task(stdout)
672 else:
673 raise RuntimeError("Error retrieving latest task UUID: %s" % stderr)
650674
651675 def activate_command_bar_quit_with_confirm(self):
652676 if self.confirm:
682706 def global_escape(self):
683707 self.denotation_pop_up.close_pop_up()
684708
709 def activate_command_bar_add(self):
710 self.activate_command_bar('add', 'Add: ')
685711
686712 def task_done(self, uuid):
687713 success, task = self.model.task_done(uuid)
731757 def task_action_denotate(self):
732758 uuid, task = self.get_focused_task()
733759 if task and task['annotations']:
734 self.denotation_pop_up.open(task)
760 self.denotation_pop_up.open(task)
735761
736762 def task_action_modify(self):
737763 uuid, _ = self.get_focused_task()
908934 self.task_config.get_projects()
909935 self.refresh_blocking_task_uuids()
910936 self.formatter.recalculate_due_datetimes()
911 context_filters = self.contexts[self.context]['filter'] if self.context else []
937 context_filters = self.contexts[self.context]['filter'] if self.context and self.reports[self.report].get('context', 1) else []
912938 try:
913939 self.model.update_report(self.report, context_filters=context_filters, extra_filters=self.extra_filters)
914940 except VitException as err:
124124 self.teardown()
125125
126126 def activate(self, text, edit_pos, reverse=False):
127 if not self.is_setup:
128 return
127129 if self.activated:
128130 self.send_tabbed_text(text, edit_pos, reverse)
129131 return
179181
180182 def add_space_escaping(self, text):
181183 if self.space_escape_regex.match(text):
182 return text.replace(' ', '\ ')
184 parts = text.split(':', 1)
185 if len(parts) > 1:
186 return "%s:'%s'" % (parts[0], parts[1])
187 else:
188 return "'%s'" % text
183189 else:
184190 return text
185191
188194
189195 def parse_text(self, text, edit_pos):
190196 full_prefix = text[:edit_pos]
191 self.prefix_parts = list(map(self.add_space_escaping, util.string_to_args(full_prefix)))
197 self.prefix_parts = list(map(self.add_space_escaping, util.string_to_args_on_whitespace(full_prefix)))
192198 if not self.prefix_parts:
193199 self.search_fragment = self.prefix = full_prefix
194200 self.suffix = text[(edit_pos + 1):]
5151
5252 # Boolean. If true, hitting backspace against an empty prompt aborts the prompt.
5353 #abort_backspace = False
54
55 # Boolean. If true, VIT will focus on the newly added task. Note: the new task must be
56 #included in the active filter for this setting to have effect.
57 #focus_on_add = False
5458
5559 [report]
5660
1111 except ImportError:
1212 import ConfigParser as configparser
1313
14 from vit import env
14 from vit import env, xdg
1515 from vit.process import Command
1616
1717 SORT_ORDER_CHARACTERS = ['+', '-']
4444 'wait': True,
4545 'mouse': False,
4646 'abort_backspace': False,
47 'focus_on_add': False,
4748 },
4849 'report': {
4950 'default_report': 'next',
9697 def __init__(self, loader):
9798 self.loader = loader
9899 self.config = configparser.SafeConfigParser()
99 self.config.optionxform=str
100 self.config.optionxform = str
100101 self.user_config_dir = self.loader.user_config_dir
101102 self.user_config_filepath = '%s/%s' % (self.user_config_dir, VIT_CONFIG_FILE)
102103 if not self.config_file_exists(self.user_config_filepath):
103104 self.optional_create_config_file(self.user_config_filepath)
105 self.config.read(self.user_config_filepath)
104106 self.taskrc_path = self.get_taskrc_path()
105107 self.validate_taskrc()
106 self.config.read(self.user_config_filepath)
107108 self.defaults = DEFAULTS
108109 self.set_config_data()
109110
185186 return True if CONFIG_BOOLEAN_TRUE_REGEX.match(value) else False
186187
187188 def get_taskrc_path(self):
188 return os.path.expanduser('TASKRC' in env.user and env.user['TASKRC'] or self.get('taskwarrior', 'taskrc'))
189 taskrc_path = os.path.expanduser('TASKRC' in env.user and env.user['TASKRC'] or self.get('taskwarrior', 'taskrc'))
190
191 if not os.path.exists(taskrc_path):
192 xdg_dir = xdg.get_xdg_config_dir(taskrc_path, "task")
193 if xdg_dir:
194 taskrc_path = os.path.join(xdg_dir, "taskrc")
195
196 return taskrc_path
189197
190198 def is_subproject_indentable(self):
191199 return self.get('report', 'indent_subprojects')
264272 return list(filter(lambda config_pair: re.match(matcher_regex, config_pair[0]), self.task_config))
265273
266274 def subtree(self, matcher, walk_subtree=True):
267 matcher_regex = matcher
268 if walk_subtree:
269 matcher_regex = r'%s' % (('^%s' % matcher).replace('.', '\.'))
270 full_tree = {}
271 lines = self.filter(matcher_regex)
272 for (hierarchy, value) in lines:
273 # NOTE: This is necessary in order to convert Taskwarrior's dotted
274 # config syntax into a full tree, as some leaves are both branches
275 # and leaves.
276 hierarchy = self.transform_string_leaves(hierarchy)
277 parts = hierarchy.split('.')
278 tree_location = full_tree
279 while True:
280 if len(parts):
281 part = parts.pop(0)
282 if part not in tree_location:
283 tree_location[part] = {} if len(parts) else value
284 tree_location = tree_location[part]
285 else:
286 break
287 if walk_subtree:
288 parts = matcher.split('.')
289 subtree = full_tree
290 while True:
291 if len(parts):
292 part = parts.pop(0)
293 if part in subtree:
294 subtree = subtree[part]
295 else:
296 return subtree
297 else:
298 return full_tree
275 matcher_regex = matcher
276 if walk_subtree:
277 matcher_regex = r'%s' % (('^%s' % matcher).replace('.', '\.'))
278 full_tree = {}
279 lines = self.filter(matcher_regex)
280 for (hierarchy, value) in lines:
281 # NOTE: This is necessary in order to convert Taskwarrior's dotted
282 # config syntax into a full tree, as some leaves are both branches
283 # and leaves.
284 hierarchy = self.transform_string_leaves(hierarchy)
285 parts = hierarchy.split('.')
286 tree_location = full_tree
287 while True:
288 if len(parts):
289 part = parts.pop(0)
290 if part not in tree_location:
291 tree_location[part] = {} if len(parts) else value
292 tree_location = tree_location[part]
293 else:
294 break
295 if walk_subtree:
296 parts = matcher.split('.')
297 subtree = full_tree
298 while True:
299 if len(parts):
300 part = parts.pop(0)
301 if part in subtree:
302 subtree = subtree[part]
303 else:
304 return subtree
305 else:
306 return full_tree
299307
300308 def parse_sort_column(self, column_string):
301309 order = collate = None
323331 self.get_task_config()
324332 subtree = self.subtree('context.')
325333 for context, filters in list(subtree.items()):
326 filters = shlex.split(re.sub(FILTER_PARENS_REGEX, r' \1 ', filters))
327334 contexts[context] = {
328 'filter': [f for f in filters if not FILTER_EXCLUSION_REGEX.match(f)],
335 'filter': self.parse_context_filters(context, filters),
329336 }
330337 self.contexts = contexts
331338 return self.contexts
332339
340 def parse_context_filters(self, context, filters):
341 # Filters can be a string (pre-2.6.0 definition, context.work=+work)
342 # or a dict (2.6.0 and newer, context.work.read=+work and
343 # context.work.write=+work).
344 if type(filters) is dict:
345 if 'read' in filters:
346 filters = filters['read']
347 else:
348 return [] # Only contexts with read component defined should be considered.
349 filters = shlex.split(re.sub(FILTER_PARENS_REGEX, r' \1 ', filters))
350 final_filters = [f for f in filters if not FILTER_EXCLUSION_REGEX.match(f)]
351 return final_filters
352
333353 def get_reports(self):
334 reports = {}
335 subtree = self.subtree('report.')
336 for report, attrs in list(subtree.items()):
337 if report in self.disallowed_reports:
338 continue
339 reports[report] = {
340 'name': report,
341 'subproject_indentable': False,
342 }
343 if 'columns' in attrs:
344 reports[report]['columns'] = attrs['columns'].split(',')
345 if 'description' in attrs:
346 reports[report]['description'] = attrs['description']
347 if 'filter' in attrs:
348 # Allows quoted strings.
349 # Adjust for missing spaces around parentheses.
350 filters = shlex.split(re.sub(FILTER_PARENS_REGEX, r' \1 ', attrs['filter']))
351 reports[report]['filter'] = [f for f in filters if not FILTER_EXCLUSION_REGEX.match(f)]
352 if 'labels' in attrs:
353 reports[report]['labels'] = attrs['labels'].split(',')
354 else:
355 reports[report]['labels'] = [ column.title() for column in attrs['columns'].split(',') ]
356 if 'sort' in attrs:
357 columns = attrs['sort'].split(',')
358 reports[report]['sort'] = [self.parse_sort_column(c) for c in columns]
359 if 'dateformat' in attrs:
360 reports[report]['dateformat'] = self.translate_date_markers(attrs['dateformat'])
361
362 self.reports = reports
363 # Another pass is needed after all report data has been parsed.
364 for report_name, report in self.reports.items():
365 self.reports[report_name] = self.rectify_report(report_name, report)
366 return self.reports
354 reports = {}
355 subtree = self.subtree('report.')
356 for report, attrs in list(subtree.items()):
357 if report in self.disallowed_reports:
358 continue
359 reports[report] = {
360 'name': report,
361 'subproject_indentable': False,
362 }
363 if 'columns' in attrs:
364 reports[report]['columns'] = attrs['columns'].split(',')
365 if 'context' in attrs:
366 reports[report]['context'] = int(attrs['context'])
367 if 'description' in attrs:
368 reports[report]['description'] = attrs['description']
369 if 'filter' in attrs:
370 # Allows quoted strings.
371 # Adjust for missing spaces around parentheses.
372 filters = shlex.split(re.sub(FILTER_PARENS_REGEX, r' \1 ', attrs['filter']))
373 reports[report]['filter'] = [f for f in filters if not FILTER_EXCLUSION_REGEX.match(f)]
374 if 'labels' in attrs:
375 reports[report]['labels'] = attrs['labels'].split(',')
376 else:
377 reports[report]['labels'] = [column.title() for column in attrs['columns'].split(',')]
378 if 'sort' in attrs:
379 columns = attrs['sort'].split(',')
380 reports[report]['sort'] = [self.parse_sort_column(c) for c in columns]
381 if 'dateformat' in attrs:
382 reports[report]['dateformat'] = self.translate_date_markers(attrs['dateformat'])
383
384 self.reports = reports
385 # Another pass is needed after all report data has been parsed.
386 for report_name, report in self.reports.items():
387 self.reports[report_name] = self.rectify_report(report_name, report)
388 return self.reports
367389
368390 def rectify_report(self, report_name, report):
369391 report['subproject_indentable'] = self.has_project_column(report_name) and self.has_primary_project_ascending_sort(report)
00 import math
11 from datetime import datetime
2 from pytz import timezone
2 try:
3 from zoneinfo import ZoneInfo
4 except ImportError:
5 from backports.zoneinfo import ZoneInfo
36
47 TIME_UNIT_MAP = {
58 'seconds': {
149152 def age(self, dt):
150153 if dt == None:
151154 return ''
152 now = datetime.now(self.formatter.zone)
155 now = datetime.now().astimezone()
153156 seconds = (now - dt).total_seconds()
154157 return self.format_duration_vague(seconds)
155158
156159 def countdown(self, dt):
157160 if dt == None:
158161 return ''
159 now = datetime.now(self.formatter.zone)
162 now = datetime.now().astimezone()
160163 if dt < now:
161164 return ''
162165 seconds = (dt - now).total_seconds()
165168 def relative(self, dt):
166169 if dt == None:
167170 return ''
168 now = datetime.now(self.formatter.zone)
171 now = datetime.now().astimezone()
169172 seconds = (dt - now).total_seconds()
170173 return self.format_duration_vague(seconds)
171174
172175 def remaining(self, dt):
173176 if dt == None:
174177 return ''
175 now = datetime.now(self.formatter.zone)
178 now = datetime.now().astimezone()
176179 if dt < now:
177180 return ''
178181 seconds = (dt - now).total_seconds()
197200 def iso(self, dt):
198201 if dt == None:
199202 return ''
200 dt = dt.replace(tzinfo=timezone('UTC'))
203 dt = dt.replace(tzinfo=ZoneInfo('UTC'))
201204 return dt.isoformat()
202205
203206 class List(Formatter):
0 import unicodedata
01 from vit.formatter import Marker
12
23 class Markers(Marker):
3435 def add_label(self, color, label, width, text_markup):
3536 if self.color_required(color) or not label:
3637 return width, text_markup
37 width += len(label)
38 width += len(label) + len([c for c in label if unicodedata.east_asian_width(c) == 'W'])
3839 text_markup += [(color, label)]
3940 return width, text_markup
4041
00 from importlib import import_module
11 from datetime import datetime, timedelta
2 from tzlocal import get_localzone
3 from pytz import timezone
2 try:
3 from zoneinfo import ZoneInfo
4 except ImportError:
5 from backports.zoneinfo import ZoneInfo
46
57 from vit import util
68 from vit import uda
2628 self.report = self.task_config.translate_date_markers(self.task_config.subtree('dateformat.report')) or self.date_default
2729 self.annotation = self.task_config.translate_date_markers(self.task_config.subtree('dateformat.annotation')) or self.date_default
2830 self.description_truncate_len = DEFAULT_DESCRIPTION_TRUNCATE_LEN
29 self.zone = get_localzone()
30 self.epoch_datetime = datetime(1970, 1, 1, tzinfo=timezone('UTC'))
31 self.epoch_datetime = datetime(1970, 1, 1, tzinfo=ZoneInfo('UTC'))
3132 self.due_days = int(self.task_config.subtree('due'))
3233 self.none_label = config.get('color', 'none_label')
3334 self.build_indicators()
9697 return (width, ' ' * space_padding , indicator, subproject)
9798
9899 def recalculate_due_datetimes(self):
99 self.now = datetime.now(self.zone)
100 self.now = datetime.now().astimezone()
100101 # NOTE: For some reason using self.zone for the tzinfo below results
101102 # in the tzinfo object having a zone of 'LMT', which is wrong. Using
102103 # the tzinfo associated with self.now returns the correct value, no
33 except:
44 import imp
55
6 from vit import env
6 from vit import env, xdg
77
88 DEFAULT_VIT_DIR = '~/.vit'
99
1010 class Loader(object):
1111 def __init__(self):
1212 self.user_config_dir = os.path.expanduser('VIT_DIR' in env.user and env.user['VIT_DIR'] or DEFAULT_VIT_DIR)
13
14 if not os.path.exists(self.user_config_dir):
15 xdg_dir = xdg.get_xdg_config_dir(self.user_config_dir, "vit")
16 if xdg_dir:
17 self.user_config_dir = xdg_dir
1318
1419 def load_user_class(self, module_type, module_name, class_name):
1520 module = '%s.%s' % (module_type, module_name)
00 import os
11 import re
22 import subprocess
3 import copy
34
45 from vit import env
56 from vit.util import clear_screen, string_to_args
1314 self.env = env.user.copy()
1415 self.env['TASKRC'] = self.config.taskrc_path
1516
16 def run(self, command, capture_output=False):
17 def run(self, command, capture_output=False, custom_env={}):
1718 if isinstance(command, str):
1819 command = string_to_args(command)
20 env = copy.copy(self.env)
21 env.update(custom_env)
1922 kwargs = {
20 'env': self.env,
23 'env': env,
2124 }
2225 if capture_output:
2326 kwargs.update({
3538 returncode = 1
3639 return returncode, stdout, self.filter_errors(returncode, stderr)
3740
38 def result(self, command, confirm=DEFAULT_CONFIRM, capture_output=False, print_output=False, clear=True):
41 def result(self, command, confirm=DEFAULT_CONFIRM, capture_output=False, print_output=False, clear=True, custom_env={}):
3942 if clear:
4043 clear_screen()
41 returncode, stdout, stderr = self.run(command, capture_output)
44 returncode, stdout, stderr = self.run(command, capture_output, custom_env)
4245 output = returncode == 0 and stdout or stderr
4346 if print_output:
4447 print(output)
7272
7373 edit_text = self.edit_obj.get_edit_text()
7474 self.edit_obj.set_edit_text(
75 edit_text[: new_edit_pos - 2] +
76 edit_text[new_edit_pos - 1] +
77 edit_text[new_edit_pos - 2] +
78 edit_text[new_edit_pos :])
75 edit_text[: new_edit_pos - 2] +
76 edit_text[new_edit_pos - 1] +
77 edit_text[new_edit_pos - 2] +
78 edit_text[new_edit_pos :])
7979 self.edit_obj.set_edit_pos(new_edit_pos)
8080 return None
8181 # Delete backwards to the beginning of the line.
1212
1313 def string_to_args(string):
1414 try:
15 return shlex.split(string)
15 return shlex.split(string)
1616 except ValueError:
17 return []
17 return []
18
19 def string_to_args_on_whitespace(string):
20 try:
21 lex = shlex.shlex(string)
22 lex.whitespace_split = True
23 return list(lex)
24 except ValueError:
25 return []
1826
1927 def is_mouse_event(key):
2028 return not isinstance(key, str)
0 VIT = '2.1.0'
0 VIT = '2.2.0'
0 import os
1
2 from vit import env
3
4
5 def get_xdg_config_dir(user_config_dir, resource):
6 xdg_config_home = env.user.get("XDG_CONFIG_HOME") or os.path.join(
7 os.path.expanduser("~"), ".config"
8 )
9
10 xdg_config_dirs = [xdg_config_home] + (
11 env.user.get("XDG_CONFIG_DIRS") or "/etc/xdg"
12 ).split(":")
13
14 for config_dir in xdg_config_dirs:
15 path = os.path.join(config_dir, resource)
16 if os.path.exists(path):
17 return path
18 return None