Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package vit for openSUSE:Factory checked in 
at 2023-04-24 22:31:19
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/vit (Old)
 and      /work/SRC/openSUSE:Factory/.vit.new.1533 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "vit"

Mon Apr 24 22:31:19 2023 rev:8 rq:1082340 version:2.3.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/vit/vit.changes  2022-05-25 20:34:46.436245368 
+0200
+++ /work/SRC/openSUSE:Factory/.vit.new.1533/vit.changes        2023-04-24 
22:31:30.739617306 +0200
@@ -1,0 +2,20 @@
+Mon Apr 24 03:35:41 UTC 2023 - Mia Herkt <m...@0x0.st>
+
+- Update to 2.3.0
+  * Allow flash configuration
+  * correctly calculate text width of full-width characters
+  * Fix required minimum Python version
+  * add documentation for auto-refresh configuration
+  * place IS_VIT_INSTANCE into environ earlier
+  * example hook for intelligent VIT refresh
+  * sample script to externally refresh VIT instances
+  * add Bash function example for vit wrapper
+  * add pid_dir option to [vit] section
+    See sample config.ini for details on usage
+  * add basic signal support
+    + SIGUSR1: refresh (equivalent to hitting refresh key in VIT)
+    + SIGTERM/SIGINT/SIGQUIT: quit VIT cleanly
+  * more user-friendly error message for unsupported color
+    definitions
+
+-------------------------------------------------------------------

Old:
----
  vit-2.2.0.tar.gz

New:
----
  vit-2.3.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ vit.spec ++++++
--- /var/tmp/diff_new_pack.fVgHij/_old  2023-04-24 22:31:32.255626317 +0200
+++ /var/tmp/diff_new_pack.fVgHij/_new  2023-04-24 22:31:32.263626365 +0200
@@ -1,7 +1,7 @@
 #
 # spec file for package vit
 #
-# Copyright (c) 2022 SUSE LLC
+# Copyright (c) 2023 SUSE LLC
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -17,7 +17,7 @@
 
 
 Name:           vit
-Version:        2.2.0
+Version:        2.3.0
 Release:        0
 Summary:        Visual Interactive Taskwarrior full-screen terminal interface
 License:        MIT

++++++ vit-2.2.0.tar.gz -> vit-2.3.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vit-2.2.0/CUSTOMIZE.md new/vit-2.3.0/CUSTOMIZE.md
--- old/vit-2.2.0/CUSTOMIZE.md  2021-11-04 14:51:45.000000000 +0100
+++ new/vit-2.3.0/CUSTOMIZE.md  2023-04-14 06:10:49.000000000 +0200
@@ -94,7 +94,7 @@
 
 ```python
 # keybinding/keybinding.py
-class Keybinding(object):
+class Keybinding:
     def replacements(self):
         def _custom_match(variable):
             if variable == 'TEST':
@@ -140,3 +140,33 @@
 Now, for example, to jump to a task whose ID is 42, you need to press `4`, `2`
 and `<Enter>`, instead of `:`, `4`, `2` and `<Enter>`.
 This saves a `:` keypress whenever jumping to a task.
+
+### Auto-refreshing VIT's interface
+
+*Note: Windows unfortunately does not support the `SIGUSR1` signal, so this 
feature is not currently available in that environment.*
+
+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.
+
+However, VIT provides some basic mechanisms that, when combined, allow for an 
easy implementation of an auto-refreshing interface:
+
+* **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
+* **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
+* **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
+
+#### Refresh all VIT instances example
+
+[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:
+
+* The script is executable, and in your `PATH` environment variable
+* You've properly set `pid_dir` in `config.ini`
+
+#### Refresh VIT when TaskWarrior updates a task
+
+[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:
+
+* The script is executable, [named 
properly](https://taskwarrior.org/docs/hooks.html), and placed in TaskWarrior's 
hooks directory, usually `.task/hooks`
+* [vit-external-refresh.sh](scripts/vit-external-refresh.sh) is configured 
correctly as above
+
+#### Other refresh scenarios
+
+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.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vit-2.2.0/DEVELOPMENT.md new/vit-2.3.0/DEVELOPMENT.md
--- old/vit-2.2.0/DEVELOPMENT.md        2020-12-24 04:52:45.000000000 +0100
+++ new/vit-2.3.0/DEVELOPMENT.md        2022-04-28 04:43:50.000000000 +0200
@@ -6,8 +6,17 @@
 4. ```vit/command_line.py``` is the entry point for the application. To run it 
without a full installation:
     * Set the ```PYTHONPATH``` environment variable to the root directory of 
the repository
     * Run it with ```python vit/command_line.py```
-    * A snazzier option is to create a command line alias. For bash:
-        * ```alias vit='PYTHONPATH=[path_to_root_dir] python 
vit/command_line.py'```
+    * A snazzier option is to create a command line alias. For Bash:
+      ```bash
+      alias vit='PYTHONPATH=[path_to_root_dir] python vit/command_line.py'
+      ```
+    * ...or a shell function. For Bash:
+      ```bash
+      vit() {
+        cd ~/git/vit && PYTHONPATH=${HOME}/git/vit python vit/command_line.py 
"$@"
+      }
+      export -f vit
+      ```
 
 ### Tests
  * Located in the ```tests``` directory
@@ -37,6 +46,28 @@
  * 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
  * 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
 
+### Release checklist
+
+For any developer managing VIT releases, here's a simple checklist:
+
+#### Pre-release
+
+ * Check `requirements.txt`, and bump any dependencies if necessary
+ * Check `setup.py`, and bump the minimum Python version if the current 
version is no longer supported
+
+#### Release
+
+ * Bump the release number in `vit/version.py`
+ * Generate changelog entries using `scripts/generate-changelog-entries.sh`
+ * Add changelog entries to `CHANGES.md`
+ * Commit
+ * Add the proper git tag and push it
+ * Create a Github release for the tag, use the generated changelog entries in 
the description
+ * Build and publish PyPi releases using `scripts/release-pypi.sh`
+
+#### Post-release
+
+ * Announce on all relevant channels
 
 ### Roadmap
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vit-2.2.0/PKG-INFO new/vit-2.3.0/PKG-INFO
--- old/vit-2.2.0/PKG-INFO      2022-04-18 01:07:49.000000000 +0200
+++ new/vit-2.3.0/PKG-INFO      2023-04-14 06:27:09.153355400 +0200
@@ -1,13 +1,11 @@
 Metadata-Version: 2.1
 Name: vit
-Version: 2.2.0
+Version: 2.3.0
 Summary: Visual Interactive Taskwarrior full-screen terminal interface
 Home-page: https://github.com/vit-project/vit
 Author: Chad Phillips
 Author-email: c...@apartmentlines.com
-License: UNKNOWN
 Keywords: taskwarrior,console,tui,text-user-interface
-Platform: UNKNOWN
 Classifier: Development Status :: 5 - Production/Stable
 Classifier: License :: OSI Approved :: MIT License
 Classifier: Environment :: Console
@@ -43,7 +41,7 @@
 ## Requirements
 
  * [Taskwarrior](https://taskwarrior.org)
- * [Python](https://www.python.org) 3.5+
+ * [Python](https://www.python.org) 3.7+
  * [pip](https://pypi.org/project/pip)
 
 ## Installation
@@ -85,5 +83,3 @@
 
  Taskwarrior Team
  supp...@taskwarrior.org
-
-
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vit-2.2.0/README.md new/vit-2.3.0/README.md
--- old/vit-2.2.0/README.md     2021-11-04 14:51:57.000000000 +0100
+++ new/vit-2.3.0/README.md     2022-05-09 16:29:06.000000000 +0200
@@ -22,7 +22,7 @@
 ## Requirements
 
  * [Taskwarrior](https://taskwarrior.org)
- * [Python](https://www.python.org) 3.5+
+ * [Python](https://www.python.org) 3.7+
  * [pip](https://pypi.org/project/pip)
 
 ## Installation
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vit-2.2.0/setup.py new/vit-2.3.0/setup.py
--- old/vit-2.2.0/setup.py      2022-04-18 00:54:13.000000000 +0200
+++ new/vit-2.3.0/setup.py      2023-04-14 06:10:49.000000000 +0200
@@ -5,7 +5,7 @@
 DEFAULT_BRANCH = "2.x"
 BASE_GITHUB_URL = "https://github.com/vit-project/vit/blob";
 
-MARKUP_LINK_REGEX = "\[([^]]+)\]\(([\w]+\.md)\)"
+MARKUP_LINK_REGEX = r"\[([^]]+)\]\(([\w]+\.md)\)"
 FILE_DIR = path.dirname(path.abspath(path.realpath(__file__)))
 
 with open(path.join(FILE_DIR, 'README.md')) as f:
@@ -21,7 +21,7 @@
 
 setup(
     name='vit',
-    packages=['vit', 'vit.formatter', 'vit.theme'],
+    packages=['vit', 'vit.config', 'vit.formatter', 'vit.keybinding', 
'vit.theme'],
     description="Visual Interactive Taskwarrior full-screen terminal 
interface",
     long_description=README,
     long_description_content_type='text/markdown',
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vit-2.2.0/vit/action_manager.py 
new/vit-2.3.0/vit/action_manager.py
--- old/vit-2.2.0/vit/action_manager.py 2021-11-22 15:10:12.000000000 +0100
+++ new/vit-2.3.0/vit/action_manager.py 2023-04-14 06:10:49.000000000 +0200
@@ -1,6 +1,6 @@
 import uuid
 
-class ActionManagerRegistrar(object):
+class ActionManagerRegistrar:
     def __init__(self, registry):
         self.registry = registry
         self.uuid = uuid.uuid4()
@@ -37,7 +37,7 @@
             return actions[keybindings[keys]['action_name']]
         return None
 
-class ActionManagerRegistry(object):
+class ActionManagerRegistry:
     def __init__(self, action_registry, keybindings, event=None):
         self.actions = {}
         self.action_registry = action_registry
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vit-2.2.0/vit/actions.py new/vit-2.3.0/vit/actions.py
--- old/vit-2.2.0/vit/actions.py        2021-11-04 14:51:57.000000000 +0100
+++ new/vit-2.3.0/vit/actions.py        2023-04-14 06:10:49.000000000 +0200
@@ -1,4 +1,4 @@
-class Actions(object):
+class Actions:
 
     def __init__(self, action_registry):
         self.action_registry = action_registry
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vit-2.2.0/vit/application.py 
new/vit-2.3.0/vit/application.py
--- old/vit-2.2.0/vit/application.py    2021-11-22 15:12:28.000000000 +0100
+++ new/vit-2.3.0/vit/application.py    2023-04-14 06:10:49.000000000 +0200
@@ -2,6 +2,8 @@
 
 from importlib import import_module
 
+import os
+import signal
 import subprocess
 # TODO: Use regex module for better PCRE support?
 #       https://bitbucket.org/mrabarnett/mrab-regex
@@ -35,6 +37,7 @@
 from vit.registry import ActionRegistry, RequestReply
 from vit.action_manager import ActionManagerRegistry
 from vit.denotation import DenotationPopupLauncher
+from vit.pid_manager import PidManager
 
 # NOTE: This entire class is a workaround for the fact that urwid catches the
 # 'ctrl l' keypress in its unhandled_input code, and prevents that from being
@@ -67,16 +70,39 @@
         else:
             return super().keypress(size, key)
 
-class Application():
+class Application:
     def __init__(self, option, filters):
         self.extra_filters = filters
         self.loader = Loader()
         self.load_early_config()
         self.set_report()
         self.setup_main_loop()
+        self.setup_signal_listeners()
         self.refresh(False)
+        self.setup_pid()
         self.loop.run()
 
+    def setup_signal_listeners(self):
+        # Since not all platforms may have all signals, ensure they are
+        # supported before adding a handler.
+        if hasattr(signal, 'SIGUSR1'):
+            pipe = self.loop.watch_pipe(self.async_refresh)
+            def sigusr1_handler(signum, frame):
+                os.write(pipe, b'x')
+            signal.signal(signal.SIGUSR1, sigusr1_handler)
+        if hasattr(signal, 'SIGTERM'):
+            def sigterm_handler(signum, frame):
+                self.signal_quit("SIGTERM")
+            signal.signal(signal.SIGTERM, sigterm_handler)
+        if hasattr(signal, 'SIGINT'):
+            def sigint_handler(signum, frame):
+                self.signal_quit("SIGINT")
+            signal.signal(signal.SIGINT, sigint_handler)
+        if hasattr(signal, 'SIGQUIT'):
+            def sigquit_handler(signum, frame):
+                self.signal_quit("SIGQUIT")
+            signal.signal(signal.SIGQUIT, sigquit_handler)
+
     def load_early_config(self):
         self.config = ConfigParser(self.loader)
         self.task_config = TaskParser(self.config)
@@ -97,6 +123,13 @@
         except:
             pass
 
+    def setup_pid(self):
+        self.pid_manager = PidManager(self.config)
+        self.pid_manager.setup()
+
+    def teardown_pid(self):
+        self.pid_manager.teardown()
+
     def set_active_context(self):
         self.context = self.task_config.get_active_context()
 
@@ -183,7 +216,7 @@
     def default_keybinding_replacements(self):
         import json
         from datetime import datetime
-        task_replacement_match = re.compile("^TASK_(\w+)$")
+        task_replacement_match = re.compile(r"^TASK_(\w+)$")
         def _task_attribute_match(variable):
             matches = re.match(task_replacement_match, variable)
             if matches:
@@ -509,20 +542,21 @@
         rows = self.table.rows
         current_index = start_index
         last_index = len(rows) - 1
-        start_matches = self.search_row_has_search_term(rows[start_index], 
search_regex)
-        current_index = self.search_increment_index(current_index, reverse)
-        while True:
-            if reverse and current_index < 0:
-                self.search_loop_warning('TOP', reverse)
-                current_index = last_index
-            elif not reverse and current_index > last_index:
-                self.search_loop_warning('BOTTOM', reverse)
-                current_index = 0
-            if self.search_row_has_search_term(rows[current_index], 
search_regex):
-                return current_index
-            if current_index == start_index:
-                return start_index if start_matches else None
+        if len(rows) > 0:
+            start_matches = self.search_row_has_search_term(rows[start_index], 
search_regex)
             current_index = self.search_increment_index(current_index, reverse)
+            while True:
+                if reverse and current_index < 0:
+                    self.search_loop_warning('TOP', reverse)
+                    current_index = last_index
+                elif not reverse and current_index > last_index:
+                    self.search_loop_warning('BOTTOM', reverse)
+                    current_index = 0
+                if self.search_row_has_search_term(rows[current_index], 
search_regex):
+                    return current_index
+                if current_index == start_index:
+                    return start_index if start_matches else None
+                current_index = self.search_increment_index(current_index, 
reverse)
 
     def search_increment_index(self, current_index, reverse=False):
         return current_index + (-1 if reverse else 1)
@@ -564,7 +598,13 @@
             pass
         return False, False
 
+    def signal_quit(self, signal):
+        #import debug
+        #debug.file("VIT received %s signal, quitting" % signal)
+        self.quit()
+
     def quit(self):
+        self.teardown_pid()
         raise urwid.ExitMainLoop()
 
     def build_task_table(self):
@@ -916,6 +956,9 @@
         else:
             raise RuntimeError("Error retrieving completed tasks: %s" % stderr)
 
+    def async_refresh(self, _):
+        self.refresh()
+
     def refresh(self, load_early_config=True):
         self.bootstrap(load_early_config)
         self.build_main_widget()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vit-2.2.0/vit/autocomplete.py 
new/vit-2.3.0/vit/autocomplete.py
--- old/vit-2.2.0/vit/autocomplete.py   2021-11-04 14:51:45.000000000 +0100
+++ new/vit-2.3.0/vit/autocomplete.py   2023-04-14 06:10:49.000000000 +0200
@@ -5,7 +5,7 @@
 from vit import util
 from vit.process import Command
 
-class AutoComplete(object):
+class AutoComplete:
 
     def __init__(self, config, default_filters=None, extra_filters=None):
         self.default_filters = default_filters or ('column', 'project', 'tag')
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vit-2.2.0/vit/base_list_box.py 
new/vit-2.3.0/vit/base_list_box.py
--- old/vit-2.2.0/vit/base_list_box.py  2020-12-24 04:52:45.000000000 +0100
+++ new/vit-2.3.0/vit/base_list_box.py  2022-10-26 15:33:43.000000000 +0200
@@ -71,11 +71,13 @@
         self.keypress(size, '<Page Down>')
 
     def keypress_home(self, size):
-        self.set_focus(0)
+        if len(self.body) > 0:
+            self.set_focus(0)
 
     def keypress_end(self, size):
-        self.set_focus(len(self.body) - 1)
-        self.set_focus_valign('bottom')
+        if len(self.body) > 0:
+            self.set_focus(len(self.body) - 1)
+            self.set_focus_valign('bottom')
 
     def keypress_screen_top(self, size):
         top, _, _ = self.get_top_middle_bottom_rows(size)
@@ -93,8 +95,9 @@
             self.set_focus(bottom.position)
 
     def keypress_focus_valign_center(self, size):
-        self.set_focus(self.focus_position)
-        self.set_focus_valign('middle')
+        if len(self.body) > 0:
+            self.set_focus(self.focus_position)
+            self.set_focus_valign('middle')
 
     def transform_special_keys(self, key):
         # NOTE: These are special key presses passed to allow navigation
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vit-2.2.0/vit/color.py new/vit-2.3.0/vit/color.py
--- old/vit-2.2.0/vit/color.py  2020-12-24 04:52:45.000000000 +0100
+++ new/vit-2.3.0/vit/color.py  2023-04-14 06:10:49.000000000 +0200
@@ -9,7 +9,11 @@
     'underline',
 ]
 
-class TaskColorConfig(object):
+INVALID_COLOR_MODIFIERS = [
+    'inverse',
+]
+
+class TaskColorConfig:
     """Colorized task output.
     """
     def __init__(self, config, task_config, theme, theme_alt_backgrounds):
@@ -24,7 +28,7 @@
         # without pipes, the 'color' config setting in Taskwarrior is not 
used, and
         # instead a custom setting is used.
         self.color_enabled = self.config.get('color', 'enabled')
-        self.display_attrs_available, self.display_attrs = 
self.convert_color_config(self.task_config.filter_to_dict('^color\.'))
+        self.display_attrs_available, self.display_attrs = 
self.convert_color_config(self.task_config.filter_to_dict(r'^color\.'))
         self.project_display_attrs = self.get_project_display_attrs()
         if self.include_subprojects:
             self.add_project_children()
@@ -96,6 +100,11 @@
         remapped_colors = self.map_named_colors(sorted_parts)
         return ','.join(remapped_colors)
 
+    def check_invalid_color_parts(self, color_parts):
+        invalid_color_parts = {*color_parts} & {*INVALID_COLOR_MODIFIERS}
+        if invalid_color_parts:
+            raise ValueError("The following TaskWarrior color definitions are 
unsupported in VIT: %s -- read the documentation for possible workarounds" % ", 
".join(invalid_color_parts))
+
     def map_named_colors(self, color_parts):
         if len(color_parts) > 0 and color_parts[0] in 
self.task_256_to_urwid_256:
             color_parts[0] = self.task_256_to_urwid_256[color_parts[0]]
@@ -103,7 +112,9 @@
 
     def make_color_parts(self, foreground, background):
         foreground_parts = self.split_color_parts(foreground)
+        self.check_invalid_color_parts(foreground_parts)
         background_parts = self.split_color_parts(background)
+        self.check_invalid_color_parts(background_parts)
         return foreground_parts, background_parts
 
     def split_color_parts(self, color_parts):
@@ -123,8 +134,8 @@
                 return 0
         return sorted(color_parts, key=cmp_to_key(comparator))
 
-class TaskColorizer(object):
-    class Decorator(object):
+class TaskColorizer:
+    class Decorator:
         def color_enabled(func):
             @wraps(func)
             def verify_color_enabled(self, *args, **kwargs):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vit-2.2.0/vit/command_bar.py 
new/vit-2.3.0/vit/command_bar.py
--- old/vit-2.2.0/vit/command_bar.py    2020-12-28 18:26:46.000000000 +0100
+++ new/vit-2.3.0/vit/command_bar.py    2023-04-14 06:10:49.000000000 +0200
@@ -95,7 +95,7 @@
     def set_edit_text_callback(self):
         return self.set_edit_text
 
-class CommandBarHistory(object):
+class CommandBarHistory:
     """Holds command-specific history for the command bar.
     """
     def __init__(self):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vit-2.2.0/vit/config/config.sample.ini 
new/vit-2.3.0/vit/config/config.sample.ini
--- old/vit-2.2.0/vit/config/config.sample.ini  2021-11-04 14:51:57.000000000 
+0100
+++ new/vit-2.3.0/vit/config/config.sample.ini  2022-10-21 18:59:04.000000000 
+0200
@@ -57,6 +57,24 @@
 #included in the active filter for this setting to have effect.
 #focus_on_add = False
 
+# Path to a directory to manage pid files for running instances of VIT.
+# If no path is provided, no pid files will be managed.
+# The special token $UID may be used, and will be substituted with the user ID
+# of the user starting VIT.
+# VIT can be run with the '--list-pids' argument, which will output a list of
+# all pids in pid_dir; useful for sending signals to the running processes.
+# If you use this feature, it's suggested to choose a directory that is
+# automatically cleaned on boot, e.g.:
+#   /var/run/user/$UID/vit
+#   /tmp/vit_pids
+#pid_dir =
+
+# Int. The number of flash repetitions focusing on the edit made
+#flash_focus_repeat_times = 2
+
+# Float. Waiting time for the blink focusing on the edit made
+#flash_focus_pause_seconds = 0.1
+
 [report]
 
 # The default Taskwarrior report to load when VIT first starts, if no report
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vit-2.2.0/vit/config_parser.py 
new/vit-2.3.0/vit/config_parser.py
--- old/vit-2.2.0/vit/config_parser.py  2022-03-06 02:27:52.000000000 +0100
+++ new/vit-2.3.0/vit/config_parser.py  2023-04-14 06:10:49.000000000 +0200
@@ -18,9 +18,9 @@
 SORT_ORDER_CHARACTERS = ['+', '-']
 SORT_COLLATE_CHARACTERS = ['/']
 VIT_CONFIG_FILE = 'config.ini'
-FILTER_EXCLUSION_REGEX = re.compile('^limit:')
-FILTER_PARENS_REGEX = re.compile('([\(\)])')
-CONFIG_BOOLEAN_TRUE_REGEX = re.compile('1|yes|true', re.IGNORECASE)
+FILTER_EXCLUSION_REGEX = re.compile(r'^limit:')
+FILTER_PARENS_REGEX = re.compile(r'([\(\)])')
+CONFIG_BOOLEAN_TRUE_REGEX = re.compile(r'1|yes|true', re.IGNORECASE)
 # TaskParser expects clean hierarchies in the Taskwarrior dotted config names.
 # However, this is occasionally violated, with a leaf ending in both a string
 # value and another branch. The below list contains the config values that
@@ -46,6 +46,9 @@
         'mouse': False,
         'abort_backspace': False,
         'focus_on_add': False,
+        'pid_dir': '',
+        'flash_focus_repeat_times': 2,
+        'flash_focus_pause_seconds': 0.1,
     },
     'report': {
         'default_report': 'next',
@@ -94,7 +97,7 @@
     'J': '%j',                           # 3 digit day of year number, 
sometimes referred to as a Julian date, eg '001', '011', or '365'
 }
 
-class ConfigParser(object):
+class ConfigParser:
     def __init__(self, loader):
         self.loader = loader
         self.config = configparser.SafeConfigParser()
@@ -165,7 +168,7 @@
         try:
             value = self.config.get(section, key)
             return self.transform(key, value, default)
-        except (configparser.NoSectionError, configparser.NoOptionError):
+        except (configparser.NoSectionError, configparser.NoOptionError, 
ValueError):
             return default
 
     def items(self, section):
@@ -180,12 +183,22 @@
     def transform(self, key, value, default):
         if isinstance(default, bool):
             return self.transform_bool(value)
+        elif isinstance(default, int):
+            return self.transform_int(value)
+        elif isinstance(default, float):
+            return self.transform_float(value)
         else:
             return value
 
     def transform_bool(self, value):
         return True if CONFIG_BOOLEAN_TRUE_REGEX.match(value) else False
 
+    def transform_int(self, value):
+        return int(value)
+
+    def transform_float(self, value):
+        return float(value)
+
     def get_taskrc_path(self):
         taskrc_path = os.path.expanduser('TASKRC' in env.user and 
env.user['TASKRC'] or self.get('taskwarrior', 'taskrc'))
 
@@ -211,7 +224,13 @@
     def is_mouse_enabled(self):
         return self.get('vit', 'mouse')
 
-class TaskParser(object):
+    def get_flash_focus_repeat_times(self):
+        return self.get('vit', 'flash_focus_repeat_times')
+
+    def get_flash_focus_pause_seconds(self):
+        return self.get('vit', 'flash_focus_pause_seconds')
+
+class TaskParser:
     def __init__(self, config):
         self.config = config
         self.task_config = []
@@ -275,7 +294,7 @@
     def subtree(self, matcher, walk_subtree=True):
         matcher_regex = matcher
         if walk_subtree:
-            matcher_regex = r'%s' % (('^%s' % matcher).replace('.', '\.'))
+            matcher_regex = r'%s' % (('^%s' % matcher).replace(r'.', r'\.'))
         full_tree = {}
         lines = self.filter(matcher_regex)
         for (hierarchy, value) in lines:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vit-2.2.0/vit/env.py new/vit-2.3.0/vit/env.py
--- old/vit-2.2.0/vit/env.py    2020-12-24 04:52:45.000000000 +0100
+++ new/vit-2.3.0/vit/env.py    2022-04-28 21:54:53.000000000 +0200
@@ -1,3 +1,5 @@
 import os
 
+os.environ['IS_VIT_INSTANCE'] = "1"
+
 user = os.environ.copy()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vit-2.2.0/vit/event.py new/vit-2.3.0/vit/event.py
--- old/vit-2.2.0/vit/event.py  2020-12-24 04:52:45.000000000 +0100
+++ new/vit-2.3.0/vit/event.py  2023-04-14 06:10:49.000000000 +0200
@@ -1,5 +1,5 @@
 # TODO: Use urwid signals instead of custom event emitter?
-class Emitter(object):
+class Emitter:
     """Simple event listener/emitter.
     """
     def __init__(self):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vit-2.2.0/vit/formatter/__init__.py 
new/vit-2.3.0/vit/formatter/__init__.py
--- old/vit-2.2.0/vit/formatter/__init__.py     2022-03-23 00:27:36.000000000 
+0100
+++ new/vit-2.3.0/vit/formatter/__init__.py     2023-04-14 06:10:49.000000000 
+0200
@@ -5,6 +5,8 @@
 except ImportError:
     from backports.zoneinfo import ZoneInfo
 
+from vit.util import unicode_len
+
 TIME_UNIT_MAP = {
     'seconds': {
         'label': 's',
@@ -42,7 +44,7 @@
      },
 }
 
-class Formatter(object):
+class Formatter:
     def __init__(self, column, report, formatter_base, blocking_task_uuids, 
**kwargs):
         self.column = column
         self.report = report
@@ -54,7 +56,7 @@
         if not obj:
             return self.empty()
         obj = str(obj)
-        return (len(obj), self.markup_element(obj))
+        return (unicode_len(obj), self.markup_element(obj))
 
     def empty(self):
         return (0, '')
@@ -64,7 +66,7 @@
 
     def markup_none(self, color):
         if color:
-            return (len(self.formatter.none_label), (color, 
self.formatter.none_label))
+            return (unicode_len(self.formatter.none_label), (color, 
self.formatter.none_label))
         else:
             return self.empty()
 
@@ -97,7 +99,7 @@
         if not obj:
             return self.empty()
         formatted_duration = self.format_duration(obj)
-        return (len(formatted_duration), self.markup_element(obj, 
formatted_duration))
+        return (unicode_len(formatted_duration), self.markup_element(obj, 
formatted_duration))
 
     def format_duration(self, obj):
         return obj
@@ -114,7 +116,7 @@
         if not dt:
             return self.empty()
         formatted_date = self.format_datetime(dt, task)
-        return (len(formatted_date), self.markup_element(dt, formatted_date, 
task))
+        return (unicode_len(formatted_date), self.markup_element(dt, 
formatted_date, task))
 
     def format_datetime(self, dt, task):
         return dt.strftime(self.custom_formatter or self.formatter.report)
@@ -210,7 +212,7 @@
         if not obj:
             return self.empty()
         formatted = self.format_list(obj, task)
-        return (len(formatted), self.markup_element(obj, formatted))
+        return (unicode_len(formatted), self.markup_element(obj, formatted))
 
     def markup_element(self, obj, formatted):
         return (self.colorize(obj), formatted)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vit-2.2.0/vit/formatter/description.py 
new/vit-2.3.0/vit/formatter/description.py
--- old/vit-2.2.0/vit/formatter/description.py  2020-12-24 04:52:45.000000000 
+0100
+++ new/vit-2.3.0/vit/formatter/description.py  2022-10-15 01:05:54.000000000 
+0200
@@ -1,12 +1,13 @@
 from functools import reduce
 
 from vit.formatter import String
+from vit.util import unicode_len
 
 class Description(String):
     def format(self, description, task):
         if not description:
             return self.empty()
-        width = len(description)
+        width = unicode_len(description)
         colorized_description = self.colorize_description(description)
         if task['annotations']:
             annotation_width, colorized_description = 
self.format_combined(colorized_description, task)
@@ -15,7 +16,7 @@
         return (width, colorized_description)
 
     def format_description_truncated(self, description):
-        return '%s...' % description[:self.formatter.description_truncate_len] 
if len(description) > self.formatter.description_truncate_len else description
+        return '%s...' % description[:self.formatter.description_truncate_len] 
if unicode_len(description) > self.formatter.description_truncate_len else 
description
 
     def format_combined(self, colorized_description, task):
         annotation_width, formatted_annotations = self.format_annotations(task)
@@ -25,7 +26,7 @@
         def reducer(accum, annotation):
             width, formatted_list = accum
             formatted = self.format_annotation(annotation)
-            new_width = len(formatted)
+            new_width = unicode_len(formatted)
             if new_width > width:
                 width = new_width
             formatted_list.append(formatted)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vit-2.2.0/vit/formatter/description_count.py 
new/vit-2.3.0/vit/formatter/description_count.py
--- old/vit-2.2.0/vit/formatter/description_count.py    2020-12-24 
04:52:45.000000000 +0100
+++ new/vit-2.3.0/vit/formatter/description_count.py    2022-10-15 
01:05:54.000000000 +0200
@@ -1,10 +1,11 @@
 from vit.formatter.description import Description
+from vit.util import unicode_len
 
 class DescriptionCount(Description):
     def format(self, description, task):
         if not description:
             return self.empty()
-        width = len(description)
+        width = unicode_len(description)
         colorized_description = self.colorize_description(description)
         if not task['annotations']:
             return (width, colorized_description)
@@ -14,7 +15,7 @@
 
     def format_count(self, colorized_description, task):
         count_string = self.format_annotation_count(task)
-        return len(count_string), colorized_description + [(None, 
count_string)]
+        return unicode_len(count_string), colorized_description + [(None, 
count_string)]
 
     def format_annotation_count(self, task):
         return " [%d]" % len(task['annotations'])
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vit-2.2.0/vit/formatter/description_desc.py 
new/vit-2.3.0/vit/formatter/description_desc.py
--- old/vit-2.2.0/vit/formatter/description_desc.py     2020-12-24 
04:52:45.000000000 +0100
+++ new/vit-2.3.0/vit/formatter/description_desc.py     2022-10-15 
01:05:54.000000000 +0200
@@ -1,8 +1,9 @@
 from vit.formatter.description import Description
+from vit.util import unicode_len
 
 class DescriptionDesc(Description):
     def format(self, description, task):
         if not description:
             return self.empty()
         colorized_description = self.colorize_description(description)
-        return (len(description), colorized_description)
+        return (unicode_len(description), colorized_description)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vit-2.2.0/vit/formatter/description_truncated.py 
new/vit-2.3.0/vit/formatter/description_truncated.py
--- old/vit-2.2.0/vit/formatter/description_truncated.py        2020-12-24 
04:52:45.000000000 +0100
+++ new/vit-2.3.0/vit/formatter/description_truncated.py        2022-10-15 
01:05:54.000000000 +0200
@@ -1,10 +1,11 @@
 from vit.formatter.description import Description
+from vit.util import unicode_len
 
 class DescriptionTruncated(Description):
     def format(self, description, task):
         if not description:
             return self.empty()
         truncated_description = self.format_description_truncated(description)
-        width = len(truncated_description)
+        width = unicode_len(truncated_description)
         colorized_description = 
self.colorize_description(truncated_description)
         return (width, colorized_description)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/vit-2.2.0/vit/formatter/description_truncated_count.py 
new/vit-2.3.0/vit/formatter/description_truncated_count.py
--- old/vit-2.2.0/vit/formatter/description_truncated_count.py  2020-12-24 
04:52:45.000000000 +0100
+++ new/vit-2.3.0/vit/formatter/description_truncated_count.py  2022-10-15 
01:05:54.000000000 +0200
@@ -1,11 +1,12 @@
 from vit.formatter.description_count import DescriptionCount
+from vit.util import unicode_len
 
 class DescriptionTruncatedCount(DescriptionCount):
     def format(self, description, task):
         if not description:
             return self.empty()
         truncated_description = self.format_description_truncated(description)
-        width = len(truncated_description)
+        width = unicode_len(truncated_description)
         colorized_description = 
self.colorize_description(truncated_description)
         if not task['annotations']:
             return (width, colorized_description)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vit-2.2.0/vit/formatter/markers.py 
new/vit-2.3.0/vit/formatter/markers.py
--- old/vit-2.2.0/vit/formatter/markers.py      2021-10-19 15:36:03.000000000 
+0200
+++ new/vit-2.3.0/vit/formatter/markers.py      2022-10-15 01:05:54.000000000 
+0200
@@ -1,5 +1,6 @@
 import unicodedata
 from vit.formatter import Marker
+from vit.util import unicode_len
 
 class Markers(Marker):
     def format(self, _, task):
@@ -36,7 +37,7 @@
     def add_label(self, color, label, width, text_markup):
         if self.color_required(color) or not label:
             return width, text_markup
-        width += len(label) + len([c for c in label if 
unicodedata.east_asian_width(c) == 'W'])
+        width += unicode_len(label)
         text_markup += [(color, label)]
         return width, text_markup
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vit-2.2.0/vit/formatter/project.py 
new/vit-2.3.0/vit/formatter/project.py
--- old/vit-2.2.0/vit/formatter/project.py      2020-12-24 04:52:45.000000000 
+0100
+++ new/vit-2.3.0/vit/formatter/project.py      2022-10-15 01:05:54.000000000 
+0200
@@ -1,4 +1,5 @@
 from vit.formatter import String
+from vit.util import unicode_len
 
 class Project(String):
     def __init__(self, column, report, defaults, blocking_task_uuids, 
**kwargs):
@@ -9,7 +10,7 @@
         return self.format_project(project, task) if project else 
self.markup_none(self.colorizer.project_none())
 
     def format_project(self, project, task):
-        return self.format_subproject_indented(project, task) if 
self.indent_subprojects else (len(project), self.markup_element(project))
+        return self.format_subproject_indented(project, task) if 
self.indent_subprojects else (unicode_len(project), 
self.markup_element(project))
 
     def format_subproject_indented(self, project, task):
         parts = project.split('.')
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vit-2.2.0/vit/formatter/project_parent.py 
new/vit-2.3.0/vit/formatter/project_parent.py
--- old/vit-2.2.0/vit/formatter/project_parent.py       2020-12-24 
04:52:45.000000000 +0100
+++ new/vit-2.3.0/vit/formatter/project_parent.py       2022-10-15 
01:05:54.000000000 +0200
@@ -1,8 +1,9 @@
 from vit import util
 from vit.formatter.project import Project
+from vit.util import unicode_len
 
 class ProjectParent(Project):
     def format(self, project, task):
         parent = util.project_get_root(project)
-        return (len(parent), self.markup_element(parent)) if parent else 
self.markup_none(self.colorizer.project_none())
+        return (unicode_len(parent), self.markup_element(parent)) if parent 
else self.markup_none(self.colorizer.project_none())
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vit-2.2.0/vit/formatter/tags.py 
new/vit-2.3.0/vit/formatter/tags.py
--- old/vit-2.2.0/vit/formatter/tags.py 2020-12-24 04:52:45.000000000 +0100
+++ new/vit-2.3.0/vit/formatter/tags.py 2022-10-15 01:05:54.000000000 +0200
@@ -1,4 +1,5 @@
 from vit.formatter import Formatter
+from vit.util import unicode_len
 
 class Tags(Formatter):
     def format(self, tags, task):
@@ -6,13 +7,13 @@
             return self.markup_none(self.colorizer.tag_none())
         elif len(tags) == 1:
             tag = list(tags)[0]
-            return (len(tag), self.markup_element(tag))
+            return (unicode_len(tag), self.markup_element(tag))
         else:
             last_tag = list(tags)[-1]
             width = 0
             text_markup = []
             for tag in tags:
-                width += len(tag)
+                width += unicode_len(tag)
                 text_markup += [self.markup_element(tag)]
                 if tag != last_tag:
                     width += 1
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vit-2.2.0/vit/formatter/uda_date.py 
new/vit-2.3.0/vit/formatter/uda_date.py
--- old/vit-2.2.0/vit/formatter/uda_date.py     2020-12-24 04:52:45.000000000 
+0100
+++ new/vit-2.3.0/vit/formatter/uda_date.py     2022-10-15 01:05:54.000000000 
+0200
@@ -1,5 +1,6 @@
 import datetime
 from vit.formatter import DateTime
+from vit.util import unicode_len
 
 # TODO: Remove this once tasklib bug is fixed.
 from tasklib.serializing import SerializingObject
@@ -13,6 +14,6 @@
         # https://github.com/robgolding/tasklib/issues/30
         dt = dt if isinstance(dt, datetime.datetime) else 
serializer.timestamp_deserializer(dt)
         formatted_date = dt.strftime(self.custom_formatter or 
self.formatter.report)
-        return (len(formatted_date), (self.colorize(dt), formatted_date))
+        return (unicode_len(formatted_date), (self.colorize(dt), 
formatted_date))
     def colorize(self, dt=None):
         return self.colorizer.uda_date(self.column, dt)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vit-2.2.0/vit/formatter/uda_duration.py 
new/vit-2.3.0/vit/formatter/uda_duration.py
--- old/vit-2.2.0/vit/formatter/uda_duration.py 2020-12-24 04:52:45.000000000 
+0100
+++ new/vit-2.3.0/vit/formatter/uda_duration.py 2022-10-15 01:05:54.000000000 
+0200
@@ -1,9 +1,10 @@
 from vit.formatter import String
+from vit.util import unicode_len
 
 class UdaDuration(String):
     def format(self, duration, task):
         if not duration:
             return self.markup_none(self.colorize())
-        return (len(duration), self.markup_element(duration))
+        return (unicode_len(duration), self.markup_element(duration))
     def colorize(self, duration=None):
         return self.colorizer.uda_duration(self.column, duration)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vit-2.2.0/vit/formatter/uda_indicator.py 
new/vit-2.3.0/vit/formatter/uda_indicator.py
--- old/vit-2.2.0/vit/formatter/uda_indicator.py        2020-12-24 
04:52:45.000000000 +0100
+++ new/vit-2.3.0/vit/formatter/uda_indicator.py        2022-10-15 
01:05:54.000000000 +0200
@@ -1,4 +1,5 @@
 from vit.formatter import Formatter
+from vit.util import unicode_len
 
 class UdaIndicator(Formatter):
     def format(self, value, task):
@@ -6,7 +7,7 @@
             return self.markup_none(self.colorize())
         else:
             indicator = self.formatter.indicator_uda[self.column]
-            return (len(indicator), (self.colorize(value), indicator))
+            return (unicode_len(indicator), (self.colorize(value), indicator))
 
     def colorize(self, value=None):
         return self.colorizer.uda_indicator(self.column, value)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vit-2.2.0/vit/formatter/uda_numeric.py 
new/vit-2.3.0/vit/formatter/uda_numeric.py
--- old/vit-2.2.0/vit/formatter/uda_numeric.py  2020-12-24 04:52:45.000000000 
+0100
+++ new/vit-2.3.0/vit/formatter/uda_numeric.py  2022-10-15 01:05:54.000000000 
+0200
@@ -1,10 +1,11 @@
 from vit.formatter import Number
+from vit.util import unicode_len
 
 class UdaNumeric(Number):
     def format(self, number, task):
         if number is None:
             return self.markup_none(self.colorize())
         number = str(number)
-        return (len(number), self.markup_element(number))
+        return (unicode_len(number), self.markup_element(number))
     def colorize(self, number=None):
         return self.colorizer.uda_numeric(self.column, number)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vit-2.2.0/vit/formatter/uda_string.py 
new/vit-2.3.0/vit/formatter/uda_string.py
--- old/vit-2.2.0/vit/formatter/uda_string.py   2020-12-24 04:52:45.000000000 
+0100
+++ new/vit-2.3.0/vit/formatter/uda_string.py   2022-10-15 01:05:54.000000000 
+0200
@@ -1,9 +1,10 @@
 from vit.formatter import String
+from vit.util import unicode_len
 
 class UdaString(String):
     def format(self, string, task):
         if not string:
             return self.markup_none(self.colorize())
-        return (len(string), self.markup_element(string))
+        return (unicode_len(string), self.markup_element(string))
     def colorize(self, string=None):
         return self.colorizer.uda_string(self.column, string)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vit-2.2.0/vit/formatter_base.py 
new/vit-2.3.0/vit/formatter_base.py
--- old/vit-2.2.0/vit/formatter_base.py 2022-03-23 00:27:36.000000000 +0100
+++ new/vit-2.3.0/vit/formatter_base.py 2023-04-14 06:10:49.000000000 +0200
@@ -7,6 +7,7 @@
 
 from vit import util
 from vit import uda
+from vit.util import unicode_len
 
 INDICATORS = [
     'active',
@@ -18,7 +19,7 @@
 
 DEFAULT_DESCRIPTION_TRUNCATE_LEN=20
 
-class FormatterBase(object):
+class FormatterBase:
     def __init__(self, loader, config, task_config, markers, task_colorizer):
         self.loader = loader
         self.config = config
@@ -89,12 +90,12 @@
     def format_subproject_indented(self, project_parts):
         if len(project_parts) == 1:
             subproject = project_parts[0]
-            return (len(subproject), '', '', subproject)
+            return (unicode_len(subproject), '', '', subproject)
         else:
             subproject = project_parts.pop()
             space_padding = (len(project_parts) * 2) - 1
             indicator = u'\u21aa '
-            width = space_padding + len(indicator) + len(subproject)
+            width = space_padding + unicode_len(indicator) + 
unicode_len(subproject)
             return (width, ' ' * space_padding , indicator, subproject)
 
     def recalculate_due_datetimes(self):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vit-2.2.0/vit/help.py new/vit-2.3.0/vit/help.py
--- old/vit-2.2.0/vit/help.py   2020-12-24 04:52:45.000000000 +0100
+++ new/vit-2.3.0/vit/help.py   2023-04-14 06:10:49.000000000 +0200
@@ -3,6 +3,7 @@
 import urwid
 
 from vit.base_list_box import BaseListBox
+from vit.util import unicode_len
 
 CURLY_BRACES_REGEX = re.compile("[{}]")
 SPECIAL_KEY_SUBSTITUTIONS = {
@@ -57,8 +58,8 @@
             'keys': 0,
         }
         for entry in entries:
-            type_len = len(entry[0])
-            keys_len = len(entry[1])
+            type_len = unicode_len(entry[0])
+            keys_len = unicode_len(entry[1])
             if type_len > column_widths['type']:
                 column_widths['type'] = type_len
             if keys_len > column_widths['keys']:
@@ -78,7 +79,7 @@
     def eat_other_keybindings(self):
         return True
 
-class Help(object):
+class Help:
     """Generates help list/display.
     """
     def __init__(self, keybinding_parser, actions, event=None, 
request_reply=None, action_manager=None):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vit-2.2.0/vit/key_cache.py 
new/vit-2.3.0/vit/key_cache.py
--- old/vit-2.2.0/vit/key_cache.py      2020-12-30 20:07:59.000000000 +0100
+++ new/vit-2.3.0/vit/key_cache.py      2023-04-14 06:10:49.000000000 +0200
@@ -3,7 +3,7 @@
 class KeyCacheError(Exception):
     pass
 
-class KeyCache(object):
+class KeyCache:
     def __init__(self, keybindings):
         self.keybindings = keybindings
         self.cached_keys = ''
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vit-2.2.0/vit/keybinding_parser.py 
new/vit-2.3.0/vit/keybinding_parser.py
--- old/vit-2.2.0/vit/keybinding_parser.py      2020-12-30 20:10:59.000000000 
+0100
+++ new/vit-2.3.0/vit/keybinding_parser.py      2023-04-14 06:10:49.000000000 
+0200
@@ -23,7 +23,7 @@
 class KeybindingError(Exception):
     pass
 
-class KeybindingParser(object):
+class KeybindingParser:
     def __init__(self, loader, config, action_registry):
         self.loader = loader
         self.config = config
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vit-2.2.0/vit/list_batcher.py 
new/vit-2.3.0/vit/list_batcher.py
--- old/vit-2.2.0/vit/list_batcher.py   2020-12-24 04:52:45.000000000 +0100
+++ new/vit-2.3.0/vit/list_batcher.py   2023-04-14 06:10:49.000000000 +0200
@@ -3,7 +3,7 @@
 class ListBatchError(Exception):
     pass
 
-class ListBatcher(object):
+class ListBatcher:
     def __init__(self, batch_from, batch_to, batch_to_formatter=None, 
default_batch_size=DEFAULT_BATCH_SIZE):
         self.batch_from = batch_from
         self.batch_to = batch_to
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vit-2.2.0/vit/loader.py new/vit-2.3.0/vit/loader.py
--- old/vit-2.2.0/vit/loader.py 2021-11-04 14:51:45.000000000 +0100
+++ new/vit-2.3.0/vit/loader.py 2023-04-14 06:10:49.000000000 +0200
@@ -8,7 +8,7 @@
 
 DEFAULT_VIT_DIR = '~/.vit'
 
-class Loader(object):
+class Loader:
     def __init__(self):
         self.user_config_dir = os.path.expanduser('VIT_DIR' in env.user and 
env.user['VIT_DIR'] or DEFAULT_VIT_DIR)
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vit-2.2.0/vit/markers.py new/vit-2.3.0/vit/markers.py
--- old/vit-2.2.0/vit/markers.py        2021-04-24 02:12:31.000000000 +0200
+++ new/vit-2.3.0/vit/markers.py        2023-04-14 06:10:49.000000000 +0200
@@ -33,7 +33,7 @@
     'until.label': '(U)',
 }
 
-class Markers(object):
+class Markers:
     def __init__(self, config, task_config):
         self.config = config
         self.task_config = task_config
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vit-2.2.0/vit/option_parser.py 
new/vit-2.3.0/vit/option_parser.py
--- old/vit-2.2.0/vit/option_parser.py  2020-12-24 04:52:45.000000000 +0100
+++ new/vit-2.3.0/vit/option_parser.py  2023-01-06 13:19:59.000000000 +0100
@@ -1,3 +1,4 @@
+import glob
 import sys
 import argparse
 
@@ -34,12 +35,21 @@
       action="store_true",
       help="list all available actions",
 )
+parser.add_argument('--list-pids',
+      dest="list_pids",
+      default=False,
+      action="store_true",
+      help="list all pids found in pid_dir, if configured",
+)
 
 def parse_options():
     options, filters = parser.parse_known_args()
     if options.list_actions:
         list_actions()
         sys.exit(0)
+    elif options.list_pids:
+        ret = list_pids()
+        sys.exit(ret)
     return options, filters
 
 def format_dictionary_list(item, description):
@@ -53,3 +63,29 @@
     actions = Actions(action_registry)
     actions.register()
     any(format_dictionary_list(action, data['description']) for action, data 
in actions.get().items())
+
+def _get_pids_from_pid_dir(pid_dir):
+        filepaths = glob.glob("%s/*.pid" % pid_dir)
+        pids = []
+        for filepath in filepaths:
+            try:
+                with open(filepath, 'r') as f:
+                    pids.append(f.read())
+            except IOError:
+                pass
+        return pids
+
+def list_pids():
+    from vit.loader import Loader
+    from vit.config_parser import ConfigParser
+    from vit.pid_manager import PidManager
+    loader = Loader()
+    config = ConfigParser(loader)
+    pid_manager = PidManager(config)
+    if pid_manager.pid_dir:
+        pids = _get_pids_from_pid_dir(pid_manager.pid_dir)
+        print("\n".join(pids))
+        return 0
+    else:
+        print("ERROR: No pid_dir configured")
+        return 1
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vit-2.2.0/vit/pid_manager.py 
new/vit-2.3.0/vit/pid_manager.py
--- old/vit-2.2.0/vit/pid_manager.py    1970-01-01 01:00:00.000000000 +0100
+++ new/vit-2.3.0/vit/pid_manager.py    2023-04-14 06:10:49.000000000 +0200
@@ -0,0 +1,49 @@
+import os
+import errno
+
+class PidManager:
+    """Simple process ID manager.
+    """
+    def __init__(self, config):
+        self.config = config
+        self.uid = os.getuid()
+        self.pid = os.getpid()
+        self._format_pid_dir()
+        self._make_pid_filepath()
+
+    def setup(self):
+        if self.pid_dir:
+            self._create_pid_dir()
+            self._write_pid_file()
+
+    def teardown(self):
+        if self.pid_dir:
+            try:
+                os.remove(self.pid_file)
+            # TODO: This needs a little more work to skip errors when no PID 
file
+            #       exists.
+            #except OSError as e:
+            #    if e.errno != errno.ENOENT:
+            #        raise OSError("could not remove pid file %s" % 
self.pid_file)
+            except:
+                pass
+
+    def _format_pid_dir(self):
+        config_pid_dir = self.config.get('vit', 'pid_dir')
+        self.pid_dir = config_pid_dir.replace("$UID", str(self.uid))
+
+    def _make_pid_filepath(self):
+        self.pid_file = "%s/%s.pid" % (self.pid_dir, self.pid)
+
+    def _create_pid_dir(self):
+        try:
+            os.makedirs(self.pid_dir, exist_ok=True)
+        except OSError:
+            raise OSError("could not create pid_dir %s" % self.pid_dir)
+
+    def _write_pid_file(self):
+        try:
+            with open(self.pid_file, "w") as f:
+                f.write(str(self.pid))
+        except IOError:
+            raise IOError("could not write pid file %s" % self.pid_file)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vit-2.2.0/vit/process.py new/vit-2.3.0/vit/process.py
--- old/vit-2.2.0/vit/process.py        2021-11-04 14:51:45.000000000 +0100
+++ new/vit-2.3.0/vit/process.py        2023-04-14 06:10:49.000000000 +0200
@@ -8,7 +8,7 @@
 
 DEFAULT_CONFIRM = 'Press Enter to continue...'
 
-class Command(object):
+class Command:
 
     def __init__(self, config):
         self.config = config
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vit-2.2.0/vit/readline.py 
new/vit-2.3.0/vit/readline.py
--- old/vit-2.2.0/vit/readline.py       2021-11-04 14:51:45.000000000 +0100
+++ new/vit-2.3.0/vit/readline.py       2023-04-14 06:10:49.000000000 +0200
@@ -1,7 +1,7 @@
 import string
 import re
 
-class Readline(object):
+class Readline:
     def __init__(self, edit_obj):
         self.edit_obj = edit_obj
         word_chars = string.ascii_letters + string.digits + "_"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vit-2.2.0/vit/registry.py 
new/vit-2.3.0/vit/registry.py
--- old/vit-2.2.0/vit/registry.py       2020-12-24 04:52:45.000000000 +0100
+++ new/vit-2.3.0/vit/registry.py       2023-04-14 06:10:49.000000000 +0200
@@ -1,6 +1,6 @@
 import uuid
 
-class ActionRegistrar(object):
+class ActionRegistrar:
     def __init__(self, registry):
         self.registry = registry
         self.uuid = uuid.uuid4()
@@ -17,7 +17,7 @@
     def actions(self):
         return self.registry.get_registered(self.uuid)
 
-class ActionRegistry(object):
+class ActionRegistry:
     def __init__(self):
         self.actions = {}
         self.noop_action_name = 'NOOP'
@@ -48,7 +48,7 @@
     def noop(self):
         pass
 
-class RequestReply(object):
+class RequestReply:
     def __init__(self):
         self.handlers = {}
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vit-2.2.0/vit/task.py new/vit-2.3.0/vit/task.py
--- old/vit-2.2.0/vit/task.py   2021-11-04 14:51:57.000000000 +0100
+++ new/vit-2.3.0/vit/task.py   2023-04-14 06:10:49.000000000 +0200
@@ -8,7 +8,7 @@
 from vit import util
 from vit.exception import VitException
 
-class TaskListModel(object):
+class TaskListModel:
     def __init__(self, task_config, reports, report=None, data_location=None):
 
         if not data_location:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vit-2.2.0/vit/task_list.py 
new/vit-2.3.0/vit/task_list.py
--- old/vit-2.2.0/vit/task_list.py      2021-07-15 18:06:41.000000000 +0200
+++ new/vit-2.3.0/vit/task_list.py      2023-04-14 06:10:49.000000000 +0200
@@ -13,12 +13,14 @@
 from vit.base_list_box import BaseListBox
 from vit.list_batcher import ListBatcher
 from vit.formatter.project import Project as ProjectFormatter
+from vit.util import unicode_len
+
 
 REDUCE_COLUMN_WIDTH_LIMIT = 20
 COLUMN_PADDING = 2
 MARKER_COLUMN_NAME = 'markers'
 
-class TaskTable(object):
+class TaskTable:
 
     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):
         self.config = config
@@ -151,10 +153,14 @@
             position = self.listbox.focus_position
         self.list_walker[position].row.set_attr_map({None: attr})
 
-    def flash_focus(self, repeat_times=2, pause_seconds=0.1):
+    def flash_focus(self, repeat_times=None, pause_seconds=None):
+        if repeat_times is None:
+            repeat_times = self.config.get_flash_focus_repeat_times()
+        if pause_seconds is None:
+            pause_seconds = self.config.get_flash_focus_pause_seconds()
         if self.listbox.focus:
             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
-            if position is not None:
+            if position is not None and repeat_times > 0:
                 self.update_focus_attr('flash on', position)
                 self.draw_screen()
                 for i in repeat(None, repeat_times):
@@ -248,7 +254,7 @@
         if isinstance(formatted_value, tuple):
             return formatted_value
         else:
-            width = len(formatted_value) if formatted_value else 0
+            width = unicode_len(formatted_value) if formatted_value else 0
             return width, formatted_value
 
     def subproject_indentable(self):
@@ -329,7 +335,7 @@
 
     def reconcile_column_width_for_label(self):
         for idx, column in enumerate(self.columns):
-            label_len = len(column['label'])
+            label_len = unicode_len(column['label'])
             if column['width'] < label_len:
                 self.columns[idx]['width'] = label_len
 
@@ -388,7 +394,7 @@
         if grew > 0:
             self.batcher.add(grew)
 
-class TaskRow():
+class TaskRow:
     def __init__(self, task, data, alt_row):
         self.task = task
         self.data = data
@@ -396,7 +402,7 @@
         self.uuid = self.task['uuid']
         self.id = self.task['id']
 
-class ProjectRow():
+class ProjectRow:
     def __init__(self, project, placeholder, alt_row):
         self.project = project
         self.placeholder = placeholder
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vit-2.2.0/vit/util.py new/vit-2.3.0/vit/util.py
--- old/vit-2.2.0/vit/util.py   2021-11-04 14:51:45.000000000 +0100
+++ new/vit-2.3.0/vit/util.py   2022-12-25 21:06:26.000000000 +0100
@@ -4,6 +4,8 @@
 import shlex
 from functools import reduce
 
+from urwid.str_util import calc_width
+
 curses.setupterm()
 e3_seq = curses.tigetstr('E3') or b''
 clear_screen_seq = curses.tigetstr('clear') or b''
@@ -55,3 +57,6 @@
 
 def file_readable(filepath):
     return os.path.isfile(filepath) and os.access(filepath, os.R_OK)
+
+def unicode_len(string):
+    return calc_width(string, 0, len(string))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vit-2.2.0/vit/version.py new/vit-2.3.0/vit/version.py
--- old/vit-2.2.0/vit/version.py        2022-04-18 00:51:26.000000000 +0200
+++ new/vit-2.3.0/vit/version.py        2023-04-14 06:10:49.000000000 +0200
@@ -1 +1 @@
-VIT = '2.2.0'
+VIT = '2.3.0'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vit-2.2.0/vit.egg-info/PKG-INFO 
new/vit-2.3.0/vit.egg-info/PKG-INFO
--- old/vit-2.2.0/vit.egg-info/PKG-INFO 2022-04-18 01:07:49.000000000 +0200
+++ new/vit-2.3.0/vit.egg-info/PKG-INFO 2023-04-14 06:27:09.000000000 +0200
@@ -1,13 +1,11 @@
 Metadata-Version: 2.1
 Name: vit
-Version: 2.2.0
+Version: 2.3.0
 Summary: Visual Interactive Taskwarrior full-screen terminal interface
 Home-page: https://github.com/vit-project/vit
 Author: Chad Phillips
 Author-email: c...@apartmentlines.com
-License: UNKNOWN
 Keywords: taskwarrior,console,tui,text-user-interface
-Platform: UNKNOWN
 Classifier: Development Status :: 5 - Production/Stable
 Classifier: License :: OSI Approved :: MIT License
 Classifier: Environment :: Console
@@ -43,7 +41,7 @@
 ## Requirements
 
  * [Taskwarrior](https://taskwarrior.org)
- * [Python](https://www.python.org) 3.5+
+ * [Python](https://www.python.org) 3.7+
  * [pip](https://pypi.org/project/pip)
 
 ## Installation
@@ -85,5 +83,3 @@
 
  Taskwarrior Team
  supp...@taskwarrior.org
-
-
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vit-2.2.0/vit.egg-info/SOURCES.txt 
new/vit-2.3.0/vit.egg-info/SOURCES.txt
--- old/vit-2.2.0/vit.egg-info/SOURCES.txt      2022-04-18 01:07:49.000000000 
+0200
+++ new/vit-2.3.0/vit.egg-info/SOURCES.txt      2023-04-14 06:27:09.000000000 
+0200
@@ -37,6 +37,7 @@
 vit/markers.py
 vit/multi_widget.py
 vit/option_parser.py
+vit/pid_manager.py
 vit/process.py
 vit/readline.py
 vit/registry.py

Reply via email to