New upstream version 2.3.0
Jochen Sprickerhof
10 months ago
0 | ##### Thu Apr 13 2023 - released v2.3.0 | |
1 | ||
2 | * **Wed Mar 01 2023:** List vit.config and vit.keybinding in packages | |
3 | * **Thu Mar 02 2023:** Remove some unnecessary parenthesis from class definitions | |
4 | * **Wed Mar 01 2023:** Remove inherits from object | |
5 | * **Wed Mar 01 2023:** Use r-prefix when necessary | |
6 | * **Sat Oct 22 2022:** Fixing IndexErrors in functions | |
7 | * **Wed Oct 19 2022:** Allow flash configuration | |
8 | * **Sat Jul 09 2022:** correctly calculate text width of full-width characters | |
9 | * **Sun May 08 2022:** Fix required minimum Python version | |
10 | * **Sun May 01 2022:** add note that windows doesn't support SIGUSR1 signal | |
11 | * **Sun May 01 2022:** test for signal before adding handler | |
12 | * **Fri Apr 29 2022:** add documentation for auto-refresh configuration | |
13 | * **Thu Apr 28 2022:** fix bad variable reference | |
14 | * **Thu Apr 28 2022:** place IS_VIT_INSTANCE into environ earlier | |
15 | * **Thu Apr 28 2022:** example hook for intelligent VIT refresh | |
16 | * **Thu Apr 28 2022:** inject environment variable | |
17 | * **Wed Apr 27 2022:** skip pid teardown if pid_dir not set | |
18 | * **Wed Apr 27 2022:** sample script to externally refresh VIT instances | |
19 | * **Wed Apr 27 2022:** add Bash function example for vit wrapper | |
20 | * **Wed Apr 27 2022:** skip raising error on not deleting pid file for now | |
21 | * **Wed Apr 27 2022:** add pid_dir option to [vit] section See sample config.ini for details on usage | |
22 | * **Wed Apr 27 2022:** add basic signal support SIGUSR1: refresh (equivalent to hitting refresh key in VIT) SIGTERM/SIGINT/SIGQUIT: quit VIT cleanly | |
23 | * **Tue Apr 26 2022:** more user-friendly error message for unsupported color definitions | |
24 | * **Sun Apr 17 2022:** add simple release checklist | |
25 | ||
0 | 26 | ##### Sun Apr 17 2022 - released v2.2.0 |
1 | 27 | |
2 | 28 | * **Sun Apr 17 2022:** bump dependency versions |
93 | 93 | |
94 | 94 | ```python |
95 | 95 | # keybinding/keybinding.py |
96 | class Keybinding(object): | |
96 | class Keybinding: | |
97 | 97 | def replacements(self): |
98 | 98 | def _custom_match(variable): |
99 | 99 | if variable == 'TEST': |
139 | 139 | Now, for example, to jump to a task whose ID is 42, you need to press `4`, `2` |
140 | 140 | and `<Enter>`, instead of `:`, `4`, `2` and `<Enter>`. |
141 | 141 | This saves a `:` keypress whenever jumping to a task. |
142 | ||
143 | ### Auto-refreshing VIT's interface | |
144 | ||
145 | *Note: Windows unfortunately does not support the `SIGUSR1` signal, so this feature is not currently available in that environment.* | |
146 | ||
147 | VIT was designed to be used in a request/response manner with the underlying TaskWarrior database, and by default its interface does not refresh when there are other changes happening outside of a specific VIT instance. | |
148 | ||
149 | However, VIT provides some basic mechanisms that, when combined, allow for an easy implementation of an auto-refreshing interface: | |
150 | ||
151 | * **Signal handling:** Sending the `SIGUSR1` signal to a VIT process will cause it to refresh its interface (the equivalent of the `{ACTION_REFRESH}` action keybinding | |
152 | * **PID management:** Configuring `pid_dir` in `config.ini` will cause VIT to manage PID files in `pid_dir`. Executing VIT with the `--list-pids` argument will output all current PIDs in `pid_dir` to standard output | |
153 | * **Instance environment variable:** VIT injects the `IS_VIT_INSTANCE` environment variable into the environment of the running process. As such, processes invoked in that environment have access to the variable | |
154 | ||
155 | #### Refresh all VIT instances example | |
156 | ||
157 | [vit-external-refresh.sh](scripts/vit-external-refresh.sh) provides an example leveraging signals to externally refresh all local VIT interfaces. To use, make sure: | |
158 | ||
159 | * The script is executable, and in your `PATH` environment variable | |
160 | * You've properly set `pid_dir` in `config.ini` | |
161 | ||
162 | #### Refresh VIT when TaskWarrior updates a task | |
163 | ||
164 | [on-exit-refresh-vit.sh](scripts/hooks/on-exit-refresh-vit.sh) provides an example TaskWarrior hook that will automatically refresh all local VIT interfaces when using Taskwarrior directly. To use, make sure: | |
165 | ||
166 | * The script is executable, [named properly](https://taskwarrior.org/docs/hooks.html), and placed in TaskWarrior's hooks directory, usually `.task/hooks` | |
167 | * [vit-external-refresh.sh](scripts/vit-external-refresh.sh) is configured correctly as above | |
168 | ||
169 | #### Other refresh scenarios | |
170 | ||
171 | The basic tools above can be used in other more complex scenarios, such as refreshing VIT's interface after an automated script updates one or more tasks. The implementation details will vary with the use case, and are left as an exercise for the user. |
5 | 5 | 4. ```vit/command_line.py``` is the entry point for the application. To run it without a full installation: |
6 | 6 | * Set the ```PYTHONPATH``` environment variable to the root directory of the repository |
7 | 7 | * Run it with ```python vit/command_line.py``` |
8 | * A snazzier option is to create a command line alias. For bash: | |
9 | * ```alias vit='PYTHONPATH=[path_to_root_dir] python vit/command_line.py'``` | |
8 | * A snazzier option is to create a command line alias. For Bash: | |
9 | ```bash | |
10 | alias vit='PYTHONPATH=[path_to_root_dir] python vit/command_line.py' | |
11 | ``` | |
12 | * ...or a shell function. For Bash: | |
13 | ```bash | |
14 | vit() { | |
15 | cd ~/git/vit && PYTHONPATH=${HOME}/git/vit python vit/command_line.py "$@" | |
16 | } | |
17 | export -f vit | |
18 | ``` | |
10 | 19 | |
11 | 20 | ### Tests |
12 | 21 | * Located in the ```tests``` directory |
36 | 45 | * Reports are generated via custom code in VIT, which allows extra features not found in Taskwarrior. Most report-related settings are read directly from the Taskwarrior configuration, which *mostly* allows a single point of configuration |
37 | 46 | * Data is written to Taskwarrior using a combination of ```import``` commands driven by [tasklib](https://github.com/robgolding/tasklib), and CLI calls for more complex scenarios |
38 | 47 | |
48 | ### Release checklist | |
49 | ||
50 | For any developer managing VIT releases, here's a simple checklist: | |
51 | ||
52 | #### Pre-release | |
53 | ||
54 | * Check `requirements.txt`, and bump any dependencies if necessary | |
55 | * Check `setup.py`, and bump the minimum Python version if the current version is no longer supported | |
56 | ||
57 | #### Release | |
58 | ||
59 | * Bump the release number in `vit/version.py` | |
60 | * Generate changelog entries using `scripts/generate-changelog-entries.sh` | |
61 | * Add changelog entries to `CHANGES.md` | |
62 | * Commit | |
63 | * Add the proper git tag and push it | |
64 | * Create a Github release for the tag, use the generated changelog entries in the description | |
65 | * Build and publish PyPi releases using `scripts/release-pypi.sh` | |
66 | ||
67 | #### Post-release | |
68 | ||
69 | * Announce on all relevant channels | |
39 | 70 | |
40 | 71 | ### Roadmap |
41 | 72 |
21 | 21 | ## Requirements |
22 | 22 | |
23 | 23 | * [Taskwarrior](https://taskwarrior.org) |
24 | * [Python](https://www.python.org) 3.5+ | |
24 | * [Python](https://www.python.org) 3.7+ | |
25 | 25 | * [pip](https://pypi.org/project/pip) |
26 | 26 | |
27 | 27 | ## Installation |
0 | #!/usr/bin/env bash | |
1 | ||
2 | # Auto-refreshes all open VIT interfaces on any modifications. | |
3 | # Looks for the special $IS_VIT_INSTANCE environment variable, | |
4 | # and if found, skips the refresh. | |
5 | ||
6 | # Make sure this script is in your PATH and executable, modify as necessary. | |
7 | # Refer to the example at scripts/vit-external-refresh.sh | |
8 | REFRESH_SCRIPT="vit-external-refresh.sh" | |
9 | ||
10 | # Count the number of tasks modified | |
11 | n=0 | |
12 | while read modified_task; do | |
13 | n=$((${n} + 1)) | |
14 | done | |
15 | ||
16 | if ((${n} > 0)); then | |
17 | if [ -z "${IS_VIT_INSTANCE}" ]; then | |
18 | logger "Tasks modified outside of VIT: ${n}, refreshing" | |
19 | ${REFRESH_SCRIPT} | |
20 | fi | |
21 | fi | |
22 | ||
23 | exit 0 |
0 | #!/usr/bin/env bash | |
1 | ||
2 | pids="$(vit --list-pids)" | |
3 | for pid in ${pids}; do | |
4 | kill -SIGUSR1 ${pid} &>/dev/null | |
5 | done |
4 | 4 | DEFAULT_BRANCH = "2.x" |
5 | 5 | BASE_GITHUB_URL = "https://github.com/vit-project/vit/blob" |
6 | 6 | |
7 | MARKUP_LINK_REGEX = "\[([^]]+)\]\(([\w]+\.md)\)" | |
7 | MARKUP_LINK_REGEX = r"\[([^]]+)\]\(([\w]+\.md)\)" | |
8 | 8 | FILE_DIR = path.dirname(path.abspath(path.realpath(__file__))) |
9 | 9 | |
10 | 10 | with open(path.join(FILE_DIR, 'README.md')) as f: |
20 | 20 | |
21 | 21 | setup( |
22 | 22 | name='vit', |
23 | packages=['vit', 'vit.formatter', 'vit.theme'], | |
23 | packages=['vit', 'vit.config', 'vit.formatter', 'vit.keybinding', 'vit.theme'], | |
24 | 24 | description="Visual Interactive Taskwarrior full-screen terminal interface", |
25 | 25 | long_description=README, |
26 | 26 | long_description_content_type='text/markdown', |
0 | 0 | import uuid |
1 | 1 | |
2 | class ActionManagerRegistrar(object): | |
2 | class ActionManagerRegistrar: | |
3 | 3 | def __init__(self, registry): |
4 | 4 | self.registry = registry |
5 | 5 | self.uuid = uuid.uuid4() |
36 | 36 | return actions[keybindings[keys]['action_name']] |
37 | 37 | return None |
38 | 38 | |
39 | class ActionManagerRegistry(object): | |
39 | class ActionManagerRegistry: | |
40 | 40 | def __init__(self, action_registry, keybindings, event=None): |
41 | 41 | self.actions = {} |
42 | 42 | self.action_registry = action_registry |
0 | class Actions(object): | |
0 | class Actions: | |
1 | 1 | |
2 | 2 | def __init__(self, action_registry): |
3 | 3 | self.action_registry = action_registry |
1 | 1 | |
2 | 2 | from importlib import import_module |
3 | 3 | |
4 | import os | |
5 | import signal | |
4 | 6 | import subprocess |
5 | 7 | # TODO: Use regex module for better PCRE support? |
6 | 8 | # https://bitbucket.org/mrabarnett/mrab-regex |
34 | 36 | from vit.registry import ActionRegistry, RequestReply |
35 | 37 | from vit.action_manager import ActionManagerRegistry |
36 | 38 | from vit.denotation import DenotationPopupLauncher |
39 | from vit.pid_manager import PidManager | |
37 | 40 | |
38 | 41 | # NOTE: This entire class is a workaround for the fact that urwid catches the |
39 | 42 | # 'ctrl l' keypress in its unhandled_input code, and prevents that from being |
66 | 69 | else: |
67 | 70 | return super().keypress(size, key) |
68 | 71 | |
69 | class Application(): | |
72 | class Application: | |
70 | 73 | def __init__(self, option, filters): |
71 | 74 | self.extra_filters = filters |
72 | 75 | self.loader = Loader() |
73 | 76 | self.load_early_config() |
74 | 77 | self.set_report() |
75 | 78 | self.setup_main_loop() |
79 | self.setup_signal_listeners() | |
76 | 80 | self.refresh(False) |
81 | self.setup_pid() | |
77 | 82 | self.loop.run() |
83 | ||
84 | def setup_signal_listeners(self): | |
85 | # Since not all platforms may have all signals, ensure they are | |
86 | # supported before adding a handler. | |
87 | if hasattr(signal, 'SIGUSR1'): | |
88 | pipe = self.loop.watch_pipe(self.async_refresh) | |
89 | def sigusr1_handler(signum, frame): | |
90 | os.write(pipe, b'x') | |
91 | signal.signal(signal.SIGUSR1, sigusr1_handler) | |
92 | if hasattr(signal, 'SIGTERM'): | |
93 | def sigterm_handler(signum, frame): | |
94 | self.signal_quit("SIGTERM") | |
95 | signal.signal(signal.SIGTERM, sigterm_handler) | |
96 | if hasattr(signal, 'SIGINT'): | |
97 | def sigint_handler(signum, frame): | |
98 | self.signal_quit("SIGINT") | |
99 | signal.signal(signal.SIGINT, sigint_handler) | |
100 | if hasattr(signal, 'SIGQUIT'): | |
101 | def sigquit_handler(signum, frame): | |
102 | self.signal_quit("SIGQUIT") | |
103 | signal.signal(signal.SIGQUIT, sigquit_handler) | |
78 | 104 | |
79 | 105 | def load_early_config(self): |
80 | 106 | self.config = ConfigParser(self.loader) |
95 | 121 | self.loop.screen.set_terminal_properties(colors=256) |
96 | 122 | except: |
97 | 123 | pass |
124 | ||
125 | def setup_pid(self): | |
126 | self.pid_manager = PidManager(self.config) | |
127 | self.pid_manager.setup() | |
128 | ||
129 | def teardown_pid(self): | |
130 | self.pid_manager.teardown() | |
98 | 131 | |
99 | 132 | def set_active_context(self): |
100 | 133 | self.context = self.task_config.get_active_context() |
182 | 215 | def default_keybinding_replacements(self): |
183 | 216 | import json |
184 | 217 | from datetime import datetime |
185 | task_replacement_match = re.compile("^TASK_(\w+)$") | |
218 | task_replacement_match = re.compile(r"^TASK_(\w+)$") | |
186 | 219 | def _task_attribute_match(variable): |
187 | 220 | matches = re.match(task_replacement_match, variable) |
188 | 221 | if matches: |
508 | 541 | rows = self.table.rows |
509 | 542 | current_index = start_index |
510 | 543 | last_index = len(rows) - 1 |
511 | start_matches = self.search_row_has_search_term(rows[start_index], search_regex) | |
512 | current_index = self.search_increment_index(current_index, reverse) | |
513 | while True: | |
514 | if reverse and current_index < 0: | |
515 | self.search_loop_warning('TOP', reverse) | |
516 | current_index = last_index | |
517 | elif not reverse and current_index > last_index: | |
518 | self.search_loop_warning('BOTTOM', reverse) | |
519 | current_index = 0 | |
520 | if self.search_row_has_search_term(rows[current_index], search_regex): | |
521 | return current_index | |
522 | if current_index == start_index: | |
523 | return start_index if start_matches else None | |
544 | if len(rows) > 0: | |
545 | start_matches = self.search_row_has_search_term(rows[start_index], search_regex) | |
524 | 546 | current_index = self.search_increment_index(current_index, reverse) |
547 | while True: | |
548 | if reverse and current_index < 0: | |
549 | self.search_loop_warning('TOP', reverse) | |
550 | current_index = last_index | |
551 | elif not reverse and current_index > last_index: | |
552 | self.search_loop_warning('BOTTOM', reverse) | |
553 | current_index = 0 | |
554 | if self.search_row_has_search_term(rows[current_index], search_regex): | |
555 | return current_index | |
556 | if current_index == start_index: | |
557 | return start_index if start_matches else None | |
558 | current_index = self.search_increment_index(current_index, reverse) | |
525 | 559 | |
526 | 560 | def search_increment_index(self, current_index, reverse=False): |
527 | 561 | return current_index + (-1 if reverse else 1) |
563 | 597 | pass |
564 | 598 | return False, False |
565 | 599 | |
600 | def signal_quit(self, signal): | |
601 | #import debug | |
602 | #debug.file("VIT received %s signal, quitting" % signal) | |
603 | self.quit() | |
604 | ||
566 | 605 | def quit(self): |
606 | self.teardown_pid() | |
567 | 607 | raise urwid.ExitMainLoop() |
568 | 608 | |
569 | 609 | def build_task_table(self): |
915 | 955 | else: |
916 | 956 | raise RuntimeError("Error retrieving completed tasks: %s" % stderr) |
917 | 957 | |
958 | def async_refresh(self, _): | |
959 | self.refresh() | |
960 | ||
918 | 961 | def refresh(self, load_early_config=True): |
919 | 962 | self.bootstrap(load_early_config) |
920 | 963 | self.build_main_widget() |
4 | 4 | from vit import util |
5 | 5 | from vit.process import Command |
6 | 6 | |
7 | class AutoComplete(object): | |
7 | class AutoComplete: | |
8 | 8 | |
9 | 9 | def __init__(self, config, default_filters=None, extra_filters=None): |
10 | 10 | self.default_filters = default_filters or ('column', 'project', 'tag') |
70 | 70 | self.keypress(size, '<Page Down>') |
71 | 71 | |
72 | 72 | def keypress_home(self, size): |
73 | self.set_focus(0) | |
73 | if len(self.body) > 0: | |
74 | self.set_focus(0) | |
74 | 75 | |
75 | 76 | def keypress_end(self, size): |
76 | self.set_focus(len(self.body) - 1) | |
77 | self.set_focus_valign('bottom') | |
77 | if len(self.body) > 0: | |
78 | self.set_focus(len(self.body) - 1) | |
79 | self.set_focus_valign('bottom') | |
78 | 80 | |
79 | 81 | def keypress_screen_top(self, size): |
80 | 82 | top, _, _ = self.get_top_middle_bottom_rows(size) |
92 | 94 | self.set_focus(bottom.position) |
93 | 95 | |
94 | 96 | def keypress_focus_valign_center(self, size): |
95 | self.set_focus(self.focus_position) | |
96 | self.set_focus_valign('middle') | |
97 | if len(self.body) > 0: | |
98 | self.set_focus(self.focus_position) | |
99 | self.set_focus_valign('middle') | |
97 | 100 | |
98 | 101 | def transform_special_keys(self, key): |
99 | 102 | # NOTE: These are special key presses passed to allow navigation |
8 | 8 | 'underline', |
9 | 9 | ] |
10 | 10 | |
11 | class TaskColorConfig(object): | |
11 | INVALID_COLOR_MODIFIERS = [ | |
12 | 'inverse', | |
13 | ] | |
14 | ||
15 | class TaskColorConfig: | |
12 | 16 | """Colorized task output. |
13 | 17 | """ |
14 | 18 | def __init__(self, config, task_config, theme, theme_alt_backgrounds): |
23 | 27 | # without pipes, the 'color' config setting in Taskwarrior is not used, and |
24 | 28 | # instead a custom setting is used. |
25 | 29 | self.color_enabled = self.config.get('color', 'enabled') |
26 | self.display_attrs_available, self.display_attrs = self.convert_color_config(self.task_config.filter_to_dict('^color\.')) | |
30 | self.display_attrs_available, self.display_attrs = self.convert_color_config(self.task_config.filter_to_dict(r'^color\.')) | |
27 | 31 | self.project_display_attrs = self.get_project_display_attrs() |
28 | 32 | if self.include_subprojects: |
29 | 33 | self.add_project_children() |
95 | 99 | remapped_colors = self.map_named_colors(sorted_parts) |
96 | 100 | return ','.join(remapped_colors) |
97 | 101 | |
102 | def check_invalid_color_parts(self, color_parts): | |
103 | invalid_color_parts = {*color_parts} & {*INVALID_COLOR_MODIFIERS} | |
104 | if invalid_color_parts: | |
105 | raise ValueError("The following TaskWarrior color definitions are unsupported in VIT: %s -- read the documentation for possible workarounds" % ", ".join(invalid_color_parts)) | |
106 | ||
98 | 107 | def map_named_colors(self, color_parts): |
99 | 108 | if len(color_parts) > 0 and color_parts[0] in self.task_256_to_urwid_256: |
100 | 109 | color_parts[0] = self.task_256_to_urwid_256[color_parts[0]] |
102 | 111 | |
103 | 112 | def make_color_parts(self, foreground, background): |
104 | 113 | foreground_parts = self.split_color_parts(foreground) |
114 | self.check_invalid_color_parts(foreground_parts) | |
105 | 115 | background_parts = self.split_color_parts(background) |
116 | self.check_invalid_color_parts(background_parts) | |
106 | 117 | return foreground_parts, background_parts |
107 | 118 | |
108 | 119 | def split_color_parts(self, color_parts): |
122 | 133 | return 0 |
123 | 134 | return sorted(color_parts, key=cmp_to_key(comparator)) |
124 | 135 | |
125 | class TaskColorizer(object): | |
126 | class Decorator(object): | |
136 | class TaskColorizer: | |
137 | class Decorator: | |
127 | 138 | def color_enabled(func): |
128 | 139 | @wraps(func) |
129 | 140 | def verify_color_enabled(self, *args, **kwargs): |
94 | 94 | def set_edit_text_callback(self): |
95 | 95 | return self.set_edit_text |
96 | 96 | |
97 | class CommandBarHistory(object): | |
97 | class CommandBarHistory: | |
98 | 98 | """Holds command-specific history for the command bar. |
99 | 99 | """ |
100 | 100 | def __init__(self): |
55 | 55 | # Boolean. If true, VIT will focus on the newly added task. Note: the new task must be |
56 | 56 | #included in the active filter for this setting to have effect. |
57 | 57 | #focus_on_add = False |
58 | ||
59 | # Path to a directory to manage pid files for running instances of VIT. | |
60 | # If no path is provided, no pid files will be managed. | |
61 | # The special token $UID may be used, and will be substituted with the user ID | |
62 | # of the user starting VIT. | |
63 | # VIT can be run with the '--list-pids' argument, which will output a list of | |
64 | # all pids in pid_dir; useful for sending signals to the running processes. | |
65 | # If you use this feature, it's suggested to choose a directory that is | |
66 | # automatically cleaned on boot, e.g.: | |
67 | # /var/run/user/$UID/vit | |
68 | # /tmp/vit_pids | |
69 | #pid_dir = | |
70 | ||
71 | # Int. The number of flash repetitions focusing on the edit made | |
72 | #flash_focus_repeat_times = 2 | |
73 | ||
74 | # Float. Waiting time for the blink focusing on the edit made | |
75 | #flash_focus_pause_seconds = 0.1 | |
58 | 76 | |
59 | 77 | [report] |
60 | 78 |
17 | 17 | SORT_ORDER_CHARACTERS = ['+', '-'] |
18 | 18 | SORT_COLLATE_CHARACTERS = ['/'] |
19 | 19 | VIT_CONFIG_FILE = 'config.ini' |
20 | FILTER_EXCLUSION_REGEX = re.compile('^limit:') | |
21 | FILTER_PARENS_REGEX = re.compile('([\(\)])') | |
22 | CONFIG_BOOLEAN_TRUE_REGEX = re.compile('1|yes|true', re.IGNORECASE) | |
20 | FILTER_EXCLUSION_REGEX = re.compile(r'^limit:') | |
21 | FILTER_PARENS_REGEX = re.compile(r'([\(\)])') | |
22 | CONFIG_BOOLEAN_TRUE_REGEX = re.compile(r'1|yes|true', re.IGNORECASE) | |
23 | 23 | # TaskParser expects clean hierarchies in the Taskwarrior dotted config names. |
24 | 24 | # However, this is occasionally violated, with a leaf ending in both a string |
25 | 25 | # value and another branch. The below list contains the config values that |
45 | 45 | 'mouse': False, |
46 | 46 | 'abort_backspace': False, |
47 | 47 | 'focus_on_add': False, |
48 | 'pid_dir': '', | |
49 | 'flash_focus_repeat_times': 2, | |
50 | 'flash_focus_pause_seconds': 0.1, | |
48 | 51 | }, |
49 | 52 | 'report': { |
50 | 53 | 'default_report': 'next', |
93 | 96 | 'J': '%j', # 3 digit day of year number, sometimes referred to as a Julian date, eg '001', '011', or '365' |
94 | 97 | } |
95 | 98 | |
96 | class ConfigParser(object): | |
99 | class ConfigParser: | |
97 | 100 | def __init__(self, loader): |
98 | 101 | self.loader = loader |
99 | 102 | self.config = configparser.SafeConfigParser() |
164 | 167 | try: |
165 | 168 | value = self.config.get(section, key) |
166 | 169 | return self.transform(key, value, default) |
167 | except (configparser.NoSectionError, configparser.NoOptionError): | |
170 | except (configparser.NoSectionError, configparser.NoOptionError, ValueError): | |
168 | 171 | return default |
169 | 172 | |
170 | 173 | def items(self, section): |
179 | 182 | def transform(self, key, value, default): |
180 | 183 | if isinstance(default, bool): |
181 | 184 | return self.transform_bool(value) |
185 | elif isinstance(default, int): | |
186 | return self.transform_int(value) | |
187 | elif isinstance(default, float): | |
188 | return self.transform_float(value) | |
182 | 189 | else: |
183 | 190 | return value |
184 | 191 | |
185 | 192 | def transform_bool(self, value): |
186 | 193 | return True if CONFIG_BOOLEAN_TRUE_REGEX.match(value) else False |
194 | ||
195 | def transform_int(self, value): | |
196 | return int(value) | |
197 | ||
198 | def transform_float(self, value): | |
199 | return float(value) | |
187 | 200 | |
188 | 201 | def get_taskrc_path(self): |
189 | 202 | taskrc_path = os.path.expanduser('TASKRC' in env.user and env.user['TASKRC'] or self.get('taskwarrior', 'taskrc')) |
210 | 223 | def is_mouse_enabled(self): |
211 | 224 | return self.get('vit', 'mouse') |
212 | 225 | |
213 | class TaskParser(object): | |
226 | def get_flash_focus_repeat_times(self): | |
227 | return self.get('vit', 'flash_focus_repeat_times') | |
228 | ||
229 | def get_flash_focus_pause_seconds(self): | |
230 | return self.get('vit', 'flash_focus_pause_seconds') | |
231 | ||
232 | class TaskParser: | |
214 | 233 | def __init__(self, config): |
215 | 234 | self.config = config |
216 | 235 | self.task_config = [] |
274 | 293 | def subtree(self, matcher, walk_subtree=True): |
275 | 294 | matcher_regex = matcher |
276 | 295 | if walk_subtree: |
277 | matcher_regex = r'%s' % (('^%s' % matcher).replace('.', '\.')) | |
296 | matcher_regex = r'%s' % (('^%s' % matcher).replace(r'.', r'\.')) | |
278 | 297 | full_tree = {} |
279 | 298 | lines = self.filter(matcher_regex) |
280 | 299 | for (hierarchy, value) in lines: |
0 | 0 | # TODO: Use urwid signals instead of custom event emitter? |
1 | class Emitter(object): | |
1 | class Emitter: | |
2 | 2 | """Simple event listener/emitter. |
3 | 3 | """ |
4 | 4 | def __init__(self): |
4 | 4 | except ImportError: |
5 | 5 | from backports.zoneinfo import ZoneInfo |
6 | 6 | |
7 | from vit.util import unicode_len | |
8 | ||
7 | 9 | TIME_UNIT_MAP = { |
8 | 10 | 'seconds': { |
9 | 11 | 'label': 's', |
41 | 43 | }, |
42 | 44 | } |
43 | 45 | |
44 | class Formatter(object): | |
46 | class Formatter: | |
45 | 47 | def __init__(self, column, report, formatter_base, blocking_task_uuids, **kwargs): |
46 | 48 | self.column = column |
47 | 49 | self.report = report |
53 | 55 | if not obj: |
54 | 56 | return self.empty() |
55 | 57 | obj = str(obj) |
56 | return (len(obj), self.markup_element(obj)) | |
58 | return (unicode_len(obj), self.markup_element(obj)) | |
57 | 59 | |
58 | 60 | def empty(self): |
59 | 61 | return (0, '') |
63 | 65 | |
64 | 66 | def markup_none(self, color): |
65 | 67 | if color: |
66 | return (len(self.formatter.none_label), (color, self.formatter.none_label)) | |
68 | return (unicode_len(self.formatter.none_label), (color, self.formatter.none_label)) | |
67 | 69 | else: |
68 | 70 | return self.empty() |
69 | 71 | |
96 | 98 | if not obj: |
97 | 99 | return self.empty() |
98 | 100 | formatted_duration = self.format_duration(obj) |
99 | return (len(formatted_duration), self.markup_element(obj, formatted_duration)) | |
101 | return (unicode_len(formatted_duration), self.markup_element(obj, formatted_duration)) | |
100 | 102 | |
101 | 103 | def format_duration(self, obj): |
102 | 104 | return obj |
113 | 115 | if not dt: |
114 | 116 | return self.empty() |
115 | 117 | formatted_date = self.format_datetime(dt, task) |
116 | return (len(formatted_date), self.markup_element(dt, formatted_date, task)) | |
118 | return (unicode_len(formatted_date), self.markup_element(dt, formatted_date, task)) | |
117 | 119 | |
118 | 120 | def format_datetime(self, dt, task): |
119 | 121 | return dt.strftime(self.custom_formatter or self.formatter.report) |
209 | 211 | if not obj: |
210 | 212 | return self.empty() |
211 | 213 | formatted = self.format_list(obj, task) |
212 | return (len(formatted), self.markup_element(obj, formatted)) | |
214 | return (unicode_len(formatted), self.markup_element(obj, formatted)) | |
213 | 215 | |
214 | 216 | def markup_element(self, obj, formatted): |
215 | 217 | return (self.colorize(obj), formatted) |
0 | 0 | from functools import reduce |
1 | 1 | |
2 | 2 | from vit.formatter import String |
3 | from vit.util import unicode_len | |
3 | 4 | |
4 | 5 | class Description(String): |
5 | 6 | def format(self, description, task): |
6 | 7 | if not description: |
7 | 8 | return self.empty() |
8 | width = len(description) | |
9 | width = unicode_len(description) | |
9 | 10 | colorized_description = self.colorize_description(description) |
10 | 11 | if task['annotations']: |
11 | 12 | annotation_width, colorized_description = self.format_combined(colorized_description, task) |
14 | 15 | return (width, colorized_description) |
15 | 16 | |
16 | 17 | def format_description_truncated(self, description): |
17 | return '%s...' % description[:self.formatter.description_truncate_len] if len(description) > self.formatter.description_truncate_len else description | |
18 | return '%s...' % description[:self.formatter.description_truncate_len] if unicode_len(description) > self.formatter.description_truncate_len else description | |
18 | 19 | |
19 | 20 | def format_combined(self, colorized_description, task): |
20 | 21 | annotation_width, formatted_annotations = self.format_annotations(task) |
24 | 25 | def reducer(accum, annotation): |
25 | 26 | width, formatted_list = accum |
26 | 27 | formatted = self.format_annotation(annotation) |
27 | new_width = len(formatted) | |
28 | new_width = unicode_len(formatted) | |
28 | 29 | if new_width > width: |
29 | 30 | width = new_width |
30 | 31 | formatted_list.append(formatted) |
0 | 0 | from vit.formatter.description import Description |
1 | from vit.util import unicode_len | |
1 | 2 | |
2 | 3 | class DescriptionCount(Description): |
3 | 4 | def format(self, description, task): |
4 | 5 | if not description: |
5 | 6 | return self.empty() |
6 | width = len(description) | |
7 | width = unicode_len(description) | |
7 | 8 | colorized_description = self.colorize_description(description) |
8 | 9 | if not task['annotations']: |
9 | 10 | return (width, colorized_description) |
13 | 14 | |
14 | 15 | def format_count(self, colorized_description, task): |
15 | 16 | count_string = self.format_annotation_count(task) |
16 | return len(count_string), colorized_description + [(None, count_string)] | |
17 | return unicode_len(count_string), colorized_description + [(None, count_string)] | |
17 | 18 | |
18 | 19 | def format_annotation_count(self, task): |
19 | 20 | return " [%d]" % len(task['annotations']) |
0 | 0 | from vit.formatter.description import Description |
1 | from vit.util import unicode_len | |
1 | 2 | |
2 | 3 | class DescriptionDesc(Description): |
3 | 4 | def format(self, description, task): |
4 | 5 | if not description: |
5 | 6 | return self.empty() |
6 | 7 | colorized_description = self.colorize_description(description) |
7 | return (len(description), colorized_description) | |
8 | return (unicode_len(description), colorized_description) |
0 | 0 | from vit.formatter.description import Description |
1 | from vit.util import unicode_len | |
1 | 2 | |
2 | 3 | class DescriptionTruncated(Description): |
3 | 4 | def format(self, description, task): |
4 | 5 | if not description: |
5 | 6 | return self.empty() |
6 | 7 | truncated_description = self.format_description_truncated(description) |
7 | width = len(truncated_description) | |
8 | width = unicode_len(truncated_description) | |
8 | 9 | colorized_description = self.colorize_description(truncated_description) |
9 | 10 | return (width, colorized_description) |
0 | 0 | from vit.formatter.description_count import DescriptionCount |
1 | from vit.util import unicode_len | |
1 | 2 | |
2 | 3 | class DescriptionTruncatedCount(DescriptionCount): |
3 | 4 | def format(self, description, task): |
4 | 5 | if not description: |
5 | 6 | return self.empty() |
6 | 7 | truncated_description = self.format_description_truncated(description) |
7 | width = len(truncated_description) | |
8 | width = unicode_len(truncated_description) | |
8 | 9 | colorized_description = self.colorize_description(truncated_description) |
9 | 10 | if not task['annotations']: |
10 | 11 | return (width, colorized_description) |
0 | 0 | import unicodedata |
1 | 1 | from vit.formatter import Marker |
2 | from vit.util import unicode_len | |
2 | 3 | |
3 | 4 | class Markers(Marker): |
4 | 5 | def format(self, _, task): |
35 | 36 | def add_label(self, color, label, width, text_markup): |
36 | 37 | if self.color_required(color) or not label: |
37 | 38 | return width, text_markup |
38 | width += len(label) + len([c for c in label if unicodedata.east_asian_width(c) == 'W']) | |
39 | width += unicode_len(label) | |
39 | 40 | text_markup += [(color, label)] |
40 | 41 | return width, text_markup |
41 | 42 |
0 | 0 | from vit.formatter import String |
1 | from vit.util import unicode_len | |
1 | 2 | |
2 | 3 | class Project(String): |
3 | 4 | def __init__(self, column, report, defaults, blocking_task_uuids, **kwargs): |
8 | 9 | return self.format_project(project, task) if project else self.markup_none(self.colorizer.project_none()) |
9 | 10 | |
10 | 11 | def format_project(self, project, task): |
11 | return self.format_subproject_indented(project, task) if self.indent_subprojects else (len(project), self.markup_element(project)) | |
12 | return self.format_subproject_indented(project, task) if self.indent_subprojects else (unicode_len(project), self.markup_element(project)) | |
12 | 13 | |
13 | 14 | def format_subproject_indented(self, project, task): |
14 | 15 | parts = project.split('.') |
0 | 0 | from vit import util |
1 | 1 | from vit.formatter.project import Project |
2 | from vit.util import unicode_len | |
2 | 3 | |
3 | 4 | class ProjectParent(Project): |
4 | 5 | def format(self, project, task): |
5 | 6 | parent = util.project_get_root(project) |
6 | return (len(parent), self.markup_element(parent)) if parent else self.markup_none(self.colorizer.project_none()) | |
7 | return (unicode_len(parent), self.markup_element(parent)) if parent else self.markup_none(self.colorizer.project_none()) | |
7 | 8 |
0 | 0 | from vit.formatter import Formatter |
1 | from vit.util import unicode_len | |
1 | 2 | |
2 | 3 | class Tags(Formatter): |
3 | 4 | def format(self, tags, task): |
5 | 6 | return self.markup_none(self.colorizer.tag_none()) |
6 | 7 | elif len(tags) == 1: |
7 | 8 | tag = list(tags)[0] |
8 | return (len(tag), self.markup_element(tag)) | |
9 | return (unicode_len(tag), self.markup_element(tag)) | |
9 | 10 | else: |
10 | 11 | last_tag = list(tags)[-1] |
11 | 12 | width = 0 |
12 | 13 | text_markup = [] |
13 | 14 | for tag in tags: |
14 | width += len(tag) | |
15 | width += unicode_len(tag) | |
15 | 16 | text_markup += [self.markup_element(tag)] |
16 | 17 | if tag != last_tag: |
17 | 18 | width += 1 |
0 | 0 | import datetime |
1 | 1 | from vit.formatter import DateTime |
2 | from vit.util import unicode_len | |
2 | 3 | |
3 | 4 | # TODO: Remove this once tasklib bug is fixed. |
4 | 5 | from tasklib.serializing import SerializingObject |
12 | 13 | # https://github.com/robgolding/tasklib/issues/30 |
13 | 14 | dt = dt if isinstance(dt, datetime.datetime) else serializer.timestamp_deserializer(dt) |
14 | 15 | formatted_date = dt.strftime(self.custom_formatter or self.formatter.report) |
15 | return (len(formatted_date), (self.colorize(dt), formatted_date)) | |
16 | return (unicode_len(formatted_date), (self.colorize(dt), formatted_date)) | |
16 | 17 | def colorize(self, dt=None): |
17 | 18 | return self.colorizer.uda_date(self.column, dt) |
0 | 0 | from vit.formatter import String |
1 | from vit.util import unicode_len | |
1 | 2 | |
2 | 3 | class UdaDuration(String): |
3 | 4 | def format(self, duration, task): |
4 | 5 | if not duration: |
5 | 6 | return self.markup_none(self.colorize()) |
6 | return (len(duration), self.markup_element(duration)) | |
7 | return (unicode_len(duration), self.markup_element(duration)) | |
7 | 8 | def colorize(self, duration=None): |
8 | 9 | return self.colorizer.uda_duration(self.column, duration) |
0 | 0 | from vit.formatter import Formatter |
1 | from vit.util import unicode_len | |
1 | 2 | |
2 | 3 | class UdaIndicator(Formatter): |
3 | 4 | def format(self, value, task): |
5 | 6 | return self.markup_none(self.colorize()) |
6 | 7 | else: |
7 | 8 | indicator = self.formatter.indicator_uda[self.column] |
8 | return (len(indicator), (self.colorize(value), indicator)) | |
9 | return (unicode_len(indicator), (self.colorize(value), indicator)) | |
9 | 10 | |
10 | 11 | def colorize(self, value=None): |
11 | 12 | return self.colorizer.uda_indicator(self.column, value) |
0 | 0 | from vit.formatter import Number |
1 | from vit.util import unicode_len | |
1 | 2 | |
2 | 3 | class UdaNumeric(Number): |
3 | 4 | def format(self, number, task): |
4 | 5 | if number is None: |
5 | 6 | return self.markup_none(self.colorize()) |
6 | 7 | number = str(number) |
7 | return (len(number), self.markup_element(number)) | |
8 | return (unicode_len(number), self.markup_element(number)) | |
8 | 9 | def colorize(self, number=None): |
9 | 10 | return self.colorizer.uda_numeric(self.column, number) |
0 | 0 | from vit.formatter import String |
1 | from vit.util import unicode_len | |
1 | 2 | |
2 | 3 | class UdaString(String): |
3 | 4 | def format(self, string, task): |
4 | 5 | if not string: |
5 | 6 | return self.markup_none(self.colorize()) |
6 | return (len(string), self.markup_element(string)) | |
7 | return (unicode_len(string), self.markup_element(string)) | |
7 | 8 | def colorize(self, string=None): |
8 | 9 | return self.colorizer.uda_string(self.column, string) |
6 | 6 | |
7 | 7 | from vit import util |
8 | 8 | from vit import uda |
9 | from vit.util import unicode_len | |
9 | 10 | |
10 | 11 | INDICATORS = [ |
11 | 12 | 'active', |
17 | 18 | |
18 | 19 | DEFAULT_DESCRIPTION_TRUNCATE_LEN=20 |
19 | 20 | |
20 | class FormatterBase(object): | |
21 | class FormatterBase: | |
21 | 22 | def __init__(self, loader, config, task_config, markers, task_colorizer): |
22 | 23 | self.loader = loader |
23 | 24 | self.config = config |
88 | 89 | def format_subproject_indented(self, project_parts): |
89 | 90 | if len(project_parts) == 1: |
90 | 91 | subproject = project_parts[0] |
91 | return (len(subproject), '', '', subproject) | |
92 | return (unicode_len(subproject), '', '', subproject) | |
92 | 93 | else: |
93 | 94 | subproject = project_parts.pop() |
94 | 95 | space_padding = (len(project_parts) * 2) - 1 |
95 | 96 | indicator = u'\u21aa ' |
96 | width = space_padding + len(indicator) + len(subproject) | |
97 | width = space_padding + unicode_len(indicator) + unicode_len(subproject) | |
97 | 98 | return (width, ' ' * space_padding , indicator, subproject) |
98 | 99 | |
99 | 100 | def recalculate_due_datetimes(self): |
2 | 2 | import urwid |
3 | 3 | |
4 | 4 | from vit.base_list_box import BaseListBox |
5 | from vit.util import unicode_len | |
5 | 6 | |
6 | 7 | CURLY_BRACES_REGEX = re.compile("[{}]") |
7 | 8 | SPECIAL_KEY_SUBSTITUTIONS = { |
56 | 57 | 'keys': 0, |
57 | 58 | } |
58 | 59 | for entry in entries: |
59 | type_len = len(entry[0]) | |
60 | keys_len = len(entry[1]) | |
60 | type_len = unicode_len(entry[0]) | |
61 | keys_len = unicode_len(entry[1]) | |
61 | 62 | if type_len > column_widths['type']: |
62 | 63 | column_widths['type'] = type_len |
63 | 64 | if keys_len > column_widths['keys']: |
77 | 78 | def eat_other_keybindings(self): |
78 | 79 | return True |
79 | 80 | |
80 | class Help(object): | |
81 | class Help: | |
81 | 82 | """Generates help list/display. |
82 | 83 | """ |
83 | 84 | def __init__(self, keybinding_parser, actions, event=None, request_reply=None, action_manager=None): |
2 | 2 | class KeyCacheError(Exception): |
3 | 3 | pass |
4 | 4 | |
5 | class KeyCache(object): | |
5 | class KeyCache: | |
6 | 6 | def __init__(self, keybindings): |
7 | 7 | self.keybindings = keybindings |
8 | 8 | self.cached_keys = '' |
22 | 22 | class KeybindingError(Exception): |
23 | 23 | pass |
24 | 24 | |
25 | class KeybindingParser(object): | |
25 | class KeybindingParser: | |
26 | 26 | def __init__(self, loader, config, action_registry): |
27 | 27 | self.loader = loader |
28 | 28 | self.config = config |
2 | 2 | class ListBatchError(Exception): |
3 | 3 | pass |
4 | 4 | |
5 | class ListBatcher(object): | |
5 | class ListBatcher: | |
6 | 6 | def __init__(self, batch_from, batch_to, batch_to_formatter=None, default_batch_size=DEFAULT_BATCH_SIZE): |
7 | 7 | self.batch_from = batch_from |
8 | 8 | self.batch_to = batch_to |
7 | 7 | |
8 | 8 | DEFAULT_VIT_DIR = '~/.vit' |
9 | 9 | |
10 | class Loader(object): | |
10 | class Loader: | |
11 | 11 | def __init__(self): |
12 | 12 | self.user_config_dir = os.path.expanduser('VIT_DIR' in env.user and env.user['VIT_DIR'] or DEFAULT_VIT_DIR) |
13 | 13 |
32 | 32 | 'until.label': '(U)', |
33 | 33 | } |
34 | 34 | |
35 | class Markers(object): | |
35 | class Markers: | |
36 | 36 | def __init__(self, config, task_config): |
37 | 37 | self.config = config |
38 | 38 | self.task_config = task_config |
0 | import glob | |
0 | 1 | import sys |
1 | 2 | import argparse |
2 | 3 | |
33 | 34 | action="store_true", |
34 | 35 | help="list all available actions", |
35 | 36 | ) |
37 | parser.add_argument('--list-pids', | |
38 | dest="list_pids", | |
39 | default=False, | |
40 | action="store_true", | |
41 | help="list all pids found in pid_dir, if configured", | |
42 | ) | |
36 | 43 | |
37 | 44 | def parse_options(): |
38 | 45 | options, filters = parser.parse_known_args() |
39 | 46 | if options.list_actions: |
40 | 47 | list_actions() |
41 | 48 | sys.exit(0) |
49 | elif options.list_pids: | |
50 | ret = list_pids() | |
51 | sys.exit(ret) | |
42 | 52 | return options, filters |
43 | 53 | |
44 | 54 | def format_dictionary_list(item, description): |
52 | 62 | actions = Actions(action_registry) |
53 | 63 | actions.register() |
54 | 64 | any(format_dictionary_list(action, data['description']) for action, data in actions.get().items()) |
65 | ||
66 | def _get_pids_from_pid_dir(pid_dir): | |
67 | filepaths = glob.glob("%s/*.pid" % pid_dir) | |
68 | pids = [] | |
69 | for filepath in filepaths: | |
70 | try: | |
71 | with open(filepath, 'r') as f: | |
72 | pids.append(f.read()) | |
73 | except IOError: | |
74 | pass | |
75 | return pids | |
76 | ||
77 | def list_pids(): | |
78 | from vit.loader import Loader | |
79 | from vit.config_parser import ConfigParser | |
80 | from vit.pid_manager import PidManager | |
81 | loader = Loader() | |
82 | config = ConfigParser(loader) | |
83 | pid_manager = PidManager(config) | |
84 | if pid_manager.pid_dir: | |
85 | pids = _get_pids_from_pid_dir(pid_manager.pid_dir) | |
86 | print("\n".join(pids)) | |
87 | return 0 | |
88 | else: | |
89 | print("ERROR: No pid_dir configured") | |
90 | return 1 |
0 | import os | |
1 | import errno | |
2 | ||
3 | class PidManager: | |
4 | """Simple process ID manager. | |
5 | """ | |
6 | def __init__(self, config): | |
7 | self.config = config | |
8 | self.uid = os.getuid() | |
9 | self.pid = os.getpid() | |
10 | self._format_pid_dir() | |
11 | self._make_pid_filepath() | |
12 | ||
13 | def setup(self): | |
14 | if self.pid_dir: | |
15 | self._create_pid_dir() | |
16 | self._write_pid_file() | |
17 | ||
18 | def teardown(self): | |
19 | if self.pid_dir: | |
20 | try: | |
21 | os.remove(self.pid_file) | |
22 | # TODO: This needs a little more work to skip errors when no PID file | |
23 | # exists. | |
24 | #except OSError as e: | |
25 | # if e.errno != errno.ENOENT: | |
26 | # raise OSError("could not remove pid file %s" % self.pid_file) | |
27 | except: | |
28 | pass | |
29 | ||
30 | def _format_pid_dir(self): | |
31 | config_pid_dir = self.config.get('vit', 'pid_dir') | |
32 | self.pid_dir = config_pid_dir.replace("$UID", str(self.uid)) | |
33 | ||
34 | def _make_pid_filepath(self): | |
35 | self.pid_file = "%s/%s.pid" % (self.pid_dir, self.pid) | |
36 | ||
37 | def _create_pid_dir(self): | |
38 | try: | |
39 | os.makedirs(self.pid_dir, exist_ok=True) | |
40 | except OSError: | |
41 | raise OSError("could not create pid_dir %s" % self.pid_dir) | |
42 | ||
43 | def _write_pid_file(self): | |
44 | try: | |
45 | with open(self.pid_file, "w") as f: | |
46 | f.write(str(self.pid)) | |
47 | except IOError: | |
48 | raise IOError("could not write pid file %s" % self.pid_file) |
7 | 7 | |
8 | 8 | DEFAULT_CONFIRM = 'Press Enter to continue...' |
9 | 9 | |
10 | class Command(object): | |
10 | class Command: | |
11 | 11 | |
12 | 12 | def __init__(self, config): |
13 | 13 | self.config = config |
0 | 0 | import string |
1 | 1 | import re |
2 | 2 | |
3 | class Readline(object): | |
3 | class Readline: | |
4 | 4 | def __init__(self, edit_obj): |
5 | 5 | self.edit_obj = edit_obj |
6 | 6 | word_chars = string.ascii_letters + string.digits + "_" |
0 | 0 | import uuid |
1 | 1 | |
2 | class ActionRegistrar(object): | |
2 | class ActionRegistrar: | |
3 | 3 | def __init__(self, registry): |
4 | 4 | self.registry = registry |
5 | 5 | self.uuid = uuid.uuid4() |
16 | 16 | def actions(self): |
17 | 17 | return self.registry.get_registered(self.uuid) |
18 | 18 | |
19 | class ActionRegistry(object): | |
19 | class ActionRegistry: | |
20 | 20 | def __init__(self): |
21 | 21 | self.actions = {} |
22 | 22 | self.noop_action_name = 'NOOP' |
47 | 47 | def noop(self): |
48 | 48 | pass |
49 | 49 | |
50 | class RequestReply(object): | |
50 | class RequestReply: | |
51 | 51 | def __init__(self): |
52 | 52 | self.handlers = {} |
53 | 53 |
7 | 7 | from vit import util |
8 | 8 | from vit.exception import VitException |
9 | 9 | |
10 | class TaskListModel(object): | |
10 | class TaskListModel: | |
11 | 11 | def __init__(self, task_config, reports, report=None, data_location=None): |
12 | 12 | |
13 | 13 | if not data_location: |
12 | 12 | from vit.base_list_box import BaseListBox |
13 | 13 | from vit.list_batcher import ListBatcher |
14 | 14 | from vit.formatter.project import Project as ProjectFormatter |
15 | from vit.util import unicode_len | |
16 | ||
15 | 17 | |
16 | 18 | REDUCE_COLUMN_WIDTH_LIMIT = 20 |
17 | 19 | COLUMN_PADDING = 2 |
18 | 20 | MARKER_COLUMN_NAME = 'markers' |
19 | 21 | |
20 | class TaskTable(object): | |
22 | class TaskTable: | |
21 | 23 | |
22 | 24 | def __init__(self, config, task_config, formatter, screen, on_select=None, event=None, action_manager=None, request_reply=None, markers=None, draw_screen_callback=None): |
23 | 25 | self.config = config |
150 | 152 | position = self.listbox.focus_position |
151 | 153 | self.list_walker[position].row.set_attr_map({None: attr}) |
152 | 154 | |
153 | def flash_focus(self, repeat_times=2, pause_seconds=0.1): | |
155 | def flash_focus(self, repeat_times=None, pause_seconds=None): | |
156 | if repeat_times is None: | |
157 | repeat_times = self.config.get_flash_focus_repeat_times() | |
158 | if pause_seconds is None: | |
159 | pause_seconds = self.config.get_flash_focus_pause_seconds() | |
154 | 160 | if self.listbox.focus: |
155 | 161 | position = self.listbox.focus_position if self.listbox.focus_position is not None else self.listbox.previous_focus_position if self.listbox.previous_focus_position is not None else None |
156 | if position is not None: | |
162 | if position is not None and repeat_times > 0: | |
157 | 163 | self.update_focus_attr('flash on', position) |
158 | 164 | self.draw_screen() |
159 | 165 | for i in repeat(None, repeat_times): |
247 | 253 | if isinstance(formatted_value, tuple): |
248 | 254 | return formatted_value |
249 | 255 | else: |
250 | width = len(formatted_value) if formatted_value else 0 | |
256 | width = unicode_len(formatted_value) if formatted_value else 0 | |
251 | 257 | return width, formatted_value |
252 | 258 | |
253 | 259 | def subproject_indentable(self): |
328 | 334 | |
329 | 335 | def reconcile_column_width_for_label(self): |
330 | 336 | for idx, column in enumerate(self.columns): |
331 | label_len = len(column['label']) | |
337 | label_len = unicode_len(column['label']) | |
332 | 338 | if column['width'] < label_len: |
333 | 339 | self.columns[idx]['width'] = label_len |
334 | 340 | |
387 | 393 | if grew > 0: |
388 | 394 | self.batcher.add(grew) |
389 | 395 | |
390 | class TaskRow(): | |
396 | class TaskRow: | |
391 | 397 | def __init__(self, task, data, alt_row): |
392 | 398 | self.task = task |
393 | 399 | self.data = data |
395 | 401 | self.uuid = self.task['uuid'] |
396 | 402 | self.id = self.task['id'] |
397 | 403 | |
398 | class ProjectRow(): | |
404 | class ProjectRow: | |
399 | 405 | def __init__(self, project, placeholder, alt_row): |
400 | 406 | self.project = project |
401 | 407 | self.placeholder = placeholder |
2 | 2 | import curses |
3 | 3 | import shlex |
4 | 4 | from functools import reduce |
5 | ||
6 | from urwid.str_util import calc_width | |
5 | 7 | |
6 | 8 | curses.setupterm() |
7 | 9 | e3_seq = curses.tigetstr('E3') or b'' |
54 | 56 | |
55 | 57 | def file_readable(filepath): |
56 | 58 | return os.path.isfile(filepath) and os.access(filepath, os.R_OK) |
59 | ||
60 | def unicode_len(string): | |
61 | return calc_width(string, 0, len(string)) |