Codebase list vit / bbf5012
Update upstream source from tag 'upstream/2.1.0' Update to upstream version '2.1.0' with Debian dir ff6319a1a9c8b4cca2001fb6e9a8b8f9e94e2610 Jochen Sprickerhof 2 years ago
38 changed file(s) with 839 addition(s) and 186 deletion(s). Raw diff Collapse all Expand all
0 ---
1 name: Bug report
2 about: Create a report to help us improve
3 title: ''
4 labels: ''
5 assignees: ''
6
7 ---
8
9 **Describe the bug**
10 <!-- A clear and concise description of what the bug is. -->
11
12 **To Reproduce**
13 <!-- Steps to reproduce the behavior. -->
14
15 **Expected behavior**
16 <!-- A clear and concise description of what you expected to happen. -->
17
18 **Test case**
19 If reproducing your issue requires any TaskWarrior setup, please produce a [test case](https://github.com/vit-project/vit/blob/2.x/TEST-CASE.md) script.
0 ---
1 name: Feature request
2 about: Suggest an idea for this project
3 title: ''
4 labels: ''
5 assignees: ''
6
7 ---
8
9 **Is your feature request related to a problem? Please describe.**
10 <!-- A clear and concise description of what the problem is. -->
11
12 **Describe the solution you'd like**
13 <!-- A clear and concise description of what you want to happen. -->
14
15 **Additional context**
16 <!-- Add any other context or screenshots about the feature request here. -->
0 # Byte-compiled / optimized / DLL files
1 __pycache__/
2 *.py[cod]
3 *$py.class
4
5 # C extensions
6 *.so
7
8 # Distribution / packaging
9 .Python
10 build/
11 develop-eggs/
12 dist/
13 downloads/
14 eggs/
15 .eggs/
16 lib/
17 lib64/
18 parts/
19 sdist/
20 var/
21 wheels/
22 share/python-wheels/
23 *.egg-info/
24 .installed.cfg
25 *.egg
26 MANIFEST
27
28 # PyInstaller
29 # Usually these files are written by a python script from a template
30 # before PyInstaller builds the exe, so as to inject date/other infos into it.
31 *.manifest
32 *.spec
33
34 # Installer logs
35 pip-log.txt
36 pip-delete-this-directory.txt
37
38 # Unit test / coverage reports
39 htmlcov/
40 .tox/
41 .nox/
42 .coverage
43 .coverage.*
44 .cache
45 nosetests.xml
46 coverage.xml
47 *.cover
48 *.py,cover
49 .hypothesis/
50 .pytest_cache/
51 cover/
52
53 # Translations
54 *.mo
55 *.pot
56
57 # Django stuff:
58 *.log
59 local_settings.py
60 db.sqlite3
61 db.sqlite3-journal
62
63 # Flask stuff:
64 instance/
65 .webassets-cache
66
67 # Scrapy stuff:
68 .scrapy
69
70 # Sphinx documentation
71 docs/_build/
72
73 # PyBuilder
74 .pybuilder/
75 target/
76
77 # Jupyter Notebook
78 .ipynb_checkpoints
79
80 # IPython
81 profile_default/
82 ipython_config.py
83
84 # pyenv
85 # For a library or package, you might want to ignore these files since the code is
86 # intended to run in multiple environments; otherwise, check them in:
087 .python-version
188
2 # Setuptools build/distribution folder.
3 /build/
4 /dist/
89 # pipenv
90 # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
91 # However, in case of collaboration, if having platform-specific dependencies or dependencies
92 # having no cross-platform support, pipenv may install dependencies that don't work, or not
93 # install all needed dependencies.
94 #Pipfile.lock
595
6 # Python egg metadata, regenerated from source files by setuptools.
7 /*.egg-info
96 # PEP 582; used by e.g. github.com/David-OConnor/pyflow
97 __pypackages__/
98
99 # Celery stuff
100 celerybeat-schedule
101 celerybeat.pid
102
103 # SageMath parsed files
104 *.sage.py
105
106 # Environments
107 .env
108 .venv
109 env/
110 venv/
111 ENV/
112 env.bak/
113 venv.bak/
114
115 # Spyder project settings
116 .spyderproject
117 .spyproject
118
119 # Rope project settings
120 .ropeproject
121
122 # mkdocs documentation
123 /site
124
125 # mypy
126 .mypy_cache/
127 .dmypy.json
128 dmypy.json
129
130 # Pyre type checker
131 .pyre/
132
133 # pytype static type analyzer
134 .pytype/
135
136 # Cython debug symbols
137 cython_debug/
0 ##### Sun Feb 28 2021 - released v2.1.0
1
2 This release includes a breaking change to the keybinding parser, and may affect
3 users who have implemented custom keybindings in their configuration.
4
5 See https://github.com/vit-project/vit/commit/dd0f34347e7b77dce37fe72e3797581d212f0d90 for more information.
6
7 * **Mon Feb 01 2021:** fix: blocked marker displaying for deleted/completed depends
8 * **Thu Jan 28 2021:** add quick start instructions to README
9 * **Wed Dec 30 2020:** [BREAKING CHANGE] correctly parse bracket expressions for keybinding keys
10 * **Thu Dec 24 2020:** Add 'abort_backspace' config, False by default. If true, hitting backspace against an empty prompt aborts the prompt.
11 * **Fri Dec 25 2020:** Use Python.ignore from github
12
13 ##### Wed Dec 23 2020 - released v2.1.0b1
14
15 This release includes compatibility with Taskwarrior 2.5.2 -- earlier releases
16 of VIT may not work with Taskwarrior 2.5.2 and beyond.
17
18 * **Wed Dec 23 2020:** update URL to vit-project
19 * **Wed Oct 14 2020:** Make VIT uppercase
20 * **Wed Oct 14 2020:** Add digit-jumping keybindings suggestion to CUSTOMIZE doc
21 * **Sun Sep 13 2020:** fix #269: Can't set priority when uda.priority.values is customized
22 * **Fri Sep 11 2020:** fix #266: print.empty.columns truthness values are not properly handled
23 * **Fri Sep 11 2020:** fix #265: Project name completion only works for projects with pending tasks
24 * **Fri Sep 11 2020:** add logo
25 * **Thu Sep 03 2020:** fix #268: remove priority formatters, use uda formatters instead
26 * **Tue Jul 28 2020:** fix #256: Non-pending blocking tasks should not appear in reports as dependencies
27 * **Tue Jul 28 2020:** fix #260: n / N commands should honor search direction
28 * **Tue Jul 28 2020:** add debug section to devel readme
29 * **Mon Jul 27 2020:** Update issue templates
30 * **Sun Jul 26 2020:** add TEST-CASE description
31 * **Sat Jul 25 2020:** fix #255: vit freezes on startup if there is no taskrc file present
32 * **Tue Jul 07 2020:** API for variable replacements in keybindings
33 * **Wed Jul 22 2020:** fix #253: Crash on switch to newly created context
34 * **Mon Jul 20 2020:** fix #252: catch filter errors and display nicely
35 * **Sat Jul 18 2020:** Fix: #251 -- Filtering with empty attribute value yields a crash
36 * **Wed Jul 15 2020:** fix #250: provide default labels when none configured in report
37 * **Fri Jul 10 2020:** provide some feedback on failed user module load
38 * **Sat May 16 2020:** Fix crash when report has project column but no sort
39 * **Wed Mar 11 2020:** Fix #203: Column resizing incorrect when column labels are wider than column
40 * **Fri Mar 06 2020:** fix #222, fix #221: first step to properly supporting spaces in tab completion
41 * **Sat Feb 29 2020:** Fix #227: Provide default report/annotation format if none configured
42 * **Tue Feb 25 2020:** resolves #225: document keybinding config for capital letters
43 * **Sun Nov 03 2019:** fix #176: Readline edit shortcuts for command bar
44 * **Wed Jan 08 2020:** fix #186, part two: get correct project column index after cleaning
45 * **Wed Jan 08 2020:** add project to dummy task script
46 * **Sat Dec 07 2019:** fix #218: specify minimum versions for dependent packages
47 * **Tue Nov 05 2019:** Allows config option value to contain '='
48 * **Sun Oct 20 2019:** fix #211: Space at the end of keybinding not parsed
49 * **Sun Oct 13 2019:** cleanup on #203, only resize again if any columns need it
50 * **Sun Oct 13 2019:** fix #203: Descriptions not shown when mulitple columns need reducing
51 * **Sun Oct 13 2019:** fix display of last header column when abuts side of terminal window
52 * **Sun Oct 06 2019:** fix #206: passing negative filters through to task don't work
53 * **Wed Oct 02 2019:** Fix #192: add disallowed reports with error message
54
055 ##### Sat Sep 28 2019 - released v2.0.0
156
257 * **Fri Sep 27 2019:** add UPGRADE.md, include v2.0.0 upgrade instructions
51106 * Table-row striping
52107 * Show version/context/report execution time in status area
53108 * Customizable config dir
54 * Comand line bash completion wrapper
109 * Command line bash completion wrapper
55110
56111 This release also changes the software license from GPL to MIT.
57112
195250 Fri Nov 30 2012 - added ./configure (autoconf) (e.g. "./configure --prefix=/usr/local/vit")
196251 Thu Nov 29 2012 - added support for '^w' (erase word) at the command line
197252 Thu Nov 29 2012 - added default.command to the list of available reports
198 Wed Nov 28 2012 - added support for ":REPORT <filter>" syntax (e.g. ":mimimal prio:H")
253 Wed Nov 28 2012 - added support for ":REPORT <filter>" syntax (e.g. ":minimal prio:H")
199254 Wed Nov 28 2012 - added support for the DEL key ('^?') as per bug #1134
200255 ```
201256
5858
5959 To see the list of actions that can be mapped, execute ```vit --list-actions```.
6060
61 ##### To override default keybindings:
61 #### To override default keybindings:
6262
6363 The ```[keybinding]``` section in ```config.ini``` overrides any core keybindings or keybindings that you place in the user ```keybinding``` directory. If you just want to make some small tweaks and/or add some macros, it's probably better to take this approach.
6464
65 The [config.sample.ini](https://github.com/scottkosty/vit/blob/2.x/vit/config/config.sample.ini) has many examples to illustrate how to customize keybindings for actions and add macros, check it out!
65 The [config.sample.ini](vit/config/config.sample.ini) has many examples to illustrate how to customize keybindings for actions and add macros, check it out!
6666
67 ##### To provide your own default keybindings:
67 #### To provide your own custom variable replacements:
68
69 VIT exposes a simple API to provide your own variable replacements in keybindings.
70
71 Variables enclosed in curly brackets that VIT doesn't know about will be passed to your custom code,
72 where you can match against the variable, and parse the string to extract metadata in the form of
73 arguments that you pass to your custom replacement callback.
74
75 To provide custom variable replacement:
76
77 1. In your user ```keybinding``` directory, create ```keybinding.py```
78 2. Create a ```Keybinding``` class in that file
79 3. Expose a ```replacements``` method in that class, that returns a list of replacement configuration objects
80 4. Replacement configuration objects have the following keys:
81 * match_callback: should be a function with one argument, which is the variable to test for a match against.
82 If the variable matches your case, return a list with any arguments you want passed to your replacement callback.
83 * replacement_callback: should be a function, with the task object as the first argument, followed by the other
84 arguments you returned from the match callback, and should return a string replacement for the variable.
85
86 Here's a minimal example:
87
88 ```python
89 # keybinding/keybinding.py
90 class Keybinding(object):
91 def replacements(self):
92 def _custom_match(variable):
93 if variable == 'TEST':
94 return ['pass']
95 def _custom_replace(task, arg):
96 return 'TEST:%s' % arg
97 return [
98 {
99 'match_callback': _custom_match,
100 'replacement_callback': _custom_replace,
101 },
102 ]
103 ```
104
105 #### To provide your own default keybindings:
68106
69107 *NOTE: This functionality is more suited to users who want to do something completely different than a Vi-style workflow -- most users will simply want to make some tweaks in the ```[keybinding]``` section of ```config.ini```.*
70108
71109 1. Create a ```keybinding``` directory in the user directory
72110 2. Copy over one of the core keybindings, and customize to your liking.
73111 3. Set the ```default_keybindings``` setting in ```config.ini``` to the name of the keybinding file you created, without the ```.ini``` extension. For example, if you created ```keybinding/strange.ini```, you would set ```default_keybindings = strange``` in ```config.ini```
112
113 #### Keybinding suggestions
114
115 ##### Jumping with digits
116
117 As jumping to tasks is central to VIT's operation, you might want to map each
118 digit key to an `ex` command containing that digit, by adding the following to
119 your keybindings:
120
121 ```
122 1 = :1
123 2 = :2
124 3 = :3
125 4 = :4
126 5 = :5
127 6 = :6
128 7 = :7
129 8 = :8
130 9 = :9
131 ```
132
133 Now, for example, to jump to a task whose ID is 42, you need to press `4`, `2`
134 and `<Enter>`, instead of `:`, `4`, `2` and `<Enter>`.
135 This saves a `:` keypress whenever jumping to a task.
1111 ### Tests
1212 * Located in the ```tests``` directory
1313 * Run with ```./run-tests.sh```
14
15 ### Debugging
16
17 VIT comes with a simple ```debug``` class for pretty-printing variables to
18 console or file. The file's default location is ```vit-debug.log``` in the
19 system's temp directory.
20
21 Usage in any of the code is straighforward:
22
23 ```python
24 import debug
25 debug.console(variable_name)
26 debug.file(variable_name)
27 ```
1428
1529 ### Architecture
1630
00 # VIT
1
2 <img src="images/great-tit-square-small.png" alt="Logo" width="150" height="150" align="right" />
13
24 Visual Interactive Taskwarrior full-screen terminal interface.
35
4 *For VIT 1.3, [visit here](https://github.com/scottkosty/vit/tree/1.3)*
6 *For VIT 1.3, [visit here](https://github.com/vit-project/vit/tree/1.3)*
7
58
69 ## Features
710
2528
2629 Follow the directions in [INSTALL.md](INSTALL.md)
2730
31 ## Quick start
32
33 Run ```vit --help``` from the command line for basic usage instructions.
34
35 Run ```vit``` from the command line to start VIT with default config, report, and filters.
36
37 While VIT is running, type ```:help``` followed by enter to review basic command/navigation actions.
38
2839 #### Recommendations:
2940
3041 * VIT will suggest to install a default user config file if none exists -- it's fully commented with all configuration options, check it out.
3142 * Do ```vit --help``` *(know the vit command line arguments)*
3243 * Do ```:help``` in vit *(look over the "commands")*
3344 * Use an xterm terminal *(for full color support)*
34 * For suggestions on futher tweaks, see [CUSTOMIZE.md](CUSTOMIZE.md)
45 * For suggestions on further tweaks, see [CUSTOMIZE.md](CUSTOMIZE.md)
3546 * VIT handles task coloring differently than Taskwarrior, see [COLOR.md](COLOR.md) for more details
3647
3748 #### Troubleshooting:
0 ## Creating a test case
1
2 If you file an issue that requires some TaskWarrior setup to replicate, the
3 developers may ask you to create a 'test case' script in order to aid in
4 the troubleshooting.
5
6 Doing so is fairly straightforward:
7
8 1. Use the [dummy install script](scripts/generate-dummy-install.sh) as a
9 starting place
10 2. Adjust the script by editing the the task commands
11 * You can remove the default ones if needed
12 * You can add any new ```task``` command needed to set up the data necessary
13 to reproduce your issue
14 3. Once you have the script complete, run it locally to make sure you can
15 reproduce the issue you're reporting
16 4. Attach the script to the issue you've filed
00 This file contains information relevant to upgrading VIT from one version to another. Breaking changes between major versions, and significant changes between release versions will be addressed.
11
2 *Note: for upgrade issues prior to VIT 1.3, please see the [legacy changelog](https://github.com/scottkosty/vit/blob/1.3/CHANGES)*.
2 *Note: for upgrade issues prior to VIT 1.3, please see the [legacy changelog](https://github.com/vit-project/vit/blob/1.3/CHANGES)*.
33
44 # v2.0.0
55
1616 * Table-row striping
1717 * Show version/context/report execution time in status area
1818 * Customizable config dir *(see [CUSTOMIZE.md](CUSTOMIZE.md))*
19 * Comand line bash completion wrapper *(see [INSTALL.md](INSTALL.md))*
19 * Command line bash completion wrapper *(see [INSTALL.md](INSTALL.md))*
2020 * Context support
2121
2222 This release also changes the software license from GPL to MIT.
2424 ### Breaking changes:
2525
2626 * Configuration has been moved from ```${HOME}/.vitrc``` to ```${HOME}/.vit/config.ini``` -- the location of the config directory can be customized, see [CUSTOMIZE.md](CUSTOMIZE.md) for details.
27 * The format of the configuration file has changed, customizations in the legacy ```.vitrc``` file will need to be manually ported to the new format. The [config.sample.ini](https://github.com/scottkosty/vit/blob/2.x/vit/config/config.sample.ini) file is *heavily* commented, and should contain reference to everything you need to migrate the legacy configuration. If no ```config.ini``` exists in the VIT configuration directory, VIT will offer the option to install the sample config upon startup -- this is the easiest way to get started with porting and customization.
28 * The method of denotating tasks has changed. It is now mapped to the ```ACTION_TASK_DENOTATE``` core action, which in the default keybindings is triggered by ```<Shift>e``` when the task is highlighted.
27 * The format of the configuration file has changed, customizations in the legacy ```.vitrc``` file will need to be manually ported to the new format. The [config.sample.ini](vit/config/config.sample.ini) file is *heavily* commented, and should contain reference to everything you need to migrate the legacy configuration. If no ```config.ini``` exists in the VIT configuration directory, VIT will offer the option to install the sample config upon startup -- this is the easiest way to get started with porting and customization.
28 * The method of removing annotations from tasks has changed. It is now mapped to the ```ACTION_TASK_DENOTATE``` core action, which in the default keybindings is triggered by ```<Shift>e``` when the task is highlighted.
2929 * VIT 1.3 supports Taskd sync via the ```s``` keybinding, which was undocumented. VIT 2.x properly documents this functionality, and moves it to the keybinding ```<Shift>s``` by default.
3030 * The ```burndown``` configuration option and display has been removed -- it may be added again in a future release or via plugin functionality.
0 pytz
1 tasklib
2 tzlocal
3 urwid
0 pytz>=2019.3
1 tasklib>=1.3.0
2 tzlocal>=2.0.0
3 urwid>=2.1.0
2929 task +LATEST start
3030 task add c
3131 task +LATEST start
32 task +LATEST modify project:foo
3233
3334 echo "Complete!
3435
2020 long_description_content_type='text/markdown',
2121 install_requires=INSTALL_PACKAGES,
2222 version=VERSION,
23 url='https://github.com/scottkosty/vit',
23 url='https://github.com/vit-project/vit',
2424 author='Chad Phillips',
2525 author_email='chad@apartmentlines.com',
2626 classifiers=[
1313 import urwid
1414
1515 from vit import version
16 from vit.exception import VitException
1617 from vit.formatter_base import FormatterBase
1718 from vit import event
1819 from vit.loader import Loader
9596 def set_active_context(self):
9697 self.context = self.task_config.get_active_context()
9798
99 def load_contexts(self):
100 self.contexts = self.task_config.get_contexts()
101
98102 def bootstrap(self, load_early_config=True):
99103 self.loader = Loader()
100104 if load_early_config:
101105 self.load_early_config()
102 self.contexts = self.task_config.get_contexts()
106 self.load_contexts()
103107 self.set_active_context()
104108 self.event = event.Emitter()
105109 self.setup_config()
106110 self.search_term_active = ''
111 self.search_direction_reverse = False
107112 self.action_registry = ActionRegistry()
108113 self.actions = Actions(self.action_registry)
109114 self.actions.register()
110115 self.keybinding_parser = KeybindingParser(self.loader, self.config, self.action_registry)
116 self.command = Command(self.config)
117 self.get_available_task_columns()
111118 self.setup_keybindings()
112119 self.action_manager = ActionManagerRegistry(self.action_registry, self.key_cache.keybindings, event=self.event)
113120 self.register_managed_actions()
114 self.command = Command(self.config)
115121 self.markers = Markers(self.config, self.task_config)
116122 self.theme = self.init_theme()
117123 self.theme_alt_backgrounds = self.get_theme_alt_backgrounds()
166172 self.action_manager_registrar.register('TASK_EDIT', self.task_action_edit)
167173 self.action_manager_registrar.register('TASK_SHOW', self.task_action_show)
168174
175 def default_keybinding_replacements(self):
176 import json
177 from datetime import datetime
178 task_replacement_match = re.compile("^TASK_(\w+)$")
179 def _task_attribute_match(variable):
180 matches = re.match(task_replacement_match, variable)
181 if matches:
182 attribute = matches.group(1).lower()
183 if attribute in self.available_columns:
184 return [attribute]
185 def _task_attribute_replace(task, attribute):
186 if task and task[attribute]:
187 if type(task[attribute]) in ['set', 'tuple', 'dict', 'list']:
188 try:
189 json.dumps(task[attribute])
190 except Exception as e:
191 raise RuntimeError('Error parsing task attribute %s as JSON: %s' % (task[attribute], e))
192 elif isinstance(task[attribute], datetime):
193 return task[attribute].strftime(self.formatter.report)
194 else:
195 return str(task[attribute])
196 return ''
197 replacements = [
198 {
199 'match_callback': _task_attribute_match,
200 'replacement_callback': _task_attribute_replace,
201 },
202 ]
203 return replacements
204
205 def add_user_keybinding_replacements(self, replacements):
206 klass = self.loader.load_user_class('keybinding', 'keybinding', 'Keybinding')
207 if klass:
208 keybinding_custom = klass()
209 replacements.extend(keybinding_custom.replacements())
210 return replacements
211
212 def wrap_replacements_callbacks(self, replacements):
213 def build_wrapper(callback):
214 def wrapper(*args):
215 _, task = self.get_focused_task()
216 return callback(task, *args)
217 return wrapper
218 for i, replacement in enumerate(replacements):
219 replacements[i]['replacement_callback'] = build_wrapper(replacement['replacement_callback'])
220 return replacements
221
169222 def setup_keybindings(self):
170223 self.keybinding_parser.load_default_keybindings()
171224 bindings = self.config.items('keybinding')
172 def _task_uuid():
173 uuid, _ = self.get_focused_task()
174 return uuid
175 replacements = {
176 'TASK_UUID': _task_uuid,
177 }
225 replacements = self.wrap_replacements_callbacks(self.add_user_keybinding_replacements(self.default_keybinding_replacements()))
178226 keybindings = self.keybinding_parser.add_keybindings(bindings=bindings, replacements=replacements)
179227 self.key_cache = KeyCache(keybindings)
180228 self.key_cache.build_multi_key_cache()
227275
228276 def prepare_keybinding_keypresses(self, keypresses):
229277 def reducer(accum, key):
230 if isfunction(key):
231 accum += list(key())
278 if type(key) is tuple:
279 accum += list(key[0](*key[1]))
232280 else:
233281 accum.append(key)
234282 return accum
295343 elif op == 'context':
296344 # TODO: Validation if more than one arg passed.
297345 context = args[0] if len(args) > 0 else 'none'
346 if context != 'none':
347 # In case a new context was added between bootstraps.
348 self.load_contexts()
298349 if self.execute_command(['task', 'context', context], wait=self.wait):
299350 self.activate_message_bar('Context switched to: %s' % context)
300351 else:
322373 self.activate_message_bar('Task %s tags updated' % self.model.task_id(task['uuid']))
323374 elif op in ('search-forward', 'search-reverse'):
324375 self.search_set_term(data['text'])
376 self.search_set_direction(op)
325377 self.search(reverse=(op == 'search-reverse'))
326378 self.widget.focus_position = 'body'
327379 if 'uuid' in metadata:
395447 self.update_report(command)
396448 if 'uuid' in metadata:
397449 metadata.pop('uuid')
450 elif command in self.task_config.disallowed_reports:
451 self.activate_message_bar("Report '%s' is non-standard, use ':!w task %s'" % (command, command), 'error')
398452 else:
399453 # Matches 's/foo/bar/' and s%/foo/bar/, allowing for separators
400454 # to be any non-word character.
413467
414468 def search_set_term(self, text):
415469 self.search_term_active = text
470
471 def search_set_direction(self, op):
472 self.search_direction_reverse = op == 'search-reverse'
416473
417474 def search(self, reverse=False):
418475 if not self.search_term_active:
504561 self.autocomplete = AutoComplete(self.config, extra_filters={'report': self.reports.keys(), 'help': self.help.autocomplete_entries(), 'context': context_list})
505562
506563 def init_command_bar(self):
507 self.command_bar = CommandBar(autocomplete=self.autocomplete, event=self.event)
564 abort_backspace = self.config.get('vit', 'abort_backspace')
565 self.command_bar = CommandBar(autocomplete=self.autocomplete, abort_backspace=abort_backspace, event=self.event)
508566
509567 def build_frame(self):
510568 self.status_report = urwid.AttrMap(urwid.Text('Welcome to VIT'), 'status')
613671 self.activate_command_bar('search-reverse', '?', {'history': 'search'})
614672
615673 def activate_command_bar_search_next(self):
616 self.search()
674 self.search(reverse=self.search_direction_reverse)
617675
618676 def activate_command_bar_search_previous(self):
619 self.search(reverse=True)
677 self.search(reverse=not self.search_direction_reverse)
620678
621679 def activate_command_bar_task_context(self):
622680 self.activate_command_bar('context', 'Context: ')
702760 def task_action_priority(self):
703761 uuid, _ = self.get_focused_task()
704762 if uuid:
705 choices = {
706 'h': 'H',
707 'm': 'M',
708 'l': 'L',
709 'n': '',
710 }
711 self.activate_command_bar('priority', 'Priority (h/m/l/n): ', {'uuid': uuid, 'choices': choices})
763 choices = {}
764 for choice in self.task_config.priority_values:
765 key = choice.lower() or 'n'
766 choices[key] = choice
767 self.activate_command_bar('priority', 'Priority (%s): ' % '/'.join(choices), {'uuid': uuid, 'choices': choices})
712768
713769 def task_action_project(self):
714770 uuid, _ = self.get_focused_task()
736792 if uuid:
737793 self.execute_command(['task', uuid, 'info'], update_report=False)
738794 self.task_list.focus_by_task_uuid(uuid)
795
796 def get_available_task_columns(self):
797 returncode, stdout, stderr = self.command.run(['task', '_columns'], capture_output=True)
798 if returncode == 0:
799 self.available_columns = stdout.split()
800 else:
801 raise RuntimeError("Error retrieving available task columns: %s" % stderr)
739802
740803 def refresh_blocking_task_uuids(self):
741804 returncode, stdout, stderr = self.command.run(['task', 'uuids', '+BLOCKING'], capture_output=True)
846909 self.refresh_blocking_task_uuids()
847910 self.formatter.recalculate_due_datetimes()
848911 context_filters = self.contexts[self.context]['filter'] if self.context else []
849 self.model.update_report(self.report, context_filters=context_filters, extra_filters=self.extra_filters)
912 try:
913 self.model.update_report(self.report, context_filters=context_filters, extra_filters=self.extra_filters)
914 except VitException as err:
915 self.activate_message_bar(str(err), 'error')
916 return
850917 self.update_task_table()
851918 self.update_status_report()
852919 self.update_status_context()
3333 for ac_type in filters:
3434 setattr(self, ac_type, self.refresh_type(ac_type))
3535
36 def get_refresh_type_command(self, ac_type):
37 command = [
38 'task',
39 ]
40 if ac_type == 'project':
41 command.extend([
42 'rc.list.all.projects=yes',
43 '_projects',
44 ])
45 else:
46 command.extend([
47 '_%ss' % ac_type
48 ])
49 return command
50
3651 def refresh_type(self, ac_type):
37 command = 'task _%ss' % ac_type
38 returncode, stdout, stderr = self.command.run(command, capture_output=True)
52 returncode, stdout, stderr = self.command.run(self.get_refresh_type_command(ac_type), capture_output=True)
3953 if returncode == 0:
4054 items = list(filter(lambda x: True if x else False, stdout.split("\n")))
4155 if ac_type == 'project':
7589 entries.sort()
7690 return entries
7791
92 def make_space_escape_regex(self, filters, filter_config):
93 prefix_parts = []
94 for ac_type in filters:
95 items = getattr(self, ac_type)
96 type_prefixes = filter_config[ac_type]['prefixes'] if ac_type in filter_config and 'prefixes' in filter_config[ac_type] else []
97 for prefix in type_prefixes:
98 prefix_parts.append(re.escape(prefix))
99 prefix_or = "|".join(prefix_parts)
100 return re.compile("^(%s).+[ ]+.+$" % prefix_or)
101
78102 def setup(self, text_callback, filters=None, filter_config=None):
79103 if self.is_setup:
80104 self.reset()
85109 filter_config = self.default_filter_config
86110 self.refresh()
87111 self.entries = self.make_entries(filters, filter_config)
112 self.space_escape_regex = self.make_space_escape_regex(filters, filter_config)
88113 self.root_only_filters = list(filter(lambda f: True if f in filter_config and 'root_only' in filter_config[f] else False, filters))
89114 self.is_setup = True
90115
152177 def is_help_request(self):
153178 return self.prefix_parts[0] in ['help']
154179
180 def add_space_escaping(self, text):
181 if self.space_escape_regex.match(text):
182 return text.replace(' ', '\ ')
183 else:
184 return text
185
186 def remove_space_escaping(self, text):
187 return text.replace('\\ ', ' ')
188
155189 def parse_text(self, text, edit_pos):
156190 full_prefix = text[:edit_pos]
157 self.prefix_parts = util.string_to_args(full_prefix)
191 self.prefix_parts = list(map(self.add_space_escaping, util.string_to_args(full_prefix)))
158192 if not self.prefix_parts:
159193 self.search_fragment = self.prefix = full_prefix
160194 self.suffix = text[(edit_pos + 1):]
165199 self.search_fragment = self.prefix_parts.pop()
166200 self.prefix = ' '.join(self.prefix_parts)
167201 self.suffix = text[(edit_pos + 1):]
202 self.search_fragment = self.remove_space_escaping(self.search_fragment)
168203
169204 def can_tab(self, text, edit_pos):
170205 if edit_pos == 0:
177212 return text[edit_pos:next_pos] in (' ', '') and text[previous_pos:edit_pos] not in (' ', '')
178213
179214 def assemble(self, tab_option, solo_match=False):
180 if solo_match and not tab_option.endswith(":"):
181 tab_option += ' '
215 if not tab_option.endswith(":"):
216 tab_option = self.add_space_escaping(tab_option)
217 if solo_match:
218 tab_option += ' '
182219 parts = [self.prefix, tab_option, self.suffix]
183220 tabbed_text = ' '.join(filter(lambda p: True if p else False, parts))
184221 parts.pop()
00 import urwid
1 from vit.readline import Readline
12
23 class CommandBar(urwid.Edit):
34 """Custom urwid.Edit class for the command bar.
45 """
56 def __init__(self, **kwargs):
6 self.event = kwargs['event']
7 self.autocomplete = kwargs['autocomplete']
7 self.event = kwargs.pop('event')
8 self.autocomplete = kwargs.pop('autocomplete')
9 self.abort_backspace = kwargs.pop('abort_backspace')
810 self.metadata = None
911 self.history = CommandBarHistory()
10 kwargs.pop('event')
11 kwargs.pop('autocomplete')
12 self.readline = Readline(self)
1213 return super().__init__(**kwargs)
1314
1415 def keypress(self, size, key):
1516 """Overrides Edit.keypress method.
1617 """
17 # TODO: Readline edit shortcuts.
1818 if key not in ('tab', 'shift tab'):
1919 self.autocomplete.deactivate()
2020 if 'choices' in self.metadata:
21 op = self.metadata['op']
22 data = {
23 'choice': None,
24 'metadata': self.get_metadata(),
25 }
26 if key in self.metadata['choices']:
27 data['choice'] = self.metadata['choices'][key]
28 self.cleanup(op)
29 self.event.emit('command-bar:keypress', data)
21 self.quit({'choice': self.metadata['choices'].get(key)})
3022 return None
31 elif key in ('ctrl a',):
32 self.set_edit_pos(0)
23 elif key in ('up',):
24 self.readline.keypress('ctrl p')
3325 return None
34 elif key in ('ctrl e',):
35 self.set_edit_pos(len(self.get_edit_text()))
36 return None
37 elif key in ('up', 'ctrl p'):
38 text = self.history.previous(self.metadata['history'])
39 if text != False:
40 self.set_edit_text(text)
41 return None
42 elif key in ('down', 'ctrl n'):
43 text = self.history.next(self.metadata['history'])
44 if text != False:
45 self.set_edit_text(text)
26 elif key in ('down',):
27 self.readline.keypress('ctrl n')
4628 return None
4729 elif key in ('enter', 'esc'):
4830 text = self.get_edit_text().strip()
49 metadata = self.get_metadata()
50 data = {
51 'key': key,
52 'text': text,
53 'metadata': metadata,
54 }
55 self.cleanup(metadata['op'])
56 if text and key in ('enter'):
57 self.history.add(metadata['history'], text)
58 self.event.emit('command-bar:keypress', data)
31 if text and key == 'enter':
32 self.history.add(self.metadata['history'], text)
33 self.quit({'key': key, 'text': text})
5934 return None
6035 elif key in ('tab', 'shift tab'):
6136 if self.is_autocomplete_op():
6540 kwargs['reverse'] = True
6641 self.autocomplete.activate(text, self.edit_pos, **kwargs)
6742 return None
43 elif key in self.readline.keys():
44 return self.readline.keypress(key)
45 elif self.is_aborting_backspace(key):
46 self.quit({'key': key})
47 return None
6848 return super().keypress(size, key)
49
50 def is_aborting_backspace(self, key):
51 return key == 'backspace' and self.abort_backspace and not self.get_edit_text()
6952
7053 def is_autocomplete_op(self):
7154 return self.metadata['op'] not in ['search-forward', 'search-reverse']
7962
8063 def set_command_prompt(self, caption, edit_text=None):
8164 self.set_caption(caption)
82 if edit_text:
65 if edit_text is not None:
8366 self.set_edit_text(edit_text)
8467
8568 def activate(self, caption, metadata, edit_text=None):
8669 self.set_metadata(metadata)
8770 self.set_command_prompt(caption, edit_text)
8871
89 def cleanup(self, command):
90 self.set_caption('')
91 self.set_edit_text('')
92 self.history.cleanup(command)
72 def deactivate(self):
73 self.set_command_prompt('', '')
74 self.history.cleanup(self.metadata['op'])
9375 self.set_metadata(None)
76
77 def quit(self, metadata_args={}):
78 data = {'metadata': self.get_metadata(), **metadata_args}
79 self.deactivate()
80 self.event.emit('command-bar:keypress', data) # remove focus from command bar
9481
9582 def get_metadata(self):
9683 return self.metadata.copy() if self.metadata else None
4848 # Boolean. If true, VIT will enable mouse support for actions such as selecting
4949 # list items.
5050 #mouse = False
51
52 # Boolean. If true, hitting backspace against an empty prompt aborts the prompt.
53 #abort_backspace = False
5154
5255 [report]
5356
187190 # wa = {ACTION_TASK_WAIT}
188191 # The above would disable the task wait action for the 'w' key, and instead
189192 # assign it to the 'wa' keybinding.
193 # For capital letter keybindings, use the letter directly:
194 # D = {ACTION_TASK_DONE}
190195
191196 # For a list of available actions, run 'vit --list-actions'.
192197 # A great reference for many of the available meta keys, and understanding the
198203 # the currently focused task UUID:
199204 # o = :!wr onenote {TASK_UUID}<Enter>
200205
201 # The special '{TASK_UUID}' variable can be used in any macro, and it will be
202 # replaced with the currently highlighted task's UUID.
206 # The special '{TASK_[attribute]}' variable can be used in any macro, and it
207 # will be replaced with the value of the attribute for the currently
208 # highlighted task. Any attribute listed in 'task _columns' is supported, e.g.
209 # o = :!wr echo project is {TASK_PROJECT}<Enter>
203210
204211 # Multiple keybindings can be associated with the same action/macro, simply
205212 # separate the keybindings with a comma:
206 # <Ctrl>z,zz = {ACTION_TASK_UNDO}
213 # <Ctrl> z,zz = {ACTION_TASK_UNDO}
214
215 # 'Special' keys are indicated by enclosing them in brackets. VIT supports the
216 # following special keys on either side of the keybinding declaration, by
217 # internally translating them into the single character:
218 #
219 # <Colon>
220 # <Equals>
221 # <Space>
222 # <Semicolon>
223 #
224 # Under the hood, VIT uses the Urwid mappings for keyboard input:
225 # http://urwid.org/manual/userinput.html
226 #
227 # Any modifier, navigation, or function keys can be described in the VIT
228 # keybinding configuration by wrapping them in angle brackets, matching the
229 # correct Urwid keyboard input structure:
230 #
231 # <Ctrl> e = :!wr echo do something
232 # <Shift> <Ctrl> <F5> = :!wr echo you used a function key
233
2020 FILTER_EXCLUSION_REGEX = re.compile('^limit:')
2121 FILTER_PARENS_REGEX = re.compile('([\(\)])')
2222 CONFIG_BOOLEAN_TRUE_REGEX = re.compile('1|yes|true', re.IGNORECASE)
23 # TaskParser expects clean hierachies in the Taskwarrior dotted config names.
24 # However, this is occassionally violated, with a leaf ending in both a string
23 # TaskParser expects clean hierarchies in the Taskwarrior dotted config names.
24 # However, this is occasionally violated, with a leaf ending in both a string
2525 # value and another branch. The below list contains the config values that
2626 # violate this convention, and transform them into a single additional branch
2727 # of value CONFIG_STRING_LEAVES_DEFAULT_BRANCH
2929 'color.calendar.due',
3030 'color.due',
3131 'color.label',
32 'dateformat',
3233 ]
3334 CONFIG_STRING_LEAVES_DEFAULT_BRANCH = 'default'
3435
4243 'confirmation': True,
4344 'wait': True,
4445 'mouse': False,
46 'abort_backspace': False,
4547 },
4648 'report': {
4749 'default_report': 'next',
99101 self.user_config_filepath = '%s/%s' % (self.user_config_dir, VIT_CONFIG_FILE)
100102 if not self.config_file_exists(self.user_config_filepath):
101103 self.optional_create_config_file(self.user_config_filepath)
104 self.taskrc_path = self.get_taskrc_path()
105 self.validate_taskrc()
102106 self.config.read(self.user_config_filepath)
103107 self.defaults = DEFAULTS
104108 self.set_config_data()
105109
106110 def set_config_data(self):
107 self.taskrc_path = self.get_taskrc_path()
108111 self.subproject_indentable = self.is_subproject_indentable()
109112 self.row_striping_enabled = self.is_row_striping_enabled()
110113 self.confirmation_enabled = self.is_confirmation_enabled()
111114 self.wait_enabled = self.is_wait_enabled()
112115 self.mouse_enabled = self.is_mouse_enabled()
116
117 def validate_taskrc(self):
118 try:
119 open(self.taskrc_path, 'r').close()
120 except FileNotFoundError:
121 message = """
122 %s not found.
123
124 VIT requires a properly configured TaskWarrior instance in the current
125 environment. Execute the 'task' binary with no arguments to initialize a new
126 configuration.
127 """ % (self.taskrc_path)
128 print(message)
129 exit(1)
113130
114131 def config_file_exists(self, filepath):
115132 try:
192209 self.projects = []
193210 self.contexts = {}
194211 self.reports = {}
212 self.disallowed_reports = [
213 'timesheet',
214 ]
195215 self.command = Command(self.config)
196216 self.get_task_config()
197217 self.get_projects()
198218 self.set_config_data()
199219
200220 def set_config_data(self):
201 self.print_empty_columns = self.subtree('print.empty.columns') == 'yes'
221 self.print_empty_columns = self.is_truthy(self.subtree('print.empty.columns'))
222 self.priority_values = self.get_priority_values()
202223
203224 def get_task_config(self):
225 self.task_config = []
204226 returncode, stdout, stderr = self.command.run('task _show', capture_output=True)
205227 if returncode == 0:
206228 lines = list(filter(lambda x: True if x else False, stdout.split("\n")))
207229 for line in lines:
208 hierarchy, values = line.split('=')
230 hierarchy, values = line.split('=', maxsplit=1)
209231 self.task_config.append((hierarchy, values))
210232 else:
211233 raise RuntimeError('Error parsing task config: %s' % stderr)
225247 self.projects.pop()
226248 else:
227249 raise RuntimeError('Error parsing task projects: %s' % stderr)
250
251 def get_priority_values(self):
252 return self.subtree('uda.priority.values').split(',')
228253
229254 def transform_string_leaves(self, hierarchy):
230255 if hierarchy in CONFIG_STRING_LEAVES:
295320
296321 def get_contexts(self):
297322 contexts = {}
323 self.get_task_config()
298324 subtree = self.subtree('context.')
299325 for context, filters in list(subtree.items()):
300326 filters = shlex.split(re.sub(FILTER_PARENS_REGEX, r' \1 ', filters))
308334 reports = {}
309335 subtree = self.subtree('report.')
310336 for report, attrs in list(subtree.items()):
337 if report in self.disallowed_reports:
338 continue
311339 reports[report] = {
312340 'name': report,
313341 'subproject_indentable': False,
323351 reports[report]['filter'] = [f for f in filters if not FILTER_EXCLUSION_REGEX.match(f)]
324352 if 'labels' in attrs:
325353 reports[report]['labels'] = attrs['labels'].split(',')
354 else:
355 reports[report]['labels'] = [ column.title() for column in attrs['columns'].split(',') ]
326356 if 'sort' in attrs:
327357 columns = attrs['sort'].split(',')
328358 reports[report]['sort'] = [self.parse_sort_column(c) for c in columns]
339369 report['subproject_indentable'] = self.has_project_column(report_name) and self.has_primary_project_ascending_sort(report)
340370 return report
341371
372 def is_truthy(self, value):
373 value = str(value)
374 return value.lower() in ['y', 'yes', 'on', 'true', '1']
375
342376 def has_project_column(self, report_name):
343377 return self.get_column_index(report_name, 'project') is not None
344378
345379 def has_primary_project_ascending_sort(self, report):
346 primary_sort = report['sort'][0]
380 try:
381 primary_sort = report['sort'][0]
382 except KeyError:
383 return False
384
347385 return primary_sort[0] == 'project' and primary_sort[1] == 'ascending'
348386
349387 def get_column_index(self, report_name, column):
0 class VitException(Exception):
1 """Base class for exceptions in this module."""
2 pass
3939 }
4040
4141 class Formatter(object):
42 def __init__(self, column, report, formatter_base, **kwargs):
42 def __init__(self, column, report, formatter_base, blocking_task_uuids, **kwargs):
4343 self.column = column
4444 self.report = report
4545 self.formatter = formatter_base
4646 self.colorizer = self.formatter.task_colorizer
47 self.blocking_task_uuids = blocking_task_uuids
4748
4849 def format(self, obj, task):
4950 if not obj:
6667 def colorize(self, obj):
6768 return None
6869
70 def filter_by_blocking_task_uuids(self, depends):
71 return [ task for task in depends if task['uuid'] in self.blocking_task_uuids ]
72
6973 class Marker(Formatter):
7074 def __init__(self, report, defaults, report_marker_columns, blocking_task_uuids):
71 super().__init__(None, report, defaults)
75 super().__init__(None, report, defaults, blocking_task_uuids)
7276 self.columns = report_marker_columns
73 self.blocking_tasks = blocking_task_uuids
7477 self.labels = self.formatter.markers.labels
7578 self.udas = self.formatter.markers.udas
7679 self.require_color = self.formatter.markers.require_color
99102 return (self.colorize(obj), formatted_duration)
100103
101104 class DateTime(Formatter):
102 def __init__(self, column, report, defaults, **kwargs):
105 def __init__(self, column, report, defaults, blocking_task_uuids, **kwargs):
103106 self.custom_formatter = None if not 'custom_formatter' in kwargs else kwargs['custom_formatter']
104 super().__init__(column, report, defaults)
107 super().__init__(column, report, defaults, blocking_task_uuids)
105108
106109 def format(self, dt, task):
107110 if not dt:
22
33 class Depends(List):
44 def format_list(self, depends, task):
5 return ','.join(list(map(lambda t: str(util.task_id_or_uuid_short(t)), depends))) if depends else ''
5 return ','.join(list(map(lambda t: str(util.task_id_or_uuid_short(t)), self.filter_by_blocking_task_uuids(depends)))) if depends else ''
66
77 def colorize(self, depends):
88 return self.colorizer.blocked(depends)
11
22 class DependsCount(Depends):
33 def format_list(self, depends, task):
4 return '[%d]' % len(depends) if depends else ''
4 return '[%d]' % len(self.filter_by_blocking_task_uuids(depends)) if depends else ''
11
22 class DependsIndicator(Depends):
33 def format_list(self, depends, task):
4 return self.formatter.indicator_dependency if depends else ''
4 return self.formatter.indicator_dependency if self.filter_by_blocking_task_uuids(depends) else ''
1111 width, text_markup = self.format_due(width, text_markup, task['due'], task)
1212 if self.mark_status:
1313 width, text_markup = self.format_status(width, text_markup, task['status'])
14 if self.mark_depends and task['depends']:
14 if self.mark_depends and self.filter_by_blocking_task_uuids(task['depends']):
1515 width, text_markup = self.format_blocked(width, text_markup, task['depends'])
1616 if self.mark_start and task['start']:
1717 width, text_markup = self.format_active(width, text_markup, task['start'], task)
2424 for uda_name, uda_type in self.udas.items():
2525 if getattr(self, 'mark_%s' % uda_name):
2626 width, text_markup = self.format_uda(width, text_markup, uda_name, uda_type, task[uda_name])
27 if task['uuid'] in self.blocking_tasks:
27 if task['uuid'] in self.blocking_task_uuids:
2828 width, text_markup = self.format_blocking(width, text_markup)
2929 return (width, '' if width == 0 else text_markup)
3030
+0
-4
vit/formatter/priority.py less more
0 from vit.formatter.uda_string import UdaString
1
2 class Priority(UdaString):
3 pass
+0
-4
vit/formatter/priority_default.py less more
0 from vit.formatter.priority import Priority
1
2 class PriorityDefault(Priority):
3 pass
00 from vit.formatter import String
11
22 class Project(String):
3 def __init__(self, column, report, defaults, **kwargs):
4 super().__init__(column, report, defaults)
3 def __init__(self, column, report, defaults, blocking_task_uuids, **kwargs):
4 super().__init__(column, report, defaults, blocking_task_uuids)
55 self.indent_subprojects = self.is_subproject_indentable()
66
77 def format(self, project, task):
99 if not dt:
1010 return self.markup_none(self.colorize())
1111 # TODO: Remove this once tasklib bug is fixed.
12 # https://github.com/robgolding/tasklib/issues/30
1213 dt = dt if isinstance(dt, datetime.datetime) else serializer.timestamp_deserializer(dt)
1314 formatted_date = dt.strftime(self.custom_formatter or self.formatter.report)
1415 return (len(formatted_date), (self.colorize(dt), formatted_date))
2222 self.task_config = task_config
2323 self.markers = markers
2424 self.task_colorizer = task_colorizer
25 self.report = self.task_config.translate_date_markers(self.task_config.subtree('dateformat.report'))
26 self.annotation = self.task_config.translate_date_markers(self.task_config.subtree('dateformat.annotation'))
25 self.date_default = self.task_config.translate_date_markers(self.task_config.subtree('dateformat')["default"])
26 self.report = self.task_config.translate_date_markers(self.task_config.subtree('dateformat.report')) or self.date_default
27 self.annotation = self.task_config.translate_date_markers(self.task_config.subtree('dateformat.annotation')) or self.date_default
2728 self.description_truncate_len = DEFAULT_DESCRIPTION_TRUNCATE_LEN
2829 self.zone = get_localzone()
2930 self.epoch_datetime = datetime(1970, 1, 1, tzinfo=timezone('UTC'))
2626 return list(filter(None, sorted_keybindings))
2727
2828 def get_non_modified_keybindings(self):
29 return [k for k in self.keybindings if not self.keybindings[k]['has_modifier']]
29 return [k for k in self.keybindings if self.keybindings[k]['has_special_keys'] or not self.keybindings[k]['has_modifier']]
3030
3131 def add_keybinding_to_key_cache(self, to_cache, keybinding, existing_keybindings, key_cache):
3232 if to_cache in existing_keybindings:
3838 # The parser doesn't recognize a space on the left side of an assignment,
3939 # the special <Space> marker is used instead.
4040 <Down>,j,<Space> = {ACTION_LIST_DOWN}
41 <Page Up>,<Ctrl>b = {ACTION_LIST_PAGE_UP}
42 <Page Down>,<Ctrl>f = {ACTION_LIST_PAGE_DOWN}
41 <Page Up>,<Ctrl> b = {ACTION_LIST_PAGE_UP}
42 <Page Down>,<Ctrl> f = {ACTION_LIST_PAGE_DOWN}
4343 gg,0 = {ACTION_LIST_HOME}
4444 G = {ACTION_LIST_END}
4545 H = {ACTION_LIST_SCREEN_TOP}
4949
5050 [report]
5151 f = {ACTION_REPORT_FILTER}
52 <Ctrl>l = {ACTION_REFRESH}
52 <Ctrl> l = {ACTION_REFRESH}
1212 BASE_DIR = os.path.dirname(os.path.realpath(__file__))
1313 BRACKETS_REGEX = re.compile("[<>]")
1414 DEFAULT_KEYBINDINGS_SECTIONS = ('global', 'navigation', 'command', 'report')
15 CONFIG_NAME_SPECIAL_KEY_SUBSTITUTIONS = {
15 CONFIG_SPECIAL_KEY_SUBSTITUTIONS = {
1616 'colon': ':',
1717 'equals': '=',
1818 'space': ' ',
19 'semicolon': ';',
1920 }
2021
2122 class KeybindingError(Exception):
5960 bindings = self.items(section)
6061 self.add_keybindings(bindings)
6162
62 def keybinding_special_keys_substitutions(self, name):
63 if name in CONFIG_NAME_SPECIAL_KEY_SUBSTITUTIONS:
64 name = CONFIG_NAME_SPECIAL_KEY_SUBSTITUTIONS[name]
65 return name
63 def keybinding_special_keys_substitutions(self, value):
64 is_special_key = value in CONFIG_SPECIAL_KEY_SUBSTITUTIONS
65 if is_special_key:
66 value = CONFIG_SPECIAL_KEY_SUBSTITUTIONS[value]
67 return value, is_special_key
6668
6769 def parse_keybinding_keys(self, keys):
6870 has_modifier = bool(re.match(BRACKETS_REGEX, keys))
69 parsed_keys = ' '.join(re.sub(BRACKETS_REGEX, ' ', keys).strip().split()).lower() if has_modifier else keys
70 return self.keybinding_special_keys_substitutions(parsed_keys), has_modifier
71 def reducer(accum, char):
72 if char == '<':
73 accum['in_brackets'] = True
74 accum['bracket_string'] = ''
75 elif char == '>':
76 accum['in_brackets'] = False
77 value, is_special_key = self.keybinding_special_keys_substitutions(accum['bracket_string'].lower())
78 accum['keys'] += value
79 accum['has_special_keys'] = is_special_key
80 else:
81 if accum['in_brackets']:
82 accum['bracket_string'] += char
83 else:
84 accum['keys'] += char
85 return accum
86 accum = reduce(reducer, keys, {
87 'keys': '',
88 'in_brackets': False,
89 'bracket_string': '',
90 'has_special_keys': False,
91 })
92 return accum['keys'], has_modifier, accum['has_special_keys']
7193
7294 def parse_keybinding_value(self, value, replacements={}):
7395 def reducer(accum, char):
7698 accum['bracket_string'] = ''
7799 elif char == '>':
78100 accum['in_brackets'] = False
79 accum['keybinding'].append(accum['bracket_string'].lower())
101 value, is_special_key = self.keybinding_special_keys_substitutions(accum['bracket_string'].lower())
102 accum['keybinding'].append(value)
80103 elif char == '{':
81104 accum['in_variable'] = True
82105 accum['variable_string'] = ''
84107 accum['in_variable'] = False
85108 if accum['variable_string'] in self.actions:
86109 accum['action_name'] = accum['variable_string']
87 elif accum['variable_string'] in replacements:
88 accum['keybinding'].append(replacements[accum['variable_string']])
89110 else:
111 for replacement in replacements:
112 args = replacement['match_callback'](accum['variable_string'])
113 if isinstance(args, list):
114 accum['keybinding'].append((replacement['replacement_callback'], args))
115 return accum
90116 raise ValueError("unknown config variable '%s'" % accum['variable_string'])
91117 else:
92118 if accum['in_brackets']:
124150 bound_keys, action_name = self.parse_keybinding_value(value, replacements)
125151 self.validate_parsed_value(key_groups, bound_keys, action_name)
126152 for keys in key_groups.strip().split(','):
127 parsed_keys, has_modifier = self.parse_keybinding_keys(keys)
153 parsed_keys, has_modifier, has_special_keys = self.parse_keybinding_keys(keys)
128154 self.keybindings[parsed_keys] = {
129155 'label': keys,
130156 'has_modifier': has_modifier,
157 'has_special_keys': has_special_keys,
131158 }
132159 if action_name:
133160 self.keybindings[parsed_keys]['action_name'] = action_name
1616 filepath = '%s/%s/%s.py' % (self.user_config_dir, module_type, module_name)
1717 try:
1818 mod = self.import_from_path(module, filepath)
19 except SyntaxError as e:
20 raise SyntaxError("User class: %s (%s) -- %s" % (class_name, filepath, e))
1921 except:
2022 return None
2123 return getattr(mod, class_name)
2224
2325 def import_from_path(self, module, filepath):
24 try:
25 spec = importlib.util.spec_from_file_location(module, filepath)
26 mod = importlib.util.module_from_spec(spec)
27 spec.loader.exec_module(mod)
28 return mod
29 except:
30 mod = imp.load_source(module, filepath)
31 return mod
26 spec = importlib.util.spec_from_file_location(module, filepath)
27 mod = importlib.util.module_from_spec(spec)
28 spec.loader.exec_module(mod)
29 return mod
00 import sys
1 import optparse
1 import argparse
22
33 from vit import version
44
5 class OptionParser(optparse.OptionParser):
6 def format_epilog(self, formatter):
7 return self.epilog
8
9 parser = OptionParser(
10 usage='%prog [options] [report] [filters]',
11 version=version.VIT,
12 epilog="""
13 VIT (Visual Interactive Taskwarrior) is a lightweight, curses-based front end for
5 parser = argparse.ArgumentParser(
6 description="VIT (Visual Interactive Taskwarrior)",
7 usage='%(prog)s [options] [report] [filters]',
8 formatter_class=argparse.RawTextHelpFormatter,
9 allow_abbrev=False,
10 epilog="""
11 VIT (Visual Interactive Taskwarrior) is a lightweight, curses-based front end for
1412 Taskwarrior that provides a convenient way to quickly navigate and process tasks.
1513 VIT allows you to interact with tasks in a Vi-intuitive way.
1614
2018
2119 While VIT is running, type :help followed by enter to review basic command/navigation actions.
2220
23 See https://github.com/scottkosty/vit for more information.
21 See https://github.com/vit-project/vit for more information.
2422
2523 """
2624 )
2725
28 parser.add_option('--list-actions',
29 dest="list_actions",
30 default=False,
31 action="store_true",
32 help="list all available actions",
26 parser.add_argument('-v', '--version',
27 action='version',
28 version=version.VIT,
29 )
30 parser.add_argument('--list-actions',
31 dest="list_actions",
32 default=False,
33 action="store_true",
34 help="list all available actions",
3335 )
3436
3537 def parse_options():
36 options, filters = parser.parse_args()
38 options, filters = parser.parse_known_args()
3739 if options.list_actions:
3840 list_actions()
3941 sys.exit(0)
0 import string
1 import re
2
3 class Readline(object):
4 def __init__(self, edit_obj):
5 self.edit_obj = edit_obj
6 word_chars = string.ascii_letters + string.digits + "_"
7 self._word_regex1 = re.compile(
8 "([%s]+)" % "|".join(re.escape(ch) for ch in word_chars)
9 )
10 self._word_regex2 = re.compile(
11 "([^%s]+)" % "|".join(re.escape(ch) for ch in word_chars)
12 )
13
14 def keys(self):
15 return ('ctrl p', 'ctrl n', 'ctrl a', 'ctrl e', 'ctrl b', 'ctrl f',
16 'ctrl h', 'ctrl d', 'ctrl t', 'ctrl u', 'ctrl k', 'meta b',
17 'meta f', 'ctrl w', 'meta d')
18
19 def keypress(self, key):
20 # Move to the previous line.
21 if key in ('ctrl p',):
22 text = self.edit_obj.history.previous(self.edit_obj.metadata['history'])
23 if text != False:
24 self.edit_obj.set_edit_text(text)
25 return None
26 # Move to the next line.
27 elif key in ('ctrl n',):
28 text = self.edit_obj.history.next(self.edit_obj.metadata['history'])
29 if text != False:
30 self.edit_obj.set_edit_text(text)
31 return None
32 # Jump to the beginning of the line.
33 elif key in ('ctrl a',):
34 self.edit_obj.set_edit_pos(0)
35 return None
36 # Jump to the end of the line.
37 elif key in ('ctrl e',):
38 self.edit_obj.set_edit_pos(len(self.edit_obj.get_edit_text()))
39 return None
40 # Jump backward one character.
41 elif key in ('ctrl b',):
42 self.edit_obj.set_edit_pos(self.edit_obj.edit_pos - 1)
43 return None
44 # Jump forward one character.
45 elif key in ('ctrl f',):
46 self.edit_obj.set_edit_pos(self.edit_obj.edit_pos + 1)
47 return None
48 # Delete previous character.
49 elif key in ('ctrl h',):
50 if self.edit_obj.edit_pos > 0:
51 self.edit_obj.set_edit_pos(self.edit_obj.edit_pos - 1)
52 self.edit_obj.set_edit_text(
53 self.edit_obj.get_edit_text()[0 : self.edit_obj.edit_pos]
54 + self.edit_obj.get_edit_text()[self.edit_obj.edit_pos + 1 :])
55 return None
56 # Delete next character.
57 elif key in ('ctrl d',):
58 if self.edit_obj.edit_pos < len(self.edit_obj.get_edit_text()):
59 edit_pos = self.edit_obj.edit_pos
60 self.edit_obj.set_edit_text(
61 self.edit_obj.get_edit_text()[0 : self.edit_obj.edit_pos] +
62 self.edit_obj.get_edit_text()[self.edit_obj.edit_pos + 1 :])
63 self.edit_obj.set_edit_pos(edit_pos)
64 return None
65 # Transpose characters.
66 elif key in ('ctrl t',):
67 # Can't transpose if there are less than 2 chars
68 if len(self.edit_obj.get_edit_text()) < 2:
69 return None
70 self.edit_obj.set_edit_pos(max(2, self.edit_obj.edit_pos + 1))
71 new_edit_pos = self.edit_obj.edit_pos
72
73 edit_text = self.edit_obj.get_edit_text()
74 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 :])
79 self.edit_obj.set_edit_pos(new_edit_pos)
80 return None
81 # Delete backwards to the beginning of the line.
82 elif key in ('ctrl u',):
83 self.edit_obj.set_edit_text(self.edit_obj.get_edit_text()[self.edit_obj.edit_pos :])
84 self.edit_obj.set_edit_pos(0)
85 return None
86 # Delete forwards to the end of the line.
87 elif key in ('ctrl k',):
88 self.edit_obj.set_edit_text(self.edit_obj.get_edit_text()[: self.edit_obj.edit_pos])
89 return None
90 # Jump backward one word.
91 elif key in ('meta b',):
92 self.jump_backward_word()
93 return None
94 # Jump forward one word.
95 elif key in ('meta f',):
96 self.jump_forward_word()
97 return None
98 # Delete backwards to the beginning of the current word.
99 elif key in ('ctrl w',):
100 old_edit_pos = self.edit_obj.edit_pos
101 self.jump_backward_word()
102 new_edit_pos = self.edit_obj.edit_pos
103 self.edit_obj.set_edit_text(self.edit_obj.edit_text[: new_edit_pos]
104 + self.edit_obj.edit_text[old_edit_pos :])
105 self.edit_obj.set_edit_pos(new_edit_pos)
106 return None
107 # Delete forwards to the end of the current word.
108 elif key in ('meta d',):
109 edit_pos = self.edit_obj.edit_pos
110 self.jump_forward_word()
111 self.edit_obj.set_edit_text(self.edit_obj.edit_text[: edit_pos]
112 + self.edit_obj.edit_text[self.edit_obj.edit_pos :])
113 self.edit_obj.set_edit_pos(edit_pos)
114 return None
115
116 def jump_backward_word(self):
117 for match in self._word_regex1.finditer(
118 self.edit_obj.edit_text[: self.edit_obj.edit_pos][::-1]
119 ):
120 self.edit_obj.set_edit_pos(self.edit_obj.edit_pos - match.end(1))
121 return
122 self.edit_obj.set_edit_pos(0)
123
124 def jump_forward_word(self):
125 for match in self._word_regex2.finditer(
126 self.edit_obj.edit_text[self.edit_obj.edit_pos :]
127 ):
128 if match.start(1) > 0:
129 self.edit_obj.set_edit_pos(self.edit_obj.edit_pos + match.start(1))
130 return
131 self.edit_obj.set_edit_pos(len(self.edit_obj.edit_text))
132
22
33 import tasklib
44 from tasklib.task import Task
5 from tasklib.backends import TaskWarriorException
56
67 from vit import util
8 from vit.exception import VitException
79
810 class TaskListModel(object):
911 def __init__(self, task_config, reports, report=None, data_location=None):
2426 def active_report(self):
2527 return self.reports[self.report]
2628
29 def parse_error(self, err):
30 messages = filter(lambda l : l.startswith('Error:'), str(err).splitlines())
31 return "\n".join(messages)
32
2733 def update_report(self, report, context_filters=[], extra_filters=[]):
2834 self.report = report
2935 active_report = self.active_report()
3036 report_filters = active_report['filter'] if 'filter' in active_report else []
3137 filters = self.build_task_filters(context_filters, report_filters, extra_filters)
32 self.tasks = self.tw.tasks.filter(filters) if filters else self.tw.tasks.all()
38 try:
39 self.tasks = self.tw.tasks.filter(filters) if filters else self.tw.tasks.all()
40 # NOTE: tasklib uses lazy loading and some operation is necessary
41 # for self.tasks to actually be populated here.
42 # See https://github.com/robgolding/tasklib/issues/81
43 len(self.tasks)
44 except TaskWarriorException as err:
45 raise VitException(self.parse_error(err))
3346
3447 def build_task_filters(self, *all_filters):
3548 def reducer(accum, filters):
3649 if filters:
37 accum.append('(%s)' % ' '.join(filters))
50 accum.append('( %s )' % ' '.join(filters))
3851 return accum
3952 filter_parts = reduce(reducer, all_filters, [])
4053 return ' '.join(filter_parts) if filter_parts else ''
8888 self.indent_subprojects = self.subproject_indentable()
8989 self.project_cache = {}
9090 # TODO: This is for the project placeholders, feels sloppy.
91 self.project_formatter = ProjectFormatter('project', self.report, self.formatter)
91 self.project_formatter = ProjectFormatter('project', self.report, self.formatter, self.get_blocking_task_uuids())
9292 self.build_rows()
9393 self.clean_columns()
94 self.has_project_column = self.project_column_present()
94 self.project_column_idx = self.get_project_column_idx()
95 self.reconcile_column_width_for_label()
9596 self.resize_columns()
96 self.reconcile_column_width_for_label()
9797 self.build_table()
9898 self.listbox.set_focus_position()
9999 self.update_focus()
100100
101101 def update_header(self, size):
102 if self.has_project_column:
102 if self.project_column_idx is not None:
103103 self.update_project_column_header(size)
104104
105 def project_column_present(self):
106 for _, column in enumerate(self.columns):
105 def get_project_column_idx(self):
106 for idx, column in enumerate(self.columns):
107107 if column['name'] == 'project':
108 return True
109 return False
108 return idx
109 return None
110110
111111 def get_project_from_row(self, row):
112112 return row.task['project'] if isinstance(row, SelectableRow) else row.project
123123 self.set_project_column_header()
124124
125125 def set_project_column_header(self, parents=None):
126 column_index = self.task_config.get_column_index(self.report['name'], 'project')
127 if self.has_marker_column():
128 column_index += 1
126 column_index = self.project_column_idx
129127 (columns_widget, _) = self.header.original_widget.contents[column_index]
130128 (widget, _) = columns_widget.contents[0]
131129 label = self.project_label_for_parents(parents)
220218 kwargs = self.column_formatter_kwargs()
221219 for idx, column_formatter in enumerate(self.report['columns']):
222220 name, formatter_class = self.formatter.get(column_formatter)
223 self.add_column(name, self.report['labels'][idx], formatter_class(name, self.report, self.formatter, **kwargs))
221 self.add_column(name, self.report['labels'][idx], formatter_class(name, self.report, self.formatter, self.get_blocking_task_uuids(), **kwargs))
224222
225223 def is_marker_column(self, column):
226224 return column == MARKER_COLUMN_NAME
314312 to_adjust.append({'idx': idx, 'width': width})
315313 if total_width > cols:
316314 self.adjust_oversized_columns(total_width - cols, to_adjust)
315 if to_adjust:
316 # This is called recursively to account for cases when further
317 # reduction is necessary because one or more column's reductions
318 # were limited to REDUCE_COLUMN_WIDTH_LIMIT.
319 self.resize_columns()
317320
318321 def adjust_oversized_columns(self, reduce_by, to_adjust):
319322 to_adjust = list(map(lambda c: c.update({'ratio': (c['width'] - REDUCE_COLUMN_WIDTH_LIMIT) / c['width']}) or c, to_adjust))
320323 ratio_total = reduce(lambda acc, c: acc + c['ratio'], to_adjust, 0)
321324 to_adjust = list(map(lambda c: c.update({'percentage': c['ratio'] / ratio_total}) or c, to_adjust))
322325 for c in to_adjust:
323 # This shouldn't technically be necessary, but there are edge cases
324 # when a single adjusted column is 1 column too wide for the
325 # display, which results in it not being displayed.
326 adjusted_width = c['width'] - (reduce_by + 1 if c['percentage'] == 1 else math.ceil(reduce_by * c['percentage']))
326 adjusted_width = c['width'] - math.ceil(reduce_by * c['percentage'])
327327 self.columns[c['idx']]['width'] = adjusted_width if adjusted_width > REDUCE_COLUMN_WIDTH_LIMIT else REDUCE_COLUMN_WIDTH_LIMIT
328328
329329 def reconcile_column_width_for_label(self):
364364 self.header = urwid.AttrMap(list_header, 'list-header')
365365
366366 def make_header_column(self, column, is_last, space_between=COLUMN_PADDING):
367 total_width = column['width'] + space_between
367 padding_width = 0 if is_last else space_between
368 total_width = column['width'] + padding_width
368369 column_content = urwid.AttrMap(urwid.Padding(urwid.Text(column['label'], align='left')), 'list-header-column')
369370 padding_content = self.make_padding(is_last and 'list-header-column' or 'list-header-column-separator')
370 columns = urwid.Columns([(column['width'], column_content), (space_between, padding_content)])
371 columns = urwid.Columns([(column['width'], column_content), (padding_width, padding_content)])
371372 return (total_width, columns)
372373
373374 def make_padding(self, display_attr):
440441 if self.on_select:
441442 key = self.on_select(self, size, key)
442443 return key
443
444 # ... and update the displayed items.
445 for t, (w, _) in zip(contents, self._columns.contents):
446 w.set_text(t)
447444
448445 class ProjectPlaceholderRow(urwid.WidgetWrap):
449446 """Wraps 'urwid.Columns' for a project placeholder row.
0 VIT = '2.0.0'
0 VIT = '2.1.0'