Codebase list vit / 64e765f
Update upstream source from tag 'upstream/2.3.0' Update to upstream version '2.3.0' with Debian dir f4e9d1316a24d866dabdb6fe00da64975fc4edeb Jochen Sprickerhof 10 months ago
49 changed file(s) with 419 addition(s) and 92 deletion(s). Raw diff Collapse all Expand all
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
026 ##### Sun Apr 17 2022 - released v2.2.0
127
228 * **Sun Apr 17 2022:** bump dependency versions
9393
9494 ```python
9595 # keybinding/keybinding.py
96 class Keybinding(object):
96 class Keybinding:
9797 def replacements(self):
9898 def _custom_match(variable):
9999 if variable == 'TEST':
139139 Now, for example, to jump to a task whose ID is 42, you need to press `4`, `2`
140140 and `<Enter>`, instead of `:`, `4`, `2` and `<Enter>`.
141141 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.
55 4. ```vit/command_line.py``` is the entry point for the application. To run it without a full installation:
66 * Set the ```PYTHONPATH``` environment variable to the root directory of the repository
77 * 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 ```
1019
1120 ### Tests
1221 * Located in the ```tests``` directory
3645 * 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
3746 * 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
3847
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
3970
4071 ### Roadmap
4172
2121 ## Requirements
2222
2323 * [Taskwarrior](https://taskwarrior.org)
24 * [Python](https://www.python.org) 3.5+
24 * [Python](https://www.python.org) 3.7+
2525 * [pip](https://pypi.org/project/pip)
2626
2727 ## 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
44 DEFAULT_BRANCH = "2.x"
55 BASE_GITHUB_URL = "https://github.com/vit-project/vit/blob"
66
7 MARKUP_LINK_REGEX = "\[([^]]+)\]\(([\w]+\.md)\)"
7 MARKUP_LINK_REGEX = r"\[([^]]+)\]\(([\w]+\.md)\)"
88 FILE_DIR = path.dirname(path.abspath(path.realpath(__file__)))
99
1010 with open(path.join(FILE_DIR, 'README.md')) as f:
2020
2121 setup(
2222 name='vit',
23 packages=['vit', 'vit.formatter', 'vit.theme'],
23 packages=['vit', 'vit.config', 'vit.formatter', 'vit.keybinding', 'vit.theme'],
2424 description="Visual Interactive Taskwarrior full-screen terminal interface",
2525 long_description=README,
2626 long_description_content_type='text/markdown',
00 import uuid
11
2 class ActionManagerRegistrar(object):
2 class ActionManagerRegistrar:
33 def __init__(self, registry):
44 self.registry = registry
55 self.uuid = uuid.uuid4()
3636 return actions[keybindings[keys]['action_name']]
3737 return None
3838
39 class ActionManagerRegistry(object):
39 class ActionManagerRegistry:
4040 def __init__(self, action_registry, keybindings, event=None):
4141 self.actions = {}
4242 self.action_registry = action_registry
0 class Actions(object):
0 class Actions:
11
22 def __init__(self, action_registry):
33 self.action_registry = action_registry
11
22 from importlib import import_module
33
4 import os
5 import signal
46 import subprocess
57 # TODO: Use regex module for better PCRE support?
68 # https://bitbucket.org/mrabarnett/mrab-regex
3436 from vit.registry import ActionRegistry, RequestReply
3537 from vit.action_manager import ActionManagerRegistry
3638 from vit.denotation import DenotationPopupLauncher
39 from vit.pid_manager import PidManager
3740
3841 # NOTE: This entire class is a workaround for the fact that urwid catches the
3942 # 'ctrl l' keypress in its unhandled_input code, and prevents that from being
6669 else:
6770 return super().keypress(size, key)
6871
69 class Application():
72 class Application:
7073 def __init__(self, option, filters):
7174 self.extra_filters = filters
7275 self.loader = Loader()
7376 self.load_early_config()
7477 self.set_report()
7578 self.setup_main_loop()
79 self.setup_signal_listeners()
7680 self.refresh(False)
81 self.setup_pid()
7782 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)
78104
79105 def load_early_config(self):
80106 self.config = ConfigParser(self.loader)
95121 self.loop.screen.set_terminal_properties(colors=256)
96122 except:
97123 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()
98131
99132 def set_active_context(self):
100133 self.context = self.task_config.get_active_context()
182215 def default_keybinding_replacements(self):
183216 import json
184217 from datetime import datetime
185 task_replacement_match = re.compile("^TASK_(\w+)$")
218 task_replacement_match = re.compile(r"^TASK_(\w+)$")
186219 def _task_attribute_match(variable):
187220 matches = re.match(task_replacement_match, variable)
188221 if matches:
508541 rows = self.table.rows
509542 current_index = start_index
510543 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)
524546 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)
525559
526560 def search_increment_index(self, current_index, reverse=False):
527561 return current_index + (-1 if reverse else 1)
563597 pass
564598 return False, False
565599
600 def signal_quit(self, signal):
601 #import debug
602 #debug.file("VIT received %s signal, quitting" % signal)
603 self.quit()
604
566605 def quit(self):
606 self.teardown_pid()
567607 raise urwid.ExitMainLoop()
568608
569609 def build_task_table(self):
915955 else:
916956 raise RuntimeError("Error retrieving completed tasks: %s" % stderr)
917957
958 def async_refresh(self, _):
959 self.refresh()
960
918961 def refresh(self, load_early_config=True):
919962 self.bootstrap(load_early_config)
920963 self.build_main_widget()
44 from vit import util
55 from vit.process import Command
66
7 class AutoComplete(object):
7 class AutoComplete:
88
99 def __init__(self, config, default_filters=None, extra_filters=None):
1010 self.default_filters = default_filters or ('column', 'project', 'tag')
7070 self.keypress(size, '<Page Down>')
7171
7272 def keypress_home(self, size):
73 self.set_focus(0)
73 if len(self.body) > 0:
74 self.set_focus(0)
7475
7576 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')
7880
7981 def keypress_screen_top(self, size):
8082 top, _, _ = self.get_top_middle_bottom_rows(size)
9294 self.set_focus(bottom.position)
9395
9496 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')
97100
98101 def transform_special_keys(self, key):
99102 # NOTE: These are special key presses passed to allow navigation
88 'underline',
99 ]
1010
11 class TaskColorConfig(object):
11 INVALID_COLOR_MODIFIERS = [
12 'inverse',
13 ]
14
15 class TaskColorConfig:
1216 """Colorized task output.
1317 """
1418 def __init__(self, config, task_config, theme, theme_alt_backgrounds):
2327 # without pipes, the 'color' config setting in Taskwarrior is not used, and
2428 # instead a custom setting is used.
2529 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\.'))
2731 self.project_display_attrs = self.get_project_display_attrs()
2832 if self.include_subprojects:
2933 self.add_project_children()
9599 remapped_colors = self.map_named_colors(sorted_parts)
96100 return ','.join(remapped_colors)
97101
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
98107 def map_named_colors(self, color_parts):
99108 if len(color_parts) > 0 and color_parts[0] in self.task_256_to_urwid_256:
100109 color_parts[0] = self.task_256_to_urwid_256[color_parts[0]]
102111
103112 def make_color_parts(self, foreground, background):
104113 foreground_parts = self.split_color_parts(foreground)
114 self.check_invalid_color_parts(foreground_parts)
105115 background_parts = self.split_color_parts(background)
116 self.check_invalid_color_parts(background_parts)
106117 return foreground_parts, background_parts
107118
108119 def split_color_parts(self, color_parts):
122133 return 0
123134 return sorted(color_parts, key=cmp_to_key(comparator))
124135
125 class TaskColorizer(object):
126 class Decorator(object):
136 class TaskColorizer:
137 class Decorator:
127138 def color_enabled(func):
128139 @wraps(func)
129140 def verify_color_enabled(self, *args, **kwargs):
9494 def set_edit_text_callback(self):
9595 return self.set_edit_text
9696
97 class CommandBarHistory(object):
97 class CommandBarHistory:
9898 """Holds command-specific history for the command bar.
9999 """
100100 def __init__(self):
5555 # Boolean. If true, VIT will focus on the newly added task. Note: the new task must be
5656 #included in the active filter for this setting to have effect.
5757 #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
5876
5977 [report]
6078
1717 SORT_ORDER_CHARACTERS = ['+', '-']
1818 SORT_COLLATE_CHARACTERS = ['/']
1919 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)
2323 # TaskParser expects clean hierarchies in the Taskwarrior dotted config names.
2424 # 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
4545 'mouse': False,
4646 'abort_backspace': False,
4747 'focus_on_add': False,
48 'pid_dir': '',
49 'flash_focus_repeat_times': 2,
50 'flash_focus_pause_seconds': 0.1,
4851 },
4952 'report': {
5053 'default_report': 'next',
9396 'J': '%j', # 3 digit day of year number, sometimes referred to as a Julian date, eg '001', '011', or '365'
9497 }
9598
96 class ConfigParser(object):
99 class ConfigParser:
97100 def __init__(self, loader):
98101 self.loader = loader
99102 self.config = configparser.SafeConfigParser()
164167 try:
165168 value = self.config.get(section, key)
166169 return self.transform(key, value, default)
167 except (configparser.NoSectionError, configparser.NoOptionError):
170 except (configparser.NoSectionError, configparser.NoOptionError, ValueError):
168171 return default
169172
170173 def items(self, section):
179182 def transform(self, key, value, default):
180183 if isinstance(default, bool):
181184 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)
182189 else:
183190 return value
184191
185192 def transform_bool(self, value):
186193 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)
187200
188201 def get_taskrc_path(self):
189202 taskrc_path = os.path.expanduser('TASKRC' in env.user and env.user['TASKRC'] or self.get('taskwarrior', 'taskrc'))
210223 def is_mouse_enabled(self):
211224 return self.get('vit', 'mouse')
212225
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:
214233 def __init__(self, config):
215234 self.config = config
216235 self.task_config = []
274293 def subtree(self, matcher, walk_subtree=True):
275294 matcher_regex = matcher
276295 if walk_subtree:
277 matcher_regex = r'%s' % (('^%s' % matcher).replace('.', '\.'))
296 matcher_regex = r'%s' % (('^%s' % matcher).replace(r'.', r'\.'))
278297 full_tree = {}
279298 lines = self.filter(matcher_regex)
280299 for (hierarchy, value) in lines:
00 import os
11
2 os.environ['IS_VIT_INSTANCE'] = "1"
3
24 user = os.environ.copy()
00 # TODO: Use urwid signals instead of custom event emitter?
1 class Emitter(object):
1 class Emitter:
22 """Simple event listener/emitter.
33 """
44 def __init__(self):
44 except ImportError:
55 from backports.zoneinfo import ZoneInfo
66
7 from vit.util import unicode_len
8
79 TIME_UNIT_MAP = {
810 'seconds': {
911 'label': 's',
4143 },
4244 }
4345
44 class Formatter(object):
46 class Formatter:
4547 def __init__(self, column, report, formatter_base, blocking_task_uuids, **kwargs):
4648 self.column = column
4749 self.report = report
5355 if not obj:
5456 return self.empty()
5557 obj = str(obj)
56 return (len(obj), self.markup_element(obj))
58 return (unicode_len(obj), self.markup_element(obj))
5759
5860 def empty(self):
5961 return (0, '')
6365
6466 def markup_none(self, color):
6567 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))
6769 else:
6870 return self.empty()
6971
9698 if not obj:
9799 return self.empty()
98100 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))
100102
101103 def format_duration(self, obj):
102104 return obj
113115 if not dt:
114116 return self.empty()
115117 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))
117119
118120 def format_datetime(self, dt, task):
119121 return dt.strftime(self.custom_formatter or self.formatter.report)
209211 if not obj:
210212 return self.empty()
211213 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))
213215
214216 def markup_element(self, obj, formatted):
215217 return (self.colorize(obj), formatted)
00 from functools import reduce
11
22 from vit.formatter import String
3 from vit.util import unicode_len
34
45 class Description(String):
56 def format(self, description, task):
67 if not description:
78 return self.empty()
8 width = len(description)
9 width = unicode_len(description)
910 colorized_description = self.colorize_description(description)
1011 if task['annotations']:
1112 annotation_width, colorized_description = self.format_combined(colorized_description, task)
1415 return (width, colorized_description)
1516
1617 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
1819
1920 def format_combined(self, colorized_description, task):
2021 annotation_width, formatted_annotations = self.format_annotations(task)
2425 def reducer(accum, annotation):
2526 width, formatted_list = accum
2627 formatted = self.format_annotation(annotation)
27 new_width = len(formatted)
28 new_width = unicode_len(formatted)
2829 if new_width > width:
2930 width = new_width
3031 formatted_list.append(formatted)
00 from vit.formatter.description import Description
1 from vit.util import unicode_len
12
23 class DescriptionCount(Description):
34 def format(self, description, task):
45 if not description:
56 return self.empty()
6 width = len(description)
7 width = unicode_len(description)
78 colorized_description = self.colorize_description(description)
89 if not task['annotations']:
910 return (width, colorized_description)
1314
1415 def format_count(self, colorized_description, task):
1516 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)]
1718
1819 def format_annotation_count(self, task):
1920 return " [%d]" % len(task['annotations'])
00 from vit.formatter.description import Description
1 from vit.util import unicode_len
12
23 class DescriptionDesc(Description):
34 def format(self, description, task):
45 if not description:
56 return self.empty()
67 colorized_description = self.colorize_description(description)
7 return (len(description), colorized_description)
8 return (unicode_len(description), colorized_description)
00 from vit.formatter.description import Description
1 from vit.util import unicode_len
12
23 class DescriptionTruncated(Description):
34 def format(self, description, task):
45 if not description:
56 return self.empty()
67 truncated_description = self.format_description_truncated(description)
7 width = len(truncated_description)
8 width = unicode_len(truncated_description)
89 colorized_description = self.colorize_description(truncated_description)
910 return (width, colorized_description)
00 from vit.formatter.description_count import DescriptionCount
1 from vit.util import unicode_len
12
23 class DescriptionTruncatedCount(DescriptionCount):
34 def format(self, description, task):
45 if not description:
56 return self.empty()
67 truncated_description = self.format_description_truncated(description)
7 width = len(truncated_description)
8 width = unicode_len(truncated_description)
89 colorized_description = self.colorize_description(truncated_description)
910 if not task['annotations']:
1011 return (width, colorized_description)
00 import unicodedata
11 from vit.formatter import Marker
2 from vit.util import unicode_len
23
34 class Markers(Marker):
45 def format(self, _, task):
3536 def add_label(self, color, label, width, text_markup):
3637 if self.color_required(color) or not label:
3738 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)
3940 text_markup += [(color, label)]
4041 return width, text_markup
4142
00 from vit.formatter import String
1 from vit.util import unicode_len
12
23 class Project(String):
34 def __init__(self, column, report, defaults, blocking_task_uuids, **kwargs):
89 return self.format_project(project, task) if project else self.markup_none(self.colorizer.project_none())
910
1011 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))
1213
1314 def format_subproject_indented(self, project, task):
1415 parts = project.split('.')
00 from vit import util
11 from vit.formatter.project import Project
2 from vit.util import unicode_len
23
34 class ProjectParent(Project):
45 def format(self, project, task):
56 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())
78
00 from vit.formatter import Formatter
1 from vit.util import unicode_len
12
23 class Tags(Formatter):
34 def format(self, tags, task):
56 return self.markup_none(self.colorizer.tag_none())
67 elif len(tags) == 1:
78 tag = list(tags)[0]
8 return (len(tag), self.markup_element(tag))
9 return (unicode_len(tag), self.markup_element(tag))
910 else:
1011 last_tag = list(tags)[-1]
1112 width = 0
1213 text_markup = []
1314 for tag in tags:
14 width += len(tag)
15 width += unicode_len(tag)
1516 text_markup += [self.markup_element(tag)]
1617 if tag != last_tag:
1718 width += 1
00 import datetime
11 from vit.formatter import DateTime
2 from vit.util import unicode_len
23
34 # TODO: Remove this once tasklib bug is fixed.
45 from tasklib.serializing import SerializingObject
1213 # https://github.com/robgolding/tasklib/issues/30
1314 dt = dt if isinstance(dt, datetime.datetime) else serializer.timestamp_deserializer(dt)
1415 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))
1617 def colorize(self, dt=None):
1718 return self.colorizer.uda_date(self.column, dt)
00 from vit.formatter import String
1 from vit.util import unicode_len
12
23 class UdaDuration(String):
34 def format(self, duration, task):
45 if not duration:
56 return self.markup_none(self.colorize())
6 return (len(duration), self.markup_element(duration))
7 return (unicode_len(duration), self.markup_element(duration))
78 def colorize(self, duration=None):
89 return self.colorizer.uda_duration(self.column, duration)
00 from vit.formatter import Formatter
1 from vit.util import unicode_len
12
23 class UdaIndicator(Formatter):
34 def format(self, value, task):
56 return self.markup_none(self.colorize())
67 else:
78 indicator = self.formatter.indicator_uda[self.column]
8 return (len(indicator), (self.colorize(value), indicator))
9 return (unicode_len(indicator), (self.colorize(value), indicator))
910
1011 def colorize(self, value=None):
1112 return self.colorizer.uda_indicator(self.column, value)
00 from vit.formatter import Number
1 from vit.util import unicode_len
12
23 class UdaNumeric(Number):
34 def format(self, number, task):
45 if number is None:
56 return self.markup_none(self.colorize())
67 number = str(number)
7 return (len(number), self.markup_element(number))
8 return (unicode_len(number), self.markup_element(number))
89 def colorize(self, number=None):
910 return self.colorizer.uda_numeric(self.column, number)
00 from vit.formatter import String
1 from vit.util import unicode_len
12
23 class UdaString(String):
34 def format(self, string, task):
45 if not string:
56 return self.markup_none(self.colorize())
6 return (len(string), self.markup_element(string))
7 return (unicode_len(string), self.markup_element(string))
78 def colorize(self, string=None):
89 return self.colorizer.uda_string(self.column, string)
66
77 from vit import util
88 from vit import uda
9 from vit.util import unicode_len
910
1011 INDICATORS = [
1112 'active',
1718
1819 DEFAULT_DESCRIPTION_TRUNCATE_LEN=20
1920
20 class FormatterBase(object):
21 class FormatterBase:
2122 def __init__(self, loader, config, task_config, markers, task_colorizer):
2223 self.loader = loader
2324 self.config = config
8889 def format_subproject_indented(self, project_parts):
8990 if len(project_parts) == 1:
9091 subproject = project_parts[0]
91 return (len(subproject), '', '', subproject)
92 return (unicode_len(subproject), '', '', subproject)
9293 else:
9394 subproject = project_parts.pop()
9495 space_padding = (len(project_parts) * 2) - 1
9596 indicator = u'\u21aa '
96 width = space_padding + len(indicator) + len(subproject)
97 width = space_padding + unicode_len(indicator) + unicode_len(subproject)
9798 return (width, ' ' * space_padding , indicator, subproject)
9899
99100 def recalculate_due_datetimes(self):
22 import urwid
33
44 from vit.base_list_box import BaseListBox
5 from vit.util import unicode_len
56
67 CURLY_BRACES_REGEX = re.compile("[{}]")
78 SPECIAL_KEY_SUBSTITUTIONS = {
5657 'keys': 0,
5758 }
5859 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])
6162 if type_len > column_widths['type']:
6263 column_widths['type'] = type_len
6364 if keys_len > column_widths['keys']:
7778 def eat_other_keybindings(self):
7879 return True
7980
80 class Help(object):
81 class Help:
8182 """Generates help list/display.
8283 """
8384 def __init__(self, keybinding_parser, actions, event=None, request_reply=None, action_manager=None):
22 class KeyCacheError(Exception):
33 pass
44
5 class KeyCache(object):
5 class KeyCache:
66 def __init__(self, keybindings):
77 self.keybindings = keybindings
88 self.cached_keys = ''
2222 class KeybindingError(Exception):
2323 pass
2424
25 class KeybindingParser(object):
25 class KeybindingParser:
2626 def __init__(self, loader, config, action_registry):
2727 self.loader = loader
2828 self.config = config
22 class ListBatchError(Exception):
33 pass
44
5 class ListBatcher(object):
5 class ListBatcher:
66 def __init__(self, batch_from, batch_to, batch_to_formatter=None, default_batch_size=DEFAULT_BATCH_SIZE):
77 self.batch_from = batch_from
88 self.batch_to = batch_to
77
88 DEFAULT_VIT_DIR = '~/.vit'
99
10 class Loader(object):
10 class Loader:
1111 def __init__(self):
1212 self.user_config_dir = os.path.expanduser('VIT_DIR' in env.user and env.user['VIT_DIR'] or DEFAULT_VIT_DIR)
1313
3232 'until.label': '(U)',
3333 }
3434
35 class Markers(object):
35 class Markers:
3636 def __init__(self, config, task_config):
3737 self.config = config
3838 self.task_config = task_config
0 import glob
01 import sys
12 import argparse
23
3334 action="store_true",
3435 help="list all available actions",
3536 )
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 )
3643
3744 def parse_options():
3845 options, filters = parser.parse_known_args()
3946 if options.list_actions:
4047 list_actions()
4148 sys.exit(0)
49 elif options.list_pids:
50 ret = list_pids()
51 sys.exit(ret)
4252 return options, filters
4353
4454 def format_dictionary_list(item, description):
5262 actions = Actions(action_registry)
5363 actions.register()
5464 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)
77
88 DEFAULT_CONFIRM = 'Press Enter to continue...'
99
10 class Command(object):
10 class Command:
1111
1212 def __init__(self, config):
1313 self.config = config
00 import string
11 import re
22
3 class Readline(object):
3 class Readline:
44 def __init__(self, edit_obj):
55 self.edit_obj = edit_obj
66 word_chars = string.ascii_letters + string.digits + "_"
00 import uuid
11
2 class ActionRegistrar(object):
2 class ActionRegistrar:
33 def __init__(self, registry):
44 self.registry = registry
55 self.uuid = uuid.uuid4()
1616 def actions(self):
1717 return self.registry.get_registered(self.uuid)
1818
19 class ActionRegistry(object):
19 class ActionRegistry:
2020 def __init__(self):
2121 self.actions = {}
2222 self.noop_action_name = 'NOOP'
4747 def noop(self):
4848 pass
4949
50 class RequestReply(object):
50 class RequestReply:
5151 def __init__(self):
5252 self.handlers = {}
5353
77 from vit import util
88 from vit.exception import VitException
99
10 class TaskListModel(object):
10 class TaskListModel:
1111 def __init__(self, task_config, reports, report=None, data_location=None):
1212
1313 if not data_location:
1212 from vit.base_list_box import BaseListBox
1313 from vit.list_batcher import ListBatcher
1414 from vit.formatter.project import Project as ProjectFormatter
15 from vit.util import unicode_len
16
1517
1618 REDUCE_COLUMN_WIDTH_LIMIT = 20
1719 COLUMN_PADDING = 2
1820 MARKER_COLUMN_NAME = 'markers'
1921
20 class TaskTable(object):
22 class TaskTable:
2123
2224 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):
2325 self.config = config
150152 position = self.listbox.focus_position
151153 self.list_walker[position].row.set_attr_map({None: attr})
152154
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()
154160 if self.listbox.focus:
155161 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:
157163 self.update_focus_attr('flash on', position)
158164 self.draw_screen()
159165 for i in repeat(None, repeat_times):
247253 if isinstance(formatted_value, tuple):
248254 return formatted_value
249255 else:
250 width = len(formatted_value) if formatted_value else 0
256 width = unicode_len(formatted_value) if formatted_value else 0
251257 return width, formatted_value
252258
253259 def subproject_indentable(self):
328334
329335 def reconcile_column_width_for_label(self):
330336 for idx, column in enumerate(self.columns):
331 label_len = len(column['label'])
337 label_len = unicode_len(column['label'])
332338 if column['width'] < label_len:
333339 self.columns[idx]['width'] = label_len
334340
387393 if grew > 0:
388394 self.batcher.add(grew)
389395
390 class TaskRow():
396 class TaskRow:
391397 def __init__(self, task, data, alt_row):
392398 self.task = task
393399 self.data = data
395401 self.uuid = self.task['uuid']
396402 self.id = self.task['id']
397403
398 class ProjectRow():
404 class ProjectRow:
399405 def __init__(self, project, placeholder, alt_row):
400406 self.project = project
401407 self.placeholder = placeholder
22 import curses
33 import shlex
44 from functools import reduce
5
6 from urwid.str_util import calc_width
57
68 curses.setupterm()
79 e3_seq = curses.tigetstr('E3') or b''
5456
5557 def file_readable(filepath):
5658 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))
0 VIT = '2.2.0'
0 VIT = '2.3.0'