pax_global_header00006660000000000000000000000064145133665760014532gustar00rootroot0000000000000052 comment=54477e8f84247d5e62f110438687e7fb007c9f4d vit-2.3.2/000077500000000000000000000000001451336657600123405ustar00rootroot00000000000000vit-2.3.2/.github/000077500000000000000000000000001451336657600137005ustar00rootroot00000000000000vit-2.3.2/.github/ISSUE_TEMPLATE/000077500000000000000000000000001451336657600160635ustar00rootroot00000000000000vit-2.3.2/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000007761451336657600205670ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- **Describe the bug** **To Reproduce** **Expected behavior** **Test case** If reproducing your issue requires any TaskWarrior setup, please produce a [test case](https://github.com/vit-project/vit/blob/2.x/TEST-CASE.md) script. vit-2.3.2/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000007001451336657600216050ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** **Describe the solution you'd like** **Additional context** vit-2.3.2/.gitignore000066400000000000000000000037611451336657600143370ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ vit-2.3.2/AUTHORS.md000066400000000000000000000015301451336657600140060ustar00rootroot00000000000000The development of VIT was made possible by the significant contributions of the following people: * Chad Phillips (Maintainer and Original Author >= 2.x) * Scott Kostyshak (Maintainer and Contributing Author) * Steve Rader (Original Author) * Paul Beckingham (Advisor) * David J Patrick (Designer) The following submitted code or analysis, and deserve special thanks: * Bryce Harrington * Nemo Inis * Devendra Ghate * Ankur Sinha * Benjamin Weber * Jochen Sprickerhof * Peter Novak * Terran Air * Rowan Thorpe * Richard Gay * Bas Zoetekouw * Dhananjay Balan Thanks to the following, who submitted detailed bug reports and excellent suggestions: * Ben Boeckel * Michael Ahern * Peter Lewis * Jean-Philippe Rutault * Jason Woofenden * Benedikt Morbach * Alick Zhao * Bernhard M. Wiedemann * Thomas Rebele vit-2.3.2/CHANGES.md000066400000000000000000000475041451336657600137440ustar00rootroot00000000000000##### Mon Oct 16 2023 - released v2.3.2 * **Sun Sep 17 2023:** fix(py3.12): `SafeConfigParser` -> `ConfigParser` * **Mon Jun 26 2023:** fix #340: Clarify how to interact inside 'Denotate' window ##### Sat Jun 24 2023 - released v2.3.1 * **Sat Jun 24 2023:** fix #339: urwid.str_util import fails on some distros ##### Thu Apr 13 2023 - released v2.3.0 * **Wed Mar 01 2023:** List vit.config and vit.keybinding in packages * **Thu Mar 02 2023:** Remove some unnecessary parenthesis from class definitions * **Wed Mar 01 2023:** Remove inherits from object * **Wed Mar 01 2023:** Use r-prefix when necessary * **Sat Oct 22 2022:** Fixing IndexErrors in functions * **Wed Oct 19 2022:** Allow flash configuration * **Sat Jul 09 2022:** correctly calculate text width of full-width characters * **Sun May 08 2022:** Fix required minimum Python version * **Sun May 01 2022:** add note that windows doesn't support SIGUSR1 signal * **Sun May 01 2022:** test for signal before adding handler * **Fri Apr 29 2022:** add documentation for auto-refresh configuration * **Thu Apr 28 2022:** fix bad variable reference * **Thu Apr 28 2022:** place IS_VIT_INSTANCE into environ earlier * **Thu Apr 28 2022:** example hook for intelligent VIT refresh * **Thu Apr 28 2022:** inject environment variable * **Wed Apr 27 2022:** skip pid teardown if pid_dir not set * **Wed Apr 27 2022:** sample script to externally refresh VIT instances * **Wed Apr 27 2022:** add Bash function example for vit wrapper * **Wed Apr 27 2022:** skip raising error on not deleting pid file for now * **Wed Apr 27 2022:** add pid_dir option to [vit] section See sample config.ini for details on usage * **Wed Apr 27 2022:** add basic signal support SIGUSR1: refresh (equivalent to hitting refresh key in VIT) SIGTERM/SIGINT/SIGQUIT: quit VIT cleanly * **Tue Apr 26 2022:** more user-friendly error message for unsupported color definitions * **Sun Apr 17 2022:** add simple release checklist ##### Sun Apr 17 2022 - released v2.2.0 * **Sun Apr 17 2022:** bump dependency versions * **Sun Apr 17 2022:** bump minimum Python version to 3.7 * **Tue Mar 22 2022:** Simplify timezone handling * **Sat Jul 24 2021:** Replace pytz and tzlocal by zoneinfo * **Sat Mar 05 2022:** Make vit respect taskrc in config.ini ##### Fri Nov 26 2021 - released v2.2.0b1 * **Fri Nov 26 2021:** fix #317: Broken links on PyPi * **Mon Nov 22 2021:** fix #313: ACTION_REFRESH keybind triggers while entering text * **Wed Oct 27 2021:** add `focus_on_add` configuration parameter, allows focusing on newly added task * **Wed Oct 27 2021:** properly escape search terms * **Tue Oct 12 2021:** Include 'report.X.context=0' option of tw 2.6.0 * **Sat Oct 09 2021:** fix #305: vit fails when using new context definition * **Wed Oct 06 2021:** bump tasklib min version * **Wed Oct 06 2021:** Support XDG_CONFIG_DIR taskrc location * **Wed Sep 29 2021:** fix #302: display task id of created task in command bar * **Sun Aug 29 2021:** fix #140, fix #230. smarter handling of spaces/quotes in autocomplete * **Sun Aug 29 2021:** clarify doc for finding user config directory * **Sun Aug 11 2019:** Add support for XDG Base Directory * **Thu Jul 15 2021:** set VIT_TASK_UUID environment variable when executing external scripts * **Thu Jul 15 2021:** allow passing custom environment variables to external commands * **Mon Jun 07 2021:** fix #296: AutoComplete space_escape_regex not initialized for 'wait' command * **Tue Mar 16 2021:** fix #287: Incorrect marker width calculation of Unicode symbols can cause markers to not be displayed * **Sun Feb 28 2021:** add support for TaskWarrior >= 2.5.2 to changelog ##### Sun Feb 28 2021 - released v2.1.0 Support for TaskWarrior >= 2.5.2 This release includes a breaking change to the keybinding parser, and may affect users who have implemented custom keybindings in their configuration. See https://github.com/vit-project/vit/commit/dd0f34347e7b77dce37fe72e3797581d212f0d90 for more information. * **Mon Feb 01 2021:** fix: blocked marker displaying for deleted/completed depends * **Thu Jan 28 2021:** add quick start instructions to README * **Wed Dec 30 2020:** [BREAKING CHANGE] correctly parse bracket expressions for keybinding keys * **Thu Dec 24 2020:** Add 'abort_backspace' config, False by default. If true, hitting backspace against an empty prompt aborts the prompt. * **Fri Dec 25 2020:** Use Python.ignore from github ##### Wed Dec 23 2020 - released v2.1.0b1 This release includes compatibility with Taskwarrior 2.5.2 -- earlier releases of VIT may not work with Taskwarrior 2.5.2 and beyond. * **Wed Dec 23 2020:** update URL to vit-project * **Wed Oct 14 2020:** Make VIT uppercase * **Wed Oct 14 2020:** Add digit-jumping keybindings suggestion to CUSTOMIZE doc * **Sun Sep 13 2020:** fix #269: Can't set priority when uda.priority.values is customized * **Fri Sep 11 2020:** fix #266: print.empty.columns truthness values are not properly handled * **Fri Sep 11 2020:** fix #265: Project name completion only works for projects with pending tasks * **Fri Sep 11 2020:** add logo * **Thu Sep 03 2020:** fix #268: remove priority formatters, use uda formatters instead * **Tue Jul 28 2020:** fix #256: Non-pending blocking tasks should not appear in reports as dependencies * **Tue Jul 28 2020:** fix #260: n / N commands should honor search direction * **Tue Jul 28 2020:** add debug section to devel readme * **Mon Jul 27 2020:** Update issue templates * **Sun Jul 26 2020:** add TEST-CASE description * **Sat Jul 25 2020:** fix #255: vit freezes on startup if there is no taskrc file present * **Tue Jul 07 2020:** API for variable replacements in keybindings * **Wed Jul 22 2020:** fix #253: Crash on switch to newly created context * **Mon Jul 20 2020:** fix #252: catch filter errors and display nicely * **Sat Jul 18 2020:** Fix: #251 -- Filtering with empty attribute value yields a crash * **Wed Jul 15 2020:** fix #250: provide default labels when none configured in report * **Fri Jul 10 2020:** provide some feedback on failed user module load * **Sat May 16 2020:** Fix crash when report has project column but no sort * **Wed Mar 11 2020:** Fix #203: Column resizing incorrect when column labels are wider than column * **Fri Mar 06 2020:** fix #222, fix #221: first step to properly supporting spaces in tab completion * **Sat Feb 29 2020:** Fix #227: Provide default report/annotation format if none configured * **Tue Feb 25 2020:** resolves #225: document keybinding config for capital letters * **Sun Nov 03 2019:** fix #176: Readline edit shortcuts for command bar * **Wed Jan 08 2020:** fix #186, part two: get correct project column index after cleaning * **Wed Jan 08 2020:** add project to dummy task script * **Sat Dec 07 2019:** fix #218: specify minimum versions for dependent packages * **Tue Nov 05 2019:** Allows config option value to contain '=' * **Sun Oct 20 2019:** fix #211: Space at the end of keybinding not parsed * **Sun Oct 13 2019:** cleanup on #203, only resize again if any columns need it * **Sun Oct 13 2019:** fix #203: Descriptions not shown when mulitple columns need reducing * **Sun Oct 13 2019:** fix display of last header column when abuts side of terminal window * **Sun Oct 06 2019:** fix #206: passing negative filters through to task don't work * **Wed Oct 02 2019:** Fix #192: add disallowed reports with error message ##### Sat Sep 28 2019 - released v2.0.0 * **Fri Sep 27 2019:** add UPGRADE.md, include v2.0.0 upgrade instructions * **Thu Sep 26 2019:** fix crash when shlex cannot parse a string to args * **Wed Sep 18 2019:** fix #201: fall back to previous list position when no task found * **Tue Sep 17 2019:** fix #202: properly group different filter types * **Tue Sep 17 2019:** fix #203: Account for edge case with single adjusted column * **Sun Sep 15 2019:** platform-independent temp dir, better debug file name * **Thu Aug 22 2019:** fix crash for reports with no sort specified * **Wed Aug 21 2019:** fix #200: change {TASKID} variable to {TASK_UUID} ##### Sun Aug 18 2019 - released v2.0.0b2 * **Thu Aug 15 2019:** Remove MAX_COLUMN_WIDTH (Closes: #190) * **Tue Aug 06 2019:** fix #196: Add action to sync with taskd * **Tue Aug 06 2019:** fix #197: Allow disabling mouse support * **Sun Aug 04 2019:** Correct import of vit modules in option parser * **Mon Jul 22 2019:** fix #187: TZ env not setup by default in WSL * **Sun Jul 21 2019:** fix #136: Context support in vit ##### Fri Jul 19 2019 - released v2.0.0b1 * **Thu Jun 20 2019:** only activate autocomplete for autocomplete capable ops * **Tue Jun 11 2019:** add script to generate a dummy installation * **Mon Jun 10 2019:** fix #186: test for project column in report before updating project column header * **Sun Jun 9 2019:** test for existence of project column when determining subproject_indentable report setting * **Fri Jun 7 2019:** remove support for inverse color attribute, also add a caveat note to COLOR.md for a workaround * **Wed Jun 5 2019:** try to focus by task UUID in no confirmation case * **Wed Jun 5 2019:** fix #183: Confirmation dialog when starting a task * **Fri May 31 2019:** fix #180: Crash when tasklib raises exception for illegal operation * **Fri May 31 2019:** fix #182: User not returned to previous task on tasks after first page * **Wed Jun 5 2019:** fix #183: Confirmation dialog when starting a task * **Thu May 30 2019:** fix #178: Smarter column width formatting * **Mon May 27 2019:** add missing color mappings for bright black/white * **Sun May 26 2019:** id or short uuid for done * **Sun May 26 2019:** fix endless loop on forward search with no results * **Sun May 26 2019:** id or short uuid for delete/start/stop * **Sat May 25 2019:** fix #179: Search forward and reverse should use same history ##### Sat May 25 2019 - released v2.0.0a1 **IMPORTANT NOTE:** This is an alpha release, no guarantees are made for stability or data integrity. While the author has used the alpha code for over a month with no data corruption issues, it is strongly recommended to back up your data prior to usage. Complete ground up rewrite in Python, feature-complete with VIT 1.x. New features include: * Advanced tab completion * Per-column colorization with markers *(see [COLOR.md](COLOR.md))* * Intelligent sub-project indenting * Multiple/customizable themes * Override/customize column formatters * Fully-customizable key bindings * Table-row striping * Show version/context/report execution time in status area * Customizable config dir * Command line bash completion wrapper This release also changes the software license from GPL to MIT. ##### Mon Aug 6 2018 - released v1.3.beta1 ``` Sat Jun 14 2018 - fix "Negative repeat count does nothing" errors (GH#153) Sat Jun 24 2017 - introduce feature for adding/removing tags (GH#5) Sat Jun 24 2017 - add config option to disable wait prompts (GH#12) Fri Jun 23 2017 - add config option to disable confirmation (GH#4) Fri Jun 23 2017 - add "wait" command bound to 'w' (GH#3) Sun Mar 12 2017 - add ctrl+g synonym for escape Sun Mar 12 2017 - add start/stop toggle bound to 'b' (GH#152, part of GH#126) Sat Aug 30 2016 - fix handling of CJK characters (GH#142) Sat Aug 6 2016 - fix for multi-byte searches Sun Jul 24 2016 - fix for multi-byte prompt input Sun Jul 24 2016 - fix display of multi-byte report data Sun Jul 17 2016 - do not exit if terminal resized to small height Mon Jun 20 2016 - work around another instance of the Perl warning Mon May 16 2016 - work around a warning from Perl versions >= v5.21.1 Sun Jan 10 2016 - annotations now correctly escape any character Sun Jan 10 2016 - do not exit when invalid regex for search string (GH#148) Sat Jan 9 2016 - clean up terminal on a Perl error or warning (part of GH#148) Fri Jan 8 2016 - do not run 'task burndown' by default Fri Jan 8 2016 - add config variable for 'task burndown' Tue Jan 5 2016 - add support for config variables. See 'man vitrc' Wed Sep 30 2015 - improve detection of annotations (GH#144) Sun Mar 8 2015 - do not beep on a 'g' keystroke Sat Mar 7 2015 - fix a bug where prompt text was invisible Wed Mar 4 2015 - add support for End and Home keys (GH#137) Mon Mar 2 2015 - add support for Del key in prompts (GH#120) Sun Mar 1 2015 - allow Taskwarrior cmds to parse for 'a' and 'm' (GH#132, GH#135) Sun Mar 1 2015 - in screen, VIT now highlights the entire line (GH#81, GH#129) Sun Mar 1 2015 - update display after sync (GH#112) Tue Aug 26 2014 - 'n'/'N' now work when not right after search Sun Aug 24 2014 - store commands file in %prefix%/share/vit/ Wed Jul 30 2014 - update documentation URLs Fri Jun 27 2014 - add prompt history scrolling with arrows (GH#54, GH#58) Thu Jun 26 2014 - install files in more conventional paths (GH#118) Wed Jun 25 2014 - fix vitrc man file (GH#119) Wed Jun 25 2014 - Makefile no longer requires sudo (GH#118) Sat Jun 21 2014 - '-version' prints the git hash if available Fri Jun 20 2014 - all of vit's options work with two dashes Fri Jun 20 2014 - 'vit -help' has 0 exit code Fri Jun 20 2014 - 'vit -version' prints the version (GH#114) ``` ##### Sun Apr 6 2014 - released v1.2 ``` Tue Apr 1 2014 - the key can now be used in shortcuts Tue Apr 1 2014 - exit with informative error if shortcut too long (see GH#103) Thu Mar 13 2014 - fix colors for running VIT in tmux Sat Mar 8 2014 - do not print control characters to prompts Thu Mar 6 2014 - fix recognition of backspace in tmux Thu Mar 6 2014 - fix a prompt bug that prevented editing Mon Mar 3 2014 - 'vit -audit' now creates a log with debug info ``` ##### Tue Feb 4 2014 - released v1.2.beta1 ``` Tue Feb 4 2014 - Add VIT man pages (#1284) Mon Oct 21 2013 - Implement cursor movement in prompts (#1403) Sun Oct 6 2013 - Clear project prompt string if escape (#1232) Sun Oct 6 2013 - Remove confusing behavior from arrow keys in prompts (#1363) Sun Sep 28 2013 - 'P' now sets priority and 'h', 'm', 'l', 'n' are freed (#1238) Sun Sep 27 2013 - 'c' is renamed to 'm' (#1231) Sun Sep 15 2013 - 't' now opens the command prompt with ":!rw task " Sun Sep 15 2013 - shell commands can now pass the arguments VIT is using (#1338, #1237) Sat Sep 14 2013 - custom keybinds can now be specified in ~/.vitrc (#1237, #1302, #1336) Thu Sep 12 2013 - added ':!' to execute arbitrary string in shell Sun Aug 12 2013 - When running an external command, VIT no longer echoes it Sun Aug 11 2013 - VIT now cleans the terminal before exiting Sun Aug 11 2013 - 'q' ('Q') now quits with(out) confirmation (#1266) Thu Aug 9 2013 - fix a bug where prompt text was invisible Mon Aug 5 2013 - 's' now runs 'task sync' if Taskwarrior >= 2.3.0 (#1301) Sun Aug 4 2013 - when in search mode, backspace now removes a character Sat Aug 3 2013 - 'D' now deletes a task when not over an annotation (#1230) Sat Jul 6 2013 - added Copyright 2013, Scott Kostyshak Sat Jul 6 2013 - added an AUTHORS file listing contributors Sat Jul 6 2013 - 'gg' now moves to first line (#1229) Sun Jun 23 2013 - added Copyright 2012 - 2013, Steve Rader ``` ##### Wed Apr 3 2013 - released v1.1 ``` Wed Apr 3 2013 - fixes for not having color=on set in ~/.taskrc Sun Mar 31 2013 - added for task info Sun Mar 31 2013 - added logging error msgs when "-audit" is used Sun Mar 31 2013 - added support for selection effects (e.g. bold) Sun Mar 31 2013 - added setting the VIT header color via color.vit.header setting Sat Mar 30 2013 - set the VIT header color via the color.header setting Fri Mar 29 2013 - added support for the "inverse" and "bright" effects Fri Mar 29 2013 - fixed parsing some ANSI underline effect escape sequences Fri Mar 29 2013 - clear the screen before exec'ing external commands as per feature #1214 Fri Mar 29 2013 - fixed a bug where some commands (e.g. ":h") incorrectly waited after exec'ing Fri Mar 29 2013 - added setting the default report via command line args as per feature #1216 Fri Mar 29 2013 - added support to allow for verbose=off as per topic #2851 Fri Mar 29 2013 - disallowed using a default.command which doesn't include an "ID" column Fri Mar 29 2013 - added support for multiple effects, e.g. bold underline ``` ##### Sun Mar 24 2013 - released v1.0 ``` Sun Mar 24 2013 - added '=' for task info as per feature #1156 Sun Mar 24 2013 - added 'u' for task undo Thu Jan 1 2013 - fixed a bug where '/' and '?' caused a crash as per bug #1152 Wed Dec 12 2012 - added graceful handling of marking only task in current report "done" Wed Dec 12 2012 - added "blinking" of the convergence info when convergence changes Wed Dec 12 2012 - disallowed marking completed tasks as "done" Wed Dec 12 2012 - fixed a problem where the selection could get lost after resize and '^l' ``` ##### Tue Dec 11 2012 - released v0.7 ``` Mon Dec 10 2012 - added ./configure checks for the perl Curses and Time::HiRes modules Mon Dec 10 2012 - added ./configure ab-end when /usr/bin/perl doesn't exist Mon Dec 10 2012 - added ./configure substitution for the localized path to the "task" command Sun Dec 9 2012 - fixed a problem where the selection color was lost after refresh Sun Dec 9 2012 - added '/', '?', 'n' and 'N' for searching the current report Sat Dec 8 2012 - added color.label to taskrc-gtk+ Fri Dec 7 2012 - added completion when using 'p' to set project Thu Dec 6 2012 - added 'p' for setting project ``` ##### Wed Dec 5 2012 - released v0.6 ``` Wed Dec 5 2012 - added 'n' for setting priority to none Wed Dec 5 2012 - added 'l' for setting priority to L Wed Dec 5 2012 - added 'm' for setting priority to M Wed Dec 5 2012 - added 'h' for setting priority to H Tue Dec 4 2012 - added 'f' for filter the current report Mon Dec 3 2012 - added checking of task command closing short pipe error Mon Dec 3 2012 - added checking of task command exit status Sun Dec 2 2012 - added 'D' for delete the current annotation (denotate) Sat Dec 1 2012 - added 'A' for add an annotation to the current task Sat Dec 1 2012 - added 'e' for edit current task Sat Dec 1 2012 - fixed problems with the header attributions (bold and underline) ``` ##### Fri Nov 30 2012 - released v0.5 ``` Fri Nov 30 2012 - added ./configure (autoconf) (e.g. "./configure --prefix=/usr/local/vit") Thu Nov 29 2012 - added support for '^w' (erase word) at the command line Thu Nov 29 2012 - added default.command to the list of available reports Wed Nov 28 2012 - added support for ":REPORT " syntax (e.g. ":minimal prio:H") Wed Nov 28 2012 - added support for the DEL key ('^?') as per bug #1134 ``` ##### Wed Nov 28 2012 - released v0.4 ``` Wed Nov 28 2012 - added ":N" for move to task number N Wed Nov 28 2012 - fixed problems with task reports that have no matches Wed Nov 28 2012 - added ":h PATTERN" for help about PATTERN (e.g. ":h help") Tue Nov 27 2012 - added ":h" for help Tue Nov 27 2012 - removed the Term::ReadKey requirement Tue Nov 27 2012 - removed xterm only requirement as per feature #1132 Tue Nov 27 2012 - fixed problems with marking the last task done Mon Nov 26 2012 - added ":STRING" and ":" for changing the current report Mon Nov 26 2012 - added ":REPORT" (e.g. ":long") for changing the report Mon Nov 26 2012 - fixed problems with single tick and double quote ``` ##### Mon Nov 26 2012 - released v0.3 ``` Mon Nov 26 2012 - added support for bold and underlines ANSI colors Sun Nov 25 2012 - wrote taskrc-gtk+ Sun Nov 25 2012 - added task-native colorization Sat Nov 24 2012 - added ' ' for move down one line ``` ##### Sat Nov 24 2012 - released v0.2 ``` Sat Nov 24 2012 - various changes for task version 2.x Sat Nov 24 2012 - added ":s/OLD/NEW/" for change description (e.g. ":s/opps/oops/") Fri Nov 23 2012 - added ":q" for quit Fri Nov 23 2012 - added 'c' for change current task ``` ##### Fri Nov 23 2012 - released v0.1 ``` Fri Nov 23 2012 - added 'a' for add task Wed Nov 21 2012 - added 'd' for mark current task done Tue Nov 20 2012 - added the 'G' and '0' Mon Nov 19 2012 - added the '^f' and '^b' Sun Nov 18 2012 - added the 'L', 'M' and 'H' Sat Nov 17 2012 - added the 'j' and 'k' Fri Nov 16 2012 - designed the layout ``` vit-2.3.2/COLOR.md000066400000000000000000000037621451336657600135500ustar00rootroot00000000000000# VIT Task Coloring VIT handles task coloring differently than Taskwarrior. Whereas Taskwarrior uses a row-based coloring scheme with color blending, VIT uses a column-based coloring scheme. This alternate coloring scheme offers several major advantages: * Much less overall color noise * More meaningful context communicated via color in a single task row These advantages come with a few tradeoffs: * Color cues are sometimes less obvious * Reports sometimes cannot display all color information for a task, if the color-related column is not displayed in the report The first tradeoff most likely becomes less of an issue as the user gets used to the more subtle coloring. VIT addresses the second tradeoff by introducing the concept of markers. Markers are short text indicators that appear in the leftmost column of a report when a column *not* in a report contains data that 'triggers' the marker. By default, markers are tied to the color configuration for the column/state in question, which means that a marker will only appear if the relevant Taskwarrior color setting is also enabled. Markers are highly configurable, including displaying them without color, and customizing the indicator text for all markers. For more information on marker and color settings, see the ```marker``` and ```color``` sections in the [default vit configuration file](vit/config/config.sample.ini). ### Caveats 1. [urwid](http://urwid.org), the library VIT uses for console rendering, does not appear to support discovering the default console colors. Therefore, it becomes impractical to support TaskWarrior's ```inverse``` color attribute. As a workaround, simply replace any color configuration that uses ```inverse``` with the exact foreground/background colors desired. When parsing TaskWarrior's color config, VIT removes the ```inverse``` attribute, which will lead to unexpected color output for the user -- therefore it is recommended to remove/override it anywhere it exists in the TaskWarrior color configuration. vit-2.3.2/CUSTOMIZE.md000066400000000000000000000221351451336657600142470ustar00rootroot00000000000000# Customization ### Configuration #### VIT's configuration VIT provides a user directory that allows for configuring basic settings *(via ```config.ini```)*, as well as custom themes, formatters, and keybindings. VIT searches for the user directory in this order of priority: 1. The ```VIT_DIR``` environment variable 2. ```~/.vit``` (the default location) 3. A ```vit``` directory in any valid [XDG base directory](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) #### Taskwarrior configuration By default, VIT uses the default location of the Taskwarrior configuration file to read configuration from Taskwarrior. Use the ```taskrc``` setting in the ```taskwarrior``` section of ```config.ini``` to override this, or set the ```TASKRC``` environment variable to override both the VIT config file and the default Taskwarrior configuration file location. ### Themes To provide your own theme: 1. Create a ```theme``` directory in the user directory 2. Copy over one of the core themes, and customize to your liking. 3. Set the ```theme``` setting in ```config.ini``` to the name of your theme without the ```.py``` extension. For example, if you created a theme at ```theme/mytheme.py```, then you would set ```theme = mytheme``` in ```config.ini``` VIT uses the [urwid](http://urwid.org) console user interface library, and the theme is essentially an urwid color palette. Check out their [tutorial](http://urwid.org/tutorial) to learn more. ### Formatters VIT provides all the standard formatters used in Taskwarrior. If you choose, you can override any default formatter with your own: 1. Create a ```formatter``` directory in the user directory 2. Find the formatter you want to override, e.g. ```description.truncated``` 3. Create the override file name by replacing dots with underscores, and adding a ```.py``` extension. For the above example, that would be ```description_truncated.py``` 4. Import a [base formatter](vit/formatter/__init__.py), or one of the other more specialized formatters. 5. Name the class by CamelCasing the file name. For the above example, that would be ```class DescriptionTruncated``` 6. Provide a ```format(self, value, task)``` method to the class. ```value``` is the column value for the task in question, ```task``` is the entire task object. The method should return a tuple, the first element is the total length of the formatted text, and the second element is any valid urwid [text markup](http://urwid.org/manual/displayattributes.html#text-markup) An excellent place to use custom formatters is for UDA columns that you want formatted in some non-standard way. For example, let's suppose you have a ```notes``` UDA created, and you want to display that in reports, but in a truncated format. Here's an example of a custom formatter that would accomplish that: ```python from vit.formatter.description_truncated import DescriptionTruncated class Notes(DescriptionTruncated): def format(self, notes, task): if not notes: return self.markup_none(self.colorize()) truncated_notes = self.format_description_truncated(notes) return (len(truncated_notes), self.markup_element(truncated_notes)) def colorize(self, notes=None): return self.colorizer.uda_string(self.column, notes) ``` There are tons of existing formatters to use as examples, and the [base formatters](vit/formatter/__init__.py) include the most common helper methods. ### Keybindings VIT exposes actions to be mapped to keybindings however you like, and custom macros can also be triggered by keybindings. By default, VIT uses the ```keybinding/vi.ini``` configuration to provide Vi-style bindings. To see the list of actions that can be mapped, execute ```vit --list-actions```. #### To override default keybindings: The ```[keybinding]``` section in ```config.ini``` overrides any core keybindings or keybindings that you place in the user ```keybinding``` directory. If you just want to make some small tweaks and/or add some macros, it's probably better to take this approach. The [config.sample.ini](vit/config/config.sample.ini) has many examples to illustrate how to customize keybindings for actions and add macros, check it out! #### To provide your own custom variable replacements: VIT exposes a simple API to provide your own variable replacements in keybindings. Variables enclosed in curly brackets that VIT doesn't know about will be passed to your custom code, where you can match against the variable, and parse the string to extract metadata in the form of arguments that you pass to your custom replacement callback. To provide custom variable replacement: 1. In your user ```keybinding``` directory, create ```keybinding.py``` 2. Create a ```Keybinding``` class in that file 3. Expose a ```replacements``` method in that class, that returns a list of replacement configuration objects 4. Replacement configuration objects have the following keys: * match_callback: should be a function with one argument, which is the variable to test for a match against. If the variable matches your case, return a list with any arguments you want passed to your replacement callback. * replacement_callback: should be a function, with the task object as the first argument, followed by the other arguments you returned from the match callback, and should return a string replacement for the variable. Here's a minimal example: ```python # keybinding/keybinding.py class Keybinding: def replacements(self): def _custom_match(variable): if variable == 'TEST': return ['pass'] def _custom_replace(task, arg): return 'TEST:%s' % arg return [ { 'match_callback': _custom_match, 'replacement_callback': _custom_replace, }, ] ``` #### To provide your own default keybindings: *NOTE: This functionality is more suited to users who want to do something completely different than a Vi-style workflow -- most users will simply want to make some tweaks in the ```[keybinding]``` section of ```config.ini```.* 1. Create a ```keybinding``` directory in the user directory 2. Copy over one of the core keybindings, and customize to your liking. 3. Set the ```default_keybindings``` setting in ```config.ini``` to the name of the keybinding file you created, without the ```.ini``` extension. For example, if you created ```keybinding/strange.ini```, you would set ```default_keybindings = strange``` in ```config.ini``` #### Keybinding suggestions ##### Jumping with digits As jumping to tasks is central to VIT's operation, you might want to map each digit key to an `ex` command containing that digit, by adding the following to your keybindings: ``` 1 = :1 2 = :2 3 = :3 4 = :4 5 = :5 6 = :6 7 = :7 8 = :8 9 = :9 ``` Now, for example, to jump to a task whose ID is 42, you need to press `4`, `2` and ``, instead of `:`, `4`, `2` and ``. 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. vit-2.3.2/DEVELOPMENT.md000066400000000000000000000057501451336657600144530ustar00rootroot00000000000000# Development 1. Clone the repository 2. Change to the root directory 3. ```pip install -r requirements.txt``` 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: ```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 * Run with ```./run-tests.sh``` ### Debugging VIT comes with a simple ```debug``` class for pretty-printing variables to console or file. The file's default location is ```vit-debug.log``` in the system's temp directory. Usage in any of the code is straighforward: ```python import debug debug.console(variable_name) debug.file(variable_name) ``` ### Architecture Whereas VIT 1.x simply layered an [ncurses](https://en.wikipedia.org/wiki/Ncurses) interface over CLI calls to the ```task``` binary, VIT 2.x handles data/reporting differently: * Data is read from Taskwarrior via the [export](https://taskwarrior.org/docs/commands/export.html) functionality using [tasklib](https://github.com/robgolding/tasklib) * 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 The long-term vision is: * Solid test coverage * Plenty of inline documentation * An interface-driven, modular design that allows most components to be overridden/customized * A plugin architecture where appropriate (a good example would be the elements of the top status bar -- each element could be a plugin, allowing third-party plugins to be written and used in that portion of the app) vit-2.3.2/FAQ.md000066400000000000000000000011001451336657600132610ustar00rootroot00000000000000# Frequently asked questions ### How do I fix UnknownTimeZoneError in Windows Subsystem for Linux? If you're running VIT in [Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/about) and getting errors like the following: ```sh pytz.exceptions.UnknownTimeZoneError: 'local' ``` You'll need to properly set the ```TZ``` environment variable to your [local time zone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones), e.g.: ```sh export TZ="America/New_York" ``` It's recommended to add this to one of your shell's startup scripts. vit-2.3.2/INSTALL.md000066400000000000000000000033431451336657600137730ustar00rootroot00000000000000# Installation 1. Make sure you have a supported version of [Python](https://www.python.org) installed, and [pip](https://pypi.org/project/pip), Python's package manager. 2. Decide what Python 'environment' you want to install vit into. Projects like [Virtualenv](https://virtualenv.pypa.io) and [pyenv](https://github.com/pyenv/pyenv) allow you to run non-system Python installations, and are the recommended way to safely install additional Python packages. You may also choose to install vit as a package in your system's Python installation. 3. Once you've decided what environment to install to, simply run: ```pip install vit``` ### Tab completion Since VIT takes report/filter arguments when invoked (just like Taskwarrior), it can be helpful to leverage Taskwarrior's existing tab completion functionality when starting VIT. [scripts/bash/vit.bash_completion](scripts/bash/vit.bash_completion) provides a wrapper to Taskwarrior's [bash completion](https://github.com/scop/bash-completion) support. Place that file somewhere that the bash completion software can load it, and restart your shell to use. ### Older System Pythons VIT is written in Python, and tracks the Python code lifecycle. As such, unsupported versions of Python may not run on VIT, and will not be supported by the maintainers. You can view a list of currently supported Python versions [here](https://devguide.python.org/#status-of-python-branches). However, even if your machine's system Python is no longer supported, you can still run VIT easily by installing a supported version of Python alongside the system Python. There are several projects such as [Virtualenv](https://virtualenv.pypa.io) and [pyenv](https://github.com/pyenv/pyenv) that make this quite easy. vit-2.3.2/LICENSE000066400000000000000000000020561451336657600133500ustar00rootroot00000000000000MIT License Copyright (c) 2019 Chad Phillips Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. vit-2.3.2/MANIFEST.in000066400000000000000000000004531451336657600141000ustar00rootroot00000000000000include scripts/bash/vit.bash_completion recursive-include test *.py include vit/config/config.sample.ini include vit/keybinding/vi.ini include vit/VERSION include COLOR.md include CUSTOMIZE.md include DEVELOPMENT.md include INSTALL.md include LICENSE include README.md include requirements.txt vit-2.3.2/README.md000066400000000000000000000037011451336657600136200ustar00rootroot00000000000000# VIT Logo Visual Interactive Taskwarrior full-screen terminal interface. *For VIT 1.3, [visit here](https://github.com/vit-project/vit/tree/1.3)* ## Features * Fully-customizable key bindings *(default Vim-like)* * Uncluttered display * No mouse * Speed * Per-column colorization * Advanced tab completion * Multiple/customizable themes * Override/customize column formatters * Intelligent sub-project indenting ## Requirements * [Taskwarrior](https://taskwarrior.org) * [Python](https://www.python.org) 3.7+ * [pip](https://pypi.org/project/pip) ## Installation Follow the directions in [INSTALL.md](INSTALL.md) ## Quick start Run ```vit --help``` from the command line for basic usage instructions. Run ```vit``` from the command line to start VIT with default config, report, and filters. While VIT is running, type ```:help``` followed by enter to review basic command/navigation actions. #### Recommendations: * VIT will suggest to install a default user config file if none exists -- it's fully commented with all configuration options, check it out. * Do ```vit --help``` *(know the vit command line arguments)* * Do ```:help``` in vit *(look over the "commands")* * Use an xterm terminal *(for full color support)* * For suggestions on further tweaks, see [CUSTOMIZE.md](CUSTOMIZE.md) * VIT handles task coloring differently than Taskwarrior, see [COLOR.md](COLOR.md) for more details #### Troubleshooting: See [FAQ.md](FAQ.md) #### Upgrading Follow the directions in [UPGRADE.md](UPGRADE.md) #### Development: Interested in the architecture, or in helping out with development? See [DEVELOPMENT.md](DEVELOPMENT.md) ##### In tribute Our friend and collaborator Steve Rader passed away in May 2013. We owe a lot to Steve for his excellent work, and so vit is preserved, maintained and continued. Taskwarrior Team support@taskwarrior.org vit-2.3.2/TEST-CASE.md000066400000000000000000000012701451336657600141520ustar00rootroot00000000000000## Creating a test case If you file an issue that requires some TaskWarrior setup to replicate, the developers may ask you to create a 'test case' script in order to aid in the troubleshooting. Doing so is fairly straightforward: 1. Use the [dummy install script](scripts/generate-dummy-install.sh) as a starting place 2. Adjust the script by editing the the task commands * You can remove the default ones if needed * You can add any new ```task``` command needed to set up the data necessary to reproduce your issue 3. Once you have the script complete, run it locally to make sure you can reproduce the issue you're reporting 4. Attach the script to the issue you've filed vit-2.3.2/UPGRADE.md000066400000000000000000000045261451336657600137600ustar00rootroot00000000000000This file contains information relevant to upgrading VIT from one version to another. Breaking changes between major versions, and significant changes between release versions will be addressed. *Note: for upgrade issues prior to VIT 1.3, please see the [legacy changelog](https://github.com/vit-project/vit/blob/1.3/CHANGES)*. # v2.0.0 Complete ground up rewrite in Python, feature-complete with VIT 1.x. ### New features: * Advanced tab completion * Per-column colorization with markers *(see [COLOR.md](COLOR.md))* * Intelligent sub-project indenting * Multiple/customizable themes *(see [CUSTOMIZE.md](CUSTOMIZE.md))* * Override/customize column formatters *(see [CUSTOMIZE.md](CUSTOMIZE.md))* * Fully-customizable key bindings *(see [CUSTOMIZE.md](CUSTOMIZE.md))* * Table-row striping * Show version/context/report execution time in status area * Customizable config dir *(see [CUSTOMIZE.md](CUSTOMIZE.md))* * Command line bash completion wrapper *(see [INSTALL.md](INSTALL.md))* * Context support This release also changes the software license from GPL to MIT. ### Breaking changes: * Configuration has been moved from ```${HOME}/.vitrc``` to ```${HOME}/.vit/config.ini``` -- the location of the config directory can be customized, see [CUSTOMIZE.md](CUSTOMIZE.md) for details. * The format of the configuration file has changed, customizations in the legacy ```.vitrc``` file will need to be manually ported to the new format. The [config.sample.ini](vit/config/config.sample.ini) file is *heavily* commented, and should contain reference to everything you need to migrate the legacy configuration. If no ```config.ini``` exists in the VIT configuration directory, VIT will offer the option to install the sample config upon startup -- this is the easiest way to get started with porting and customization. * The method of removing annotations from tasks has changed. It is now mapped to the ```ACTION_TASK_DENOTATE``` core action, which in the default keybindings is triggered by ```e``` when the task is highlighted. * VIT 1.3 supports Taskd sync via the ```s``` keybinding, which was undocumented. VIT 2.x properly documents this functionality, and moves it to the keybinding ```s``` by default. * The ```burndown``` configuration option and display has been removed -- it may be added again in a future release or via plugin functionality. vit-2.3.2/images/000077500000000000000000000000001451336657600136055ustar00rootroot00000000000000vit-2.3.2/images/great-tit-square-small.png000066400000000000000000000111631451336657600206210ustar00rootroot00000000000000PNG  IHDRQfzTXtRaw profile type exifx]( YE/I`~z>`;7}gfb@I pQsĕJ*\x^eӮ1z$hh_a׏~W{krt e aNrT9ԃW&P{68lHb!aBwgrwAMBG'Ѱw /w1> " >'-:_~-iayDį&/DՔh2*[=~,nEv)(kl@cJB *3PN&6j1`CXŸp)B 8X6Af~B{ݲkX2ᑿ-9ے?B\a,r,yq-].) [f x.ܒY0Oў[$ڊ`bFShF*"gI|rGDp;ўʙgE RR%GUMԃYrʚsjbԲ.\={Z@-XRJ*U1r!G:ȇ~6OKM[nּV;w8&zֽ^b#>ʨ6e3O>ˬj՟PoRk=n]@-Hh^̢SJ-f062&tZĀ0 b`Aƿ"\}Aﹶpik`Kht1$F#iCCPICC profilexJPiNJqhL.U! 1V:I$$>LA |gp{e'aZBU+{%VC;˼y'4W,/]<癏2TYXbgZUlC؎,?42lv4?6qvqn-\9fȄ1 ]i{R{JBiBT37RN.Hi۬< 1I#YiufyPujFkX놬okq P]!bKGD̿ pHYs.#.#x?vtIME -)Z IDATxixUYB`"uAĀ,""@- *VDAQ\pRjb*`d?$sf9ss?_8Y}g( 'Zj%)- ЍrGM (MD1?M\+9Ds Ś 86adWʭO%i2 i#d*5.1#JrʕkLGb0Qz9F9ܧv.rXR$L:BȦ!J^N*]*8t4BɣR2og&G2XP,Eq)z *@e<6d19rHD aPȥ$мI|!2 0#CяAr6Y!L9EAdyJ !\* <:FPn\PN:J=ĕIGGF LP>)鍊cWdRfvp- '(XNsВHZҟpH iF W25er2*V{BFWMT1XA+@-(kL'`f<ǧTXfTؼ,%HQ(51!gIm{a\ ԤG.}#mT|͵-!L6fWlt6Xf1.qQV0Ke/na1A^:1FVmE7҇tG F3e2kq2w/d$iӑ uWFON)? &Sǵt_5aJ;:@%}[_]-F:7==CNJjL>=L)NG}C:(a|WO I_i9+9qN9&QC+]VS ܰ K288UO79eD1Yi:E:Yz SXEMu+ʕQ1h gm{UZVvOSx^a7ISDH 1:οj+8gJ4#!<4v$"G4aKp Vw׸&(rxO*M..W ]hrt|j04Õ$)G QtaqDQVfALsI"k&C c-ܭF4 "ZI_ɓ.V倃5)^Rz f)pۭѤ޾I|5-تt9`9V/,0^U]WUՒ_=E޾gY2tR# q{m-vy9ї+U5 S:VzQ/[dzr~COT',2I&!r_qwՌȾ4;b*cK:ZEJ=c5Q!Kulq7v4}Iqi2S,= 8> Cm.yEY [M]qGe8JY_!ҩF15!ґ3oGiBdQ^J:Νi4!2Y8D&D"jP8Ig<4!pYNkŷ,ƂKQo[Y+D"fjBNg&DAI {+kLY7˖c43idsfҷ.9̓ߙMI)8Io{vPY mo|S%4UT tw10Xc\SlbF0d XTMXoBfE*(<|t6Ey2t ^kzZ|"6z72Lٲ dԕvqra3ŏ_ZkA#Ck}d"q7=B,LnOKJX2%Ymz:@W/oθ~C#2?g*6&Q&bGM; x{^@1F6{bZpKF0oqdQ mf<2y:4)}Lc]cܚM3u(͕1=42", "wheel" ] build-backend = "setuptools.build_meta" vit-2.3.2/requirements.txt000066400000000000000000000001041451336657600156170ustar00rootroot00000000000000tasklib>=2.4.3 urwid>=2.1.2 backports.zoneinfo;python_version<"3.9" vit-2.3.2/run-tests.sh000077500000000000000000000002151451336657600146410ustar00rootroot00000000000000#!/usr/bin/env bash ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" PYTHONPATH=${ROOT_DIR} python -m unittest vit-2.3.2/scripts/000077500000000000000000000000001451336657600140275ustar00rootroot00000000000000vit-2.3.2/scripts/bash/000077500000000000000000000000001451336657600147445ustar00rootroot00000000000000vit-2.3.2/scripts/bash/vit.bash_completion000066400000000000000000000002311451336657600206320ustar00rootroot00000000000000# This throws 'bash: COMP_WORDS: bad array subscript' without the redirect. _xfunc task _task &> /dev/null complete -o nospace -F _task vit # vi: ft=sh vit-2.3.2/scripts/generate-changelog-entries.sh000077500000000000000000000005741451336657600215620ustar00rootroot00000000000000#!/usr/bin/env bash SCRIPT_NAME=`basename $0` usage() { echo " ${SCRIPT_NAME} Simple script to generate pretty changelog entries from git commits for use in a markdown list. commit: The git tag or commit hash to generate the log from. " } if [ $# -ne 1 ]; then usage exit 1 fi git log --no-merges --date=format:"%a %b %d %Y" --pretty="* **%ad:** %s" ${1}.. vit-2.3.2/scripts/generate-dummy-install.sh000077500000000000000000000020041451336657600207510ustar00rootroot00000000000000#!/bin/sh set -e set -u TMP_DIR="$(mktemp --directory --suffix .vit)" VIT_DIR="$TMP_DIR/vit" TASK_DIR="$TMP_DIR/task" TASKRC="$TMP_DIR/taskrc" echo " This script will create a dummy TaskWarrior database and VIT configuration. " read -p "Press enter to continue... " DUMMY echo "Creating dummy task configuration..." mkdir "$TASK_DIR" cat > "$TASKRC" < 0)); then if [ -z "${IS_VIT_INSTANCE}" ]; then logger "Tasks modified outside of VIT: ${n}, refreshing" ${REFRESH_SCRIPT} fi fi exit 0 vit-2.3.2/scripts/release-pypi.sh000077500000000000000000000022201451336657600167610ustar00rootroot00000000000000#!/usr/bin/env bash # Convenience script to handle preparing for PyPi release, and printing out the # commands to execute it. execute() { local update_build_release_packages="pip install --upgrade wheel build twine" local clean="rm -rfv dist/ build/" local build="python -m build" local test_pypi_upload="python -m twine upload --repository testpypi dist/*" local pypi_upload="python -m twine upload --skip-existing dist/*" echo "Updating build and release packages with command:" echo " ${update_build_release_packages}" ${update_build_release_packages} if [ $? -eq 0 ]; then echo "Cleaning build environment with command:" echo " ${clean}" ${clean} if [ $? -eq 0 ]; then echo "Building release with command:" echo " ${build}" ${build} if [ $? -eq 0 ]; then echo "Build successful" echo echo "Test release with command:" echo " ${test_pypi_upload}" echo echo "Release with command:" echo " ${pypi_upload}" fi fi fi } if [ -d vit ] && [ -r setup.py ]; then execute else echo "ERROR: must run script from VIT repository root" fi vit-2.3.2/scripts/vit-external-refresh.sh000077500000000000000000000001561451336657600204460ustar00rootroot00000000000000#!/usr/bin/env bash pids="$(vit --list-pids)" for pid in ${pids}; do kill -SIGUSR1 ${pid} &>/dev/null done vit-2.3.2/setup.py000066400000000000000000000034371451336657600140610ustar00rootroot00000000000000import re from setuptools import setup from os import path DEFAULT_BRANCH = "2.x" BASE_GITHUB_URL = "https://github.com/vit-project/vit/blob" 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: readme_contents = f.read() markup_link_substitution = '[\\1](%s/%s/\\2)' % (BASE_GITHUB_URL, DEFAULT_BRANCH) README = re.sub(MARKUP_LINK_REGEX, markup_link_substitution, readme_contents, flags=re.MULTILINE) with open(path.join(FILE_DIR, 'requirements.txt')) as f: INSTALL_PACKAGES = f.read().splitlines() with open(path.join(FILE_DIR, 'vit', 'version.py')) as f: VERSION = re.match(r"^VIT = '([\w\.]+)'$", f.read().strip())[1] setup( name='vit', 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', install_requires=INSTALL_PACKAGES, version=VERSION, url='https://github.com/vit-project/vit', author='Chad Phillips', author_email='chad@apartmentlines.com', classifiers=[ 'Development Status :: 5 - Production/Stable', 'License :: OSI Approved :: MIT License', 'Environment :: Console', 'Programming Language :: Python', 'Natural Language :: English', 'Topic :: Text Processing :: General', ], keywords=[ 'taskwarrior', 'console', 'tui', 'text-user-interface', ], tests_require=[ ], entry_points = { 'console_scripts': [ 'vit=vit.command_line:main', ], }, include_package_data=True, python_requires='>=3.7', zip_safe=False ) vit-2.3.2/test/000077500000000000000000000000001451336657600133175ustar00rootroot00000000000000vit-2.3.2/test/__init__.py000066400000000000000000000000001451336657600154160ustar00rootroot00000000000000vit-2.3.2/test/test_list_batcher.py000066400000000000000000000055661451336657600174070ustar00rootroot00000000000000import unittest from pprint import pprint from vit.list_batcher import ListBatcher DEFAULT_BATCH_FROM = list('abcdefghijklmnopqrstuvwxyz') def default_batcher(): batch_to = [] return ListBatcher(DEFAULT_BATCH_FROM, batch_to), batch_to class TestListBatcher(unittest.TestCase): def test_batch_default_batch(self): batcher, batch_to = default_batcher() complete = batcher.add() self.assertEqual(complete, True) self.assertEqual(batcher.get_last_position(), len(DEFAULT_BATCH_FROM)) self.assertEqual(batch_to, DEFAULT_BATCH_FROM) def test_batch_1(self): batcher, batch_to = default_batcher() complete = batcher.add(1) self.assertEqual(complete, False) self.assertEqual(batcher.get_last_position(), 1) self.assertEqual(batch_to, ['a']) def test_batch_5(self): batcher, batch_to = default_batcher() complete = batcher.add(5) self.assertEqual(complete, False) self.assertEqual(batcher.get_last_position(), 5) self.assertEqual(batch_to, ['a', 'b', 'c', 'd', 'e']) def test_batch_1_and_1(self): batcher, batch_to = default_batcher() batcher.add(1) complete = batcher.add(1) self.assertEqual(complete, False) self.assertEqual(batcher.get_last_position(), 2) self.assertEqual(batch_to, ['a', 'b']) def test_batch_1_and_rest(self): batcher, batch_to = default_batcher() batcher.add(1) complete = batcher.add(0) self.assertEqual(complete, True) self.assertEqual(batcher.get_last_position(), len(DEFAULT_BATCH_FROM)) self.assertEqual(batch_to, DEFAULT_BATCH_FROM) def test_batch_batch_size_greater_than_default(self): batcher, batch_to = default_batcher() complete = batcher.add(100000) self.assertEqual(complete, True) self.assertEqual(batcher.get_last_position(), len(DEFAULT_BATCH_FROM)) self.assertEqual(batch_to, DEFAULT_BATCH_FROM) def test_batch_add_on_completed(self): batcher, batch_to = default_batcher() complete = batcher.add() self.assertEqual(complete, True) complete = batcher.add() self.assertEqual(complete, True) self.assertEqual(batcher.get_last_position(), len(DEFAULT_BATCH_FROM)) self.assertEqual(batch_to, DEFAULT_BATCH_FROM) def test_batch_5_with_formatter(self): def formatter(partial, start_idx): return ['before'] + [row * 2 for row in partial] + ['after'] batch_to = [] batcher = ListBatcher(DEFAULT_BATCH_FROM, batch_to, batch_to_formatter=formatter) complete = batcher.add(5) self.assertEqual(complete, False) self.assertEqual(batcher.get_last_position(), 5) self.assertEqual(batch_to, ['before', 'aa', 'bb', 'cc', 'dd', 'ee', 'after']) if __name__ == '__main__': unittest.main() vit-2.3.2/vit/000077500000000000000000000000001451336657600131425ustar00rootroot00000000000000vit-2.3.2/vit/__init__.py000066400000000000000000000003021451336657600152460ustar00rootroot00000000000000from .option_parser import parse_options from .application import Application from .formatter import ( Formatter, DateTime, Duration, List, Marker, Number, String ) vit-2.3.2/vit/action_manager.py000066400000000000000000000046731451336657600164750ustar00rootroot00000000000000import uuid class ActionManagerRegistrar: def __init__(self, registry): self.registry = registry self.uuid = uuid.uuid4() def register(self, name, callback): self.registry.register(self.uuid, name, callback) def deregister(self, name=None): if name: self.registry.deregister(self.uuid, name) else: any(self.registry.deregister(self.uuid, action) for _, action in self.actions().items()) def actions(self): return self.registry.get_registered(self.uuid) def execute_handler(self, *args): keys, args = args[0], args[1:] handled_action = self.handled_action(keys) if handled_action: handled_action['callback'](*args) data = { 'name': handled_action['name'], 'args': args, } self.registry.event.emit('action-manager:action-executed', data) return True return False def handled_action(self, keys): keybindings = self.registry.keybindings actions = self.actions() if keys in keybindings and 'action_name' in keybindings[keys] and keybindings[keys]['action_name'] in actions: return actions[keybindings[keys]['action_name']] return None class ActionManagerRegistry: def __init__(self, action_registry, keybindings, event=None): self.actions = {} self.action_registry = action_registry self.keybindings = keybindings self.event = event def get_registrar(self): return ActionManagerRegistrar(self) def active_registration_id(self, registration_id): return registration_id in self.actions def get_registered(self, registration_id): return self.actions[registration_id] if self.active_registration_id(registration_id) else {} def register(self, registration_id, name, callback): if not self.active_registration_id(registration_id): self.actions[registration_id] = {} self.actions[registration_id][self.action_registry.make_action_name(name)] = { 'name': name, 'callback': callback, } def deregister(self, registration_id, name_or_action): if self.active_registration_id(registration_id): name = name_or_action['name'] if isinstance(name_or_action, dict) else name_or_action self.actions[registration_id].pop(self.action_registry.make_action_name(name)) vit-2.3.2/vit/actions.py000066400000000000000000000073721451336657600151650ustar00rootroot00000000000000class Actions: def __init__(self, action_registry): self.action_registry = action_registry def register(self): self.action_registrar = self.action_registry.get_registrar() # Global. self.action_registrar.register('QUIT', 'Quit the application') self.action_registrar.register('QUIT_WITH_CONFIRM', 'Quit the application, after confirmation') self.action_registrar.register('GLOBAL_ESCAPE', 'Top-level escape function') self.action_registrar.register('REFRESH', 'Refresh the current report') self.action_registrar.register('TASK_ADD', 'Add a task (supports tab completion)') self.action_registrar.register('REPORT_FILTER', 'Filter current report using provided FILTER arguments (supports tab completion)') self.action_registrar.register('TASK_UNDO', 'Undo last task change') self.action_registrar.register('TASK_SYNC', 'Synchronize with configured taskd server') self.action_registrar.register('COMMAND_BAR_EX', "Open the command bar in 'ex' mode") self.action_registrar.register('COMMAND_BAR_EX_TASK_READ_WAIT', "Open the command bar in 'ex' mode with '!rw task ' appended") self.action_registrar.register('COMMAND_BAR_SEARCH_FORWARD', 'Search forward for provided STRING') self.action_registrar.register('COMMAND_BAR_SEARCH_REVERSE', 'Search reverse for provided STRING') self.action_registrar.register('COMMAND_BAR_SEARCH_NEXT', 'Search next') self.action_registrar.register('COMMAND_BAR_SEARCH_PREVIOUS', 'Search previous') self.action_registrar.register('COMMAND_BAR_TASK_CONTEXT', 'Set task context') self.action_registrar.register(self.action_registry.noop_action_name, 'Used to disable a default keybinding action') # List. self.action_registrar.register('LIST_UP', 'Move list focus up one entry') self.action_registrar.register('LIST_DOWN', 'Move list focus down one entry') self.action_registrar.register('LIST_PAGE_UP', 'Move list focus up one page') self.action_registrar.register('LIST_PAGE_DOWN', 'Move list focus down one page') self.action_registrar.register('LIST_HOME', 'Move list focus to top of the list') self.action_registrar.register('LIST_END', 'Move list focus to bottom of the list') self.action_registrar.register('LIST_SCREEN_TOP', 'Move list focus to top of the screen') self.action_registrar.register('LIST_SCREEN_MIDDLE', 'Move list focus to middle of the screen') self.action_registrar.register('LIST_SCREEN_BOTTOM', 'Move list focus to bottom of the screen') self.action_registrar.register('LIST_FOCUS_VALIGN_CENTER', 'Move focused item to center of the screen') # Task. self.action_registrar.register('TASK_ANNOTATE', 'Add an annotation to a task') self.action_registrar.register('TASK_DELETE', 'Delete task') self.action_registrar.register('TASK_DENOTATE', 'Denotate a task') self.action_registrar.register('TASK_MODIFY', 'Modify task (supports tab completion)') self.action_registrar.register('TASK_START_STOP', 'Start/stop task') self.action_registrar.register('TASK_DONE', 'Mark task done') self.action_registrar.register('TASK_PRIORITY', 'Modify task priority') self.action_registrar.register('TASK_PROJECT', 'Modify task project (supports tab completion)') self.action_registrar.register('TASK_TAGS', 'Modify task tags (supports tab completion, +TAG adds, -TAG removes)') self.action_registrar.register('TASK_WAIT', 'Wait a task') self.action_registrar.register('TASK_EDIT', 'Edit a task via the default editor') self.action_registrar.register('TASK_SHOW', 'Show task details') def get(self): return self.action_registry.actions vit-2.3.2/vit/application.py000066400000000000000000001301621451336657600160220ustar00rootroot00000000000000#!/usr/bin/env python 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 import re import time import copy from inspect import isfunction from functools import reduce import urwid from vit import version from vit.exception import VitException from vit.formatter_base import FormatterBase from vit import event from vit.loader import Loader from vit.config_parser import ConfigParser, TaskParser from vit.util import clear_screen, string_to_args, is_mouse_event, task_id_or_uuid_short from vit.process import Command from vit.task import TaskListModel from vit.autocomplete import AutoComplete from vit.keybinding_parser import KeybindingParser from vit.help import Help from vit.key_cache import KeyCache from vit.actions import Actions from vit.markers import Markers from vit.color import TaskColorConfig, TaskColorizer from vit.task_list import TaskTable from vit.multi_widget import MultiWidget from vit.command_bar import CommandBar 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 # used for the refresh report functionality. Sadly, that is the default # keybinding, therefore the only way to make it work is to catch the refresh # action here in the top frame. class MainFrame(urwid.Frame): def __init__(self, body, header=None, footer=None, focus_part='body', key_cache=None, action_manager=None, refresh=None): self.key_cache = key_cache self.action_manager = action_manager self.refresh = refresh self.register_managed_actions() super().__init__(body, header, footer, focus_part) def register_managed_actions(self): self.action_manager_registrar = self.action_manager.get_registrar() self.action_manager_registrar.register('REFRESH', self.refresh) def is_default_refresh_key(self, keys): return keys == 'ctrl l' def keypress(self, size, key): keys = self.key_cache.get(key) if self.action_manager_registrar.handled_action(keys) and self.is_default_refresh_key(keys): # NOTE: Calling refresh directly here to avoid the # action-manager:action-executed event, which clobbers the load # time currently. self.refresh() return None else: return super().keypress(size, key) 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) self.reports = self.task_config.get_reports() def set_report(self): if len(self.extra_filters) == 0: self.report = self.config.get('report', 'default_report') elif self.extra_filters[0] in self.reports: self.report = self.extra_filters.pop(0) else: self.report = self.config.get('report', 'default_filter_only_report') def setup_main_loop(self): self.loop = urwid.MainLoop(urwid.Text(''), unhandled_input=self.key_pressed, pop_ups=True, handle_mouse=self.config.mouse_enabled) try: self.loop.screen.set_terminal_properties(colors=256) 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() def load_contexts(self): self.contexts = self.task_config.get_contexts() def bootstrap(self, load_early_config=True): self.loader = Loader() if load_early_config: self.load_early_config() self.load_contexts() self.set_active_context() self.event = event.Emitter() self.setup_config() self.search_term_active = '' self.search_direction_reverse = False self.action_registry = ActionRegistry() self.actions = Actions(self.action_registry) self.actions.register() self.keybinding_parser = KeybindingParser(self.loader, self.config, self.action_registry) self.command = Command(self.config) self.get_available_task_columns() self.setup_keybindings() self.action_manager = ActionManagerRegistry(self.action_registry, self.key_cache.keybindings, event=self.event) self.register_managed_actions() self.markers = Markers(self.config, self.task_config) self.theme = self.init_theme() self.theme_alt_backgrounds = self.get_theme_alt_backgrounds() self.task_color_config = TaskColorConfig(self.config, self.task_config, self.theme, self.theme_alt_backgrounds) self.init_task_colors() self.task_colorizer = TaskColorizer(self.task_color_config) self.formatter = FormatterBase(self.loader, self.config, self.task_config, self.markers, self.task_colorizer) self.request_reply = RequestReply() self.set_request_callbacks() # TODO: TaskTable is dependent on a bunch of setup above, this order # feels brittle. self.build_task_table() self.help = Help(self.keybinding_parser, self.actions.get(), event=self.event, request_reply=self.request_reply, action_manager=self.action_manager) self.event.listen('command-bar:keypress', self.command_bar_keypress) self.event.listen('task:denotate', self.denotate_task) self.event.listen('action-manager:action-executed', self.action_manager_action_executed) self.event.listen('help:exit', self.deactivate_help) def setup_config(self): self.confirm = self.config.confirmation_enabled self.wait = self.config.wait_enabled def register_managed_actions(self): # Global. self.action_manager_registrar = self.action_manager.get_registrar() self.action_manager_registrar.register('QUIT', self.quit) self.action_manager_registrar.register('QUIT_WITH_CONFIRM', self.activate_command_bar_quit_with_confirm) # NOTE: This is a no-op for the default refresh keybinding, which is # handled by the MainFrame() class. It's included here in case the user # assigns a non-default key to the REFRESH action. self.action_manager_registrar.register('REFRESH', self.refresh) self.action_manager_registrar.register('TASK_ADD', self.activate_command_bar_add) self.action_manager_registrar.register('REPORT_FILTER', self.activate_command_bar_filter) self.action_manager_registrar.register('TASK_UNDO', self.task_undo) self.action_manager_registrar.register('TASK_SYNC', self.task_sync) self.action_manager_registrar.register('COMMAND_BAR_EX', self.activate_command_bar_ex) self.action_manager_registrar.register('COMMAND_BAR_EX_TASK_READ_WAIT', self.activate_command_bar_ex_read_wait_task) self.action_manager_registrar.register('COMMAND_BAR_SEARCH_FORWARD', self.activate_command_bar_search_forward) self.action_manager_registrar.register('COMMAND_BAR_SEARCH_REVERSE', self.activate_command_bar_search_reverse) self.action_manager_registrar.register('COMMAND_BAR_SEARCH_NEXT', self.activate_command_bar_search_next) self.action_manager_registrar.register('COMMAND_BAR_SEARCH_PREVIOUS', self.activate_command_bar_search_previous) self.action_manager_registrar.register('COMMAND_BAR_TASK_CONTEXT', self.activate_command_bar_task_context) self.action_manager_registrar.register('GLOBAL_ESCAPE', self.global_escape) self.action_manager_registrar.register(self.action_registry.noop_action_name, self.action_registry.noop) # Task. self.action_manager_registrar.register('TASK_ANNOTATE', self.task_action_annotate) self.action_manager_registrar.register('TASK_DELETE', self.task_action_delete) self.action_manager_registrar.register('TASK_DENOTATE', self.task_action_denotate) self.action_manager_registrar.register('TASK_MODIFY', self.task_action_modify) self.action_manager_registrar.register('TASK_START_STOP', self.task_action_start_stop) self.action_manager_registrar.register('TASK_DONE', self.task_action_done) self.action_manager_registrar.register('TASK_PRIORITY', self.task_action_priority) self.action_manager_registrar.register('TASK_PROJECT', self.task_action_project) self.action_manager_registrar.register('TASK_TAGS', self.task_action_tags) self.action_manager_registrar.register('TASK_WAIT', self.task_action_wait) self.action_manager_registrar.register('TASK_EDIT', self.task_action_edit) self.action_manager_registrar.register('TASK_SHOW', self.task_action_show) def default_keybinding_replacements(self): import json from datetime import datetime task_replacement_match = re.compile(r"^TASK_(\w+)$") def _task_attribute_match(variable): matches = re.match(task_replacement_match, variable) if matches: attribute = matches.group(1).lower() if attribute in self.available_columns: return [attribute] def _task_attribute_replace(task, attribute): if task and task[attribute]: if type(task[attribute]) in ['set', 'tuple', 'dict', 'list']: try: json.dumps(task[attribute]) except Exception as e: raise RuntimeError('Error parsing task attribute %s as JSON: %s' % (task[attribute], e)) elif isinstance(task[attribute], datetime): return task[attribute].strftime(self.formatter.report) else: return str(task[attribute]) return '' replacements = [ { 'match_callback': _task_attribute_match, 'replacement_callback': _task_attribute_replace, }, ] return replacements def add_user_keybinding_replacements(self, replacements): klass = self.loader.load_user_class('keybinding', 'keybinding', 'Keybinding') if klass: keybinding_custom = klass() replacements.extend(keybinding_custom.replacements()) return replacements def wrap_replacements_callbacks(self, replacements): def build_wrapper(callback): def wrapper(*args): _, task = self.get_focused_task() return callback(task, *args) return wrapper for i, replacement in enumerate(replacements): replacements[i]['replacement_callback'] = build_wrapper(replacement['replacement_callback']) return replacements def setup_keybindings(self): self.keybinding_parser.load_default_keybindings() bindings = self.config.items('keybinding') replacements = self.wrap_replacements_callbacks(self.add_user_keybinding_replacements(self.default_keybinding_replacements())) keybindings = self.keybinding_parser.add_keybindings(bindings=bindings, replacements=replacements) self.key_cache = KeyCache(keybindings) self.key_cache.build_multi_key_cache() def set_request_callbacks(self): self.request_reply.set_handler('application:keybindings', 'Get keybindings', lambda: self.keybinding_parser.keybindings) self.request_reply.set_handler('application:key_cache', 'Get key cache', lambda: self.key_cache) self.request_reply.set_handler('application:blocking_task_uuids', 'Get blocking task uuids', lambda: self.blocking_task_uuids) def init_theme(self): theme = self.config.get('vit', 'theme') user_theme = self.loader.load_user_class('theme', theme, 'theme') if user_theme: return user_theme try: return import_module('vit.theme.%s' % theme).theme except ImportError: raise ImportError("theme '%s' not found" % theme) def get_theme_setting(self, setting): for s in self.theme: if s[0] == setting: return s def get_theme_alt_backgrounds(self): stiped_table_row = self.get_theme_setting('striped-table-row') return { '.striped-table-row': (stiped_table_row[2], stiped_table_row[5]), } def init_task_colors(self): self.theme += self.task_color_config.display_attrs def clear_key_cache(self): self.key_cache.set() self.update_status_key_cache() def action_manager_action_executed(self, data): self.clear_key_cache() def check_macro(self, keys): keybinding = self.keybinding_parser.keybindings[keys] if keys in self.keybinding_parser.keybindings else False return keybinding if keybinding and 'keys' in keybinding else False def execute_macro(self, keys): keybinding = self.check_macro(keys) if keybinding: keypresses = self.prepare_keybinding_keypresses(keybinding['keys']) self.loop.process_input(keypresses) def prepare_keybinding_keypresses(self, keypresses): def reducer(accum, key): if type(key) is tuple: accum += list(key[0](*key[1])) else: accum.append(key) return accum return reduce(reducer, keypresses, []) def denotate_task(self, data): task = self.model.task_denotate(data['uuid'], data['annotation']) if task: self.table.flash_focus() self.update_report() self.activate_message_bar('Task %s denotated' % self.model.task_id(task['uuid'])) self.task_list.focus_by_task_uuid(data['uuid'], self.previous_focus_position) def command_bar_keypress(self, data): metadata = data['metadata'] op = metadata['op'] if 'choices' in data['metadata']: choice = data['choice'] if op == 'quit' and choice: self.quit() elif op == 'done' and choice is not None: self.task_done(metadata['uuid']) elif op == 'delete' and choice is not None: self.task_delete(metadata['uuid']) elif op == 'start-stop' and choice is not None: self.task_start_stop(metadata['uuid']) elif op == 'priority' and choice is not None: task = self.model.task_priority(metadata['uuid'], choice) if task: self.table.flash_focus() self.update_report() self.activate_message_bar('Task %s priority set to: %s' % (self.model.task_id(task['uuid']), task['priority'] or 'None')) elif data['key'] in ('enter',): args = string_to_args(data['text']) if op == 'ex': metadata = self.ex(data['text'], data['metadata']) elif op == 'filter': self.extra_filters = args self.update_report() elif op == 'project': # TODO: Validation if more than one arg passed. project = args[0] if len(args) > 0 else '' task = self.model.task_project(metadata['uuid'], project) if task: self.table.flash_focus() self.update_report() self.activate_message_bar('Task %s project updated' % self.model.task_id(task['uuid'])) elif op == 'wait': # TODO: Validation if more than one arg passed. wait = args[0] if len(args) > 0 else '' # NOTE: Modify is used here to support the special date # handling Taskwarrior makes available. It's possible the # modified task could be a recurring task, and to make the # individual edit actions consistent, recurrence.confirmation # is set to 'no', so that only the edited recurring task is # modified. returncode, stdout, stderr = self.command.run(['task', 'rc.recurrence.confirmation=no', metadata['uuid'], 'modify', 'wait:%s' % wait], capture_output=True) if returncode == 0: self.table.flash_focus() self.update_report() self.activate_message_bar('Task %s wait updated' % self.model.task_id(metadata['uuid'])) else: self.activate_message_bar("Error setting wait: %s" % stderr, 'error') elif op == 'context': # TODO: Validation if more than one arg passed. context = args[0] if len(args) > 0 else 'none' if context != 'none': # In case a new context was added between bootstraps. self.load_contexts() if self.execute_command(['task', 'context', context], wait=self.wait): self.activate_message_bar('Context switched to: %s' % context) else: self.activate_message_bar('Error switching context', 'error') elif len(args) > 0: if op == 'add': if self.execute_command(['task', 'add'] + args, wait=self.wait): task = self.task_get_latest() self.activate_message_bar('Task %s added' % task_id_or_uuid_short(task)) self.focus_new_task(task) elif op == 'modify': # TODO: Will this break if user clicks another list item # before hitting enter? if self.execute_command(['task', metadata['uuid'], 'modify'] + args, wait=self.wait): self.activate_message_bar('Task %s modified' % self.model.task_id(metadata['uuid'])) elif op == 'annotate': task = self.model.task_annotate(metadata['uuid'], data['text']) if task: self.table.flash_focus() self.update_report() self.activate_message_bar('Annotated task %s' % self.model.task_id(task['uuid'])) elif op == 'tag': task = self.model.task_tags(metadata['uuid'], args) if task: self.table.flash_focus() self.update_report() self.activate_message_bar('Task %s tags updated' % self.model.task_id(task['uuid'])) elif op in ('search-forward', 'search-reverse'): self.search_set_term(data['text']) self.search_set_direction(op) self.search(reverse=(op == 'search-reverse')) self.widget.focus_position = 'body' if 'uuid' in metadata: self.task_list.focus_by_task_uuid(metadata['uuid'], self.previous_focus_position) def focus_new_task(self, task): if self.config.get('vit', 'focus_on_add'): self.task_list.focus_by_task_uuid(task['uuid'], self.previous_focus_position) def key_pressed(self, key): if is_mouse_event(key): return None keys = self.key_cache.get(key) if self.action_manager_registrar.handled_action(keys): self.activate_message_bar() self.action_manager_registrar.execute_handler(keys) elif self.check_macro(keys): self.clear_key_cache() self.activate_message_bar() self.execute_macro(keys) elif keys in self.key_cache.multi_key_cache: self.key_cache.set(keys) self.update_status_key_cache() else: self.clear_key_cache() def on_select(self, row, size, key): keys = self.key_cache.get(key) if self.action_manager_registrar.handled_action(keys): self.activate_message_bar() self.action_manager_registrar.execute_handler(keys) return None else: return key def activate_help(self, args): [self.original_header_top_contents, self.original_header_bottom_contents] = self.header.contents self.original_widget_body = self.widget.body self.original_footer = self.footer help_widget = self.help.update(args) self.header.contents[:] = [] self.widget.body = help_widget self.footer = urwid.Pile([]) def deactivate_help(self, data): self.header.contents[:] = [self.original_header_top_contents, self.original_header_bottom_contents] self.widget.body = self.original_widget_body self.footer = self.original_footer def ex(self, text, metadata): args = string_to_args(text) if len(args): command = args.pop(0) if command in ('h', 'help'): metadata = {} self.activate_help(args) elif command in ('q',): self.quit() elif command in ('!', '!r', '!w', '!rw', '!wr'): kwargs = {} if command in ('!', '!w'): kwargs['update_report'] = False if command in ('!', '!r'): kwargs['confirm'] = None kwargs['wait'] = False else: kwargs['wait'] = True uuid, _ = self.get_focused_task() if not uuid: uuid = "" kwargs['custom_env'] = { "VIT_TASK_UUID": uuid, } self.execute_command(args, **kwargs) elif command.isdigit(): self.task_list.focus_by_task_id(int(command)) if 'uuid' in metadata: metadata.pop('uuid') elif command in self.reports: self.extra_filters = args self.update_report(command) if 'uuid' in metadata: metadata.pop('uuid') elif command in self.task_config.disallowed_reports: self.activate_message_bar("Report '%s' is non-standard, use ':!w task %s'" % (command, command), 'error') else: # Matches 's/foo/bar/' and s%/foo/bar/, allowing for separators # to be any non-word character. matches = re.match(r'^%?s(\W)((?:(?!\1).)*)\1((?:(?!\1).)*)\1$', text) if matches and 'uuid' in metadata: before, after = matches.group(2, 3) task = self.model.get_task(metadata['uuid']) if task: description = re.sub(r'%s' % before, after, task['description']) task = self.model.task_description(metadata['uuid'], description) if task: self.table.flash_focus() self.update_report() self.activate_message_bar('Task %s description updated' % self.model.task_id(task['uuid'])) return metadata def search_set_term(self, text): self.search_term_active = text def search_set_direction(self, op): self.search_direction_reverse = op == 'search-reverse' def search(self, reverse=False): if not self.search_term_active: return self.table.batcher.add(0) self.search_display_message(reverse) current_index = 0 if self.task_list.focus is None else self.task_list.focus_position new_focus = self.search_rows(self.search_term_active, current_index, reverse) if new_focus is None: self.activate_message_bar("Pattern not found: %s" % self.search_term_active, 'error') else: self.task_list.focus_position = new_focus def search_rows(self, term, start_index=0, reverse=False): escaped_term = re.escape(term) search_regex = re.compile(escaped_term, re.MULTILINE) rows = self.table.rows current_index = start_index last_index = len(rows) - 1 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) def search_display_message(self, reverse=False): self.activate_message_bar("Search %s for: %s" % ('reverse' if reverse else 'forward', self.search_term_active)) def search_loop_warning(self, hit, reverse=False): self.activate_message_bar('Search hit %s, continuing at %s' % (hit, hit == 'TOP' and 'BOTTOM' or 'TOP')) self.loop.draw_screen() time.sleep(0.8) self.search_display_message(reverse) def reconstitute_markup_element_as_string(self, accum, markup): if isinstance(markup, tuple): _, markup = markup return accum + markup def reconstitute_markup_as_string(self, markup): if isinstance(markup, list): return reduce(self.reconstitute_markup_element_as_string, markup, '') return self.reconstitute_markup_element_as_string('', markup) def search_row_has_search_term(self, row, search_regex): # TODO: Cleaner way to detect valid searchable row. if hasattr(row, 'data'): for column in row.data: value = self.reconstitute_markup_as_string(column) if value and search_regex.search(value): return True return False def get_focused_task(self): try: uuid = self.task_list.focus.uuid task = self.model.get_task(uuid) return uuid, task except: 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): self.table = TaskTable(self.config, self.task_config, self.formatter, self.loop.screen, on_select=self.on_select, event=self.event, action_manager=self.action_manager, request_reply=self.request_reply, markers=self.markers, draw_screen_callback=self.loop.draw_screen) def update_task_table(self): self.table.update_data(self.reports[self.report], self.model.tasks) def init_task_list(self): self.model = TaskListModel(self.task_config, self.reports) def init_autocomplete(self): context_list = list(self.contexts.keys()) + ['none'] self.autocomplete = AutoComplete(self.config, extra_filters={'report': self.reports.keys(), 'help': self.help.autocomplete_entries(), 'context': context_list}) def init_command_bar(self): abort_backspace = self.config.get('vit', 'abort_backspace') self.command_bar = CommandBar(autocomplete=self.autocomplete, abort_backspace=abort_backspace, event=self.event) def build_frame(self): self.status_report = urwid.AttrMap(urwid.Text('Welcome to VIT'), 'status') self.status_context = urwid.AttrMap(urwid.Text(''), 'status') self.status_performance = urwid.AttrMap(urwid.Text('', align='center'), 'status') self.status_version = urwid.AttrMap(urwid.Text('vit (%s)' % version.VIT, align='center'), 'status') self.status_tasks_shown = urwid.AttrMap(urwid.Text('', align='right'), 'status') self.status_tasks_completed = urwid.AttrMap(urwid.Text('', align='right'), 'status') self.top_column_left = urwid.Pile([ self.status_report, self.status_context, ]) self.top_column_center = urwid.Pile([ self.status_version, self.status_performance, ]) self.top_column_right = urwid.Pile([ self.status_tasks_shown, self.status_tasks_completed, ]) self.header = urwid.Pile([ urwid.Columns([ self.top_column_left, self.top_column_center, self.top_column_right, ]), urwid.Text('Loading...'), ]) self.footer = MultiWidget() self.init_autocomplete() self.init_command_bar() self.message_bar = urwid.Text('', align='center') self.footer.add_widget('command', self.command_bar) self.footer.add_widget('message', self.message_bar) def command_error(self, returncode, error_message): if returncode != 0: self.activate_message_bar("Command error: %s" % error_message, 'error') return True return False def execute_command(self, args, **kwargs): update_report = True wait = True if 'update_report' in kwargs: update_report = kwargs.pop('update_report') if 'wait' in kwargs: wait = kwargs.pop('wait') if not wait: kwargs['confirm'] = None self.loop.stop() # TODO: This is a shitty hack, if not waiting, then we must # override the confirmation setting for recurring tasks. if not wait and args[0] == 'task': args.insert(1, 'rc.recurrence.confirmation=no') def execute(): returncode, output = self.command.result(args, **kwargs) if self.command_error(returncode, output): return False return True success = execute() if update_report and success: self.update_report() self.loop.start() return success def activate_command_bar(self, op, caption, metadata={}, edit_text=None): metadata['op'] = op self.footer.show_widget('command') self.setup_autocomplete(op) self.command_bar.activate(caption, metadata, edit_text) self.widget.focus_position = 'footer' def activate_command_bar_filter(self): self.activate_command_bar('filter', 'Filter: ') def task_undo(self): self.execute_command(['task', 'undo']) def task_sync(self): self.execute_command(['task', 'sync']) def task_get_latest(self): returncode, stdout, stderr = self.command.run(['task', '+LATEST', 'uuids'], capture_output=True) if returncode == 0: return self.model.get_task(stdout) else: raise RuntimeError("Error retrieving latest task UUID: %s" % stderr) def activate_command_bar_quit_with_confirm(self): if self.confirm: self.activate_command_bar('quit', 'Quit?', {'choices': {'y': True}}) else: self.quit() def activate_command_bar_ex(self): metadata = {} uuid, _ = self.get_focused_task() if uuid: metadata['uuid'] = uuid self.activate_command_bar('ex', ':', metadata) def activate_command_bar_ex_read_wait_task(self): self.activate_command_bar('ex', ':', {}, edit_text='!rw task ') def activate_command_bar_search_forward(self): self.activate_command_bar('search-forward', '/', {'history': 'search'}) def activate_command_bar_search_reverse(self): self.activate_command_bar('search-reverse', '?', {'history': 'search'}) def activate_command_bar_search_next(self): self.search(reverse=self.search_direction_reverse) def activate_command_bar_search_previous(self): self.search(reverse=not self.search_direction_reverse) def activate_command_bar_task_context(self): self.activate_command_bar('context', 'Context: ') def global_escape(self): self.denotation_pop_up.close_pop_up() def activate_command_bar_add(self): self.activate_command_bar('add', 'Add: ') def task_done(self, uuid): success, task = self.model.task_done(uuid) if task: if success: self.table.flash_focus() self.update_report() self.activate_message_bar('Task %s marked done' % self.model.task_id(task['uuid'])) else: self.activate_message_bar('Error: %s' % task, 'error') def task_delete(self, uuid): success, task = self.model.task_delete(uuid) if task: if success: self.table.flash_focus() self.update_report() self.activate_message_bar('Task %s deleted' % self.model.task_id(task['uuid'])) else: self.activate_message_bar('Error: %s' % task, 'error') def task_start_stop(self, uuid): success, task = self.model.task_start_stop(uuid) if task: if success: self.table.flash_focus() self.update_report() self.activate_message_bar('Task %s %s' % (self.model.task_id(task['uuid']), 'started' if task.active else 'stopped')) else: self.activate_message_bar('Error: %s' % task, 'error') def task_action_annotate(self): uuid, _ = self.get_focused_task() if uuid: self.activate_command_bar('annotate', 'Annotate: ', {'uuid': uuid}) self.task_list.focus_by_task_uuid(uuid, self.previous_focus_position) def task_action_delete(self): uuid, task = self.get_focused_task() if task: if self.confirm: self.activate_command_bar('delete', 'Delete task %s? (y/n): ' % self.model.task_id(uuid), {'uuid': uuid, 'choices': {'y': True}}) else: self.task_delete(uuid) self.task_list.focus_by_task_uuid(uuid, self.previous_focus_position) def task_action_denotate(self): uuid, task = self.get_focused_task() if task and task['annotations']: self.denotation_pop_up.open(task) def task_action_modify(self): uuid, _ = self.get_focused_task() if uuid: self.activate_command_bar('modify', 'Modify: ', {'uuid': uuid}) self.task_list.focus_by_task_uuid(uuid, self.previous_focus_position) def task_action_start_stop(self): uuid, task = self.get_focused_task() if task: if self.confirm: self.activate_command_bar('start-stop', '%s task %s? (y/n): ' % (task.active and 'Stop' or 'Start', self.model.task_id(uuid)), {'uuid': uuid, 'choices': {'y': True}}) else: self.task_start_stop(uuid) self.task_list.focus_by_task_uuid(uuid, self.previous_focus_position) def task_action_done(self): uuid, task = self.get_focused_task() if task: if self.confirm: self.activate_command_bar('done', 'Mark task %s done? (y/n): ' % self.model.task_id(uuid), {'uuid': uuid, 'choices': {'y': True}}) else: self.task_done(uuid) self.task_list.focus_by_task_uuid(uuid, self.previous_focus_position) def task_action_priority(self): uuid, _ = self.get_focused_task() if uuid: choices = {} for choice in self.task_config.priority_values: key = choice.lower() or 'n' choices[key] = choice self.activate_command_bar('priority', 'Priority (%s): ' % '/'.join(choices), {'uuid': uuid, 'choices': choices}) def task_action_project(self): uuid, _ = self.get_focused_task() if uuid: self.activate_command_bar('project', 'Project: ', {'uuid': uuid}) def task_action_tags(self): uuid, _ = self.get_focused_task() if uuid: self.activate_command_bar('tag', 'Tag: ', {'uuid': uuid}) def task_action_wait(self): uuid, _ = self.get_focused_task() if uuid: self.activate_command_bar('wait', 'Wait: ', {'uuid': uuid}) def task_action_edit(self): uuid, _ = self.get_focused_task() if uuid: self.execute_command(['task', uuid, 'edit'], wait=self.wait) self.task_list.focus_by_task_uuid(uuid, self.previous_focus_position) def task_action_show(self): uuid, _ = self.get_focused_task() if uuid: self.execute_command(['task', uuid, 'info'], update_report=False) self.task_list.focus_by_task_uuid(uuid) def get_available_task_columns(self): returncode, stdout, stderr = self.command.run(['task', '_columns'], capture_output=True) if returncode == 0: self.available_columns = stdout.split() else: raise RuntimeError("Error retrieving available task columns: %s" % stderr) def refresh_blocking_task_uuids(self): returncode, stdout, stderr = self.command.run(['task', 'uuids', '+BLOCKING'], capture_output=True) if returncode == 0: self.blocking_task_uuids = stdout.split() else: raise RuntimeError("Error retrieving blocking task UUIDs: %s" % stderr) def setup_autocomplete(self, op): callback = self.command_bar.set_edit_text_callback() if op in ('filter', 'add', 'modify'): self.autocomplete.setup(callback) elif op in ('ex',): filters = ('report', 'column', 'project', 'tag', 'help') filter_config = copy.deepcopy(self.autocomplete.default_filter_config) filter_config['report'] = { 'include_unprefixed': True, 'root_only': True, } filter_config['help'] = { 'include_unprefixed': True, 'root_only': True, } self.autocomplete.setup(callback, filters=filters, filter_config=filter_config) elif op in ('project',): filters = ('project',) filter_config = { 'project': { 'prefixes': [], 'include_unprefixed': True, }, } self.autocomplete.setup(callback, filters=filters, filter_config=filter_config) elif op in ('tag',): filters = ('tag',) filter_config = { 'tag': { 'prefixes': ['+', '-'], 'include_unprefixed': True, }, } self.autocomplete.setup(callback, filters=filters, filter_config=filter_config) elif op in ('context',): filters = ('context',) filter_config = { 'context': { 'include_unprefixed': True, 'root_only': True, }, } self.autocomplete.setup(callback, filters=filters, filter_config=filter_config) def activate_message_bar(self, message='', message_type='status'): self.footer.show_widget('message') display = 'message %s' % message_type self.message_bar.set_text((display, message)) def update_status_report(self): filtered_report = 'task %s %s' % (self.report, ' '.join(self.extra_filters)) self.status_report.original_widget.set_text(filtered_report) def update_status_performance(self, seconds): text = 'Exec. time: %dms' % (seconds * 1000) self.status_performance.original_widget.set_text(text) # TODO: This is riding on top of status_performance currently, should # probably be abstracted def update_status_key_cache(self): keys = self.key_cache.get() text = 'Key cache: %s' % keys if keys else '' self.status_performance.original_widget.set_text(text) def update_status_context(self): text = 'Context: %s' % self.context if self.context else 'No context' self.status_context.original_widget.set_text(text) def update_status_tasks_shown(self): num_tasks = len(self.model.tasks) text = '%s %s shown' % (num_tasks, num_tasks == 1 and 'task' or 'tasks') self.status_tasks_shown.original_widget.set_text(text) def update_status_tasks_completed(self): returncode, stdout, stderr = self.command.run(['task', '+COMPLETED', 'count'], capture_output=True) if returncode == 0: num_tasks = int(stdout.strip()) text = '%s %s completed' % (num_tasks, num_tasks == 1 and 'task' or 'tasks') self.status_tasks_completed.original_widget.set_text(text) 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() # NOTE: Don't see any other way to clear the old palette. self.loop.screen._palette = {} self.loop.screen.register_palette(self.theme) self.loop.screen.clear() self.loop.widget = self.widget def update_report(self, report=None): start = time.time() self.task_list = self.table.listbox self.previous_focus_position = self.task_list.focus_position if self.task_list.list_walker else 0 if report: self.report = report self.set_active_context() self.task_config.get_projects() self.refresh_blocking_task_uuids() self.formatter.recalculate_due_datetimes() context_filters = self.contexts[self.context]['filter'] if self.context and self.reports[self.report].get('context', 1) else [] try: self.model.update_report(self.report, context_filters=context_filters, extra_filters=self.extra_filters) except VitException as err: self.activate_message_bar(str(err), 'error') return self.update_task_table() self.update_status_report() self.update_status_context() self.update_status_tasks_shown() self.update_status_tasks_completed() self.header.contents[1] = (self.table.header, self.header.options()) self.denotation_pop_up = DenotationPopupLauncher(self.task_list, self.formatter, self.loop.screen, event=self.event, request_reply=self.request_reply, action_manager=self.action_manager) self.widget.body = self.denotation_pop_up self.autocomplete.refresh() end = time.time() self.update_status_performance(end - start) def build_main_widget(self, report=None): if report: self.report = report self.init_task_list() self.build_frame() self.widget = MainFrame( urwid.ListBox([]), header=self.header, footer=self.footer, key_cache=self.key_cache, action_manager=self.action_manager, refresh=self.refresh, ) self.update_report(self.report) vit-2.3.2/vit/autocomplete.py000066400000000000000000000262021451336657600162170ustar00rootroot00000000000000from functools import reduce import re from vit import util from vit.process import Command class AutoComplete: def __init__(self, config, default_filters=None, extra_filters=None): self.default_filters = default_filters or ('column', 'project', 'tag') self.extra_filters = extra_filters or {} self.default_filter_config = { 'column': { 'suffixes': [':'], }, 'project': { 'prefixes': ['project:'], }, 'tag': { 'prefixes': ['+', '-'], }, } self.config = config self.command = Command(self.config) for ac_type in self.default_filters: setattr(self, ac_type, []) for ac_type, items in list(self.extra_filters.items()): setattr(self, ac_type, items) self.reset() def refresh(self, filters=None): filters = filters or self.default_filters for ac_type in filters: setattr(self, ac_type, self.refresh_type(ac_type)) def get_refresh_type_command(self, ac_type): command = [ 'task', ] if ac_type == 'project': command.extend([ 'rc.list.all.projects=yes', '_projects', ]) else: command.extend([ '_%ss' % ac_type ]) return command def refresh_type(self, ac_type): returncode, stdout, stderr = self.command.run(self.get_refresh_type_command(ac_type), capture_output=True) if returncode == 0: items = list(filter(lambda x: True if x else False, stdout.split("\n"))) if ac_type == 'project': items = self.create_project_entries(items) return items else: raise RuntimeError("Error running command '%s': %s" % (command, stderr)) def create_project_entries(self, projects): def projects_reducer(projects_accum, project): def project_reducer(project_accum, part): project_accum.append(part) project_string = '.'.join(project_accum) if not project_string in projects_accum: projects_accum.append(project_string) return project_accum reduce(project_reducer, project.split('.'), []) return projects_accum return reduce(projects_reducer, projects, []) def make_entries(self, filters, filter_config): entries = [] for ac_type in filters: items = getattr(self, ac_type) include_unprefixed = filter_config[ac_type]['include_unprefixed'] if ac_type in filter_config and 'include_unprefixed' in filter_config[ac_type] else False type_prefixes = filter_config[ac_type]['prefixes'] if ac_type in filter_config and 'prefixes' in filter_config[ac_type] else [] type_suffixes = filter_config[ac_type]['suffixes'] if ac_type in filter_config and 'suffixes' in filter_config[ac_type] else [] if include_unprefixed: for item in items: entries.append((ac_type, item)) for prefix in type_prefixes: for item in items: entries.append((ac_type, '%s%s' % (prefix, item))) for suffix in type_suffixes: for item in items: entries.append((ac_type, '%s%s' % (item, suffix))) entries.sort() return entries def make_space_escape_regex(self, filters, filter_config): prefix_parts = [] for ac_type in filters: items = getattr(self, ac_type) type_prefixes = filter_config[ac_type]['prefixes'] if ac_type in filter_config and 'prefixes' in filter_config[ac_type] else [] for prefix in type_prefixes: prefix_parts.append(re.escape(prefix)) prefix_or = "|".join(prefix_parts) return re.compile("^(%s).+[ ]+.+$" % prefix_or) def setup(self, text_callback, filters=None, filter_config=None): if self.is_setup: self.reset() self.text_callback = text_callback if not filters: filters = self.default_filters if not filter_config: filter_config = self.default_filter_config self.refresh() self.entries = self.make_entries(filters, filter_config) self.space_escape_regex = self.make_space_escape_regex(filters, filter_config) self.root_only_filters = list(filter(lambda f: True if f in filter_config and 'root_only' in filter_config[f] else False, filters)) self.is_setup = True def teardown(self): self.is_setup = False self.entries = [] self.root_only_filters = [] self.callback = None self.deactivate() def reset(self): self.teardown() def activate(self, text, edit_pos, reverse=False): if not self.is_setup: return if self.activated: self.send_tabbed_text(text, edit_pos, reverse) return if self.can_tab(text, edit_pos): self.activated = True self.generate_tab_options(text, edit_pos) self.send_tabbed_text(text, edit_pos, reverse) def deactivate(self): self.activated = False self.idx = None self.tab_options = [] self.root_search = False self.search_fragment = None self.prefix = None self.suffix = None self.partial = None def send_tabbed_text(self, text, edit_pos, reverse): tabbed_text, final_edit_pos = self.next_tab_item(text, reverse) self.text_callback(tabbed_text, final_edit_pos) def generate_tab_options(self, text, edit_pos): if self.root_search: if self.has_root_only_filters(): self.tab_options = list(map(lambda e: e[1], filter(lambda e: True if e[0] in self.root_only_filters else False, self.entries))) else: self.tab_options = list(map(lambda e: e[1], self.entries)) else: self.parse_text(text, edit_pos) exp = self.regexify(self.search_fragment) if self.has_root_only_filters(): if self.search_fragment_is_root(): self.tab_options = list(map(lambda e: e[1], filter(lambda e: True if e[0] in self.root_only_filters and exp.match(e[1]) else False, self.entries))) else: self.tab_options = list(map(lambda e: e[1], filter(lambda e: True if e[0] not in self.root_only_filters and exp.match(e[1]) else False, self.entries))) else: self.tab_options = list(map(lambda e: e[1], filter(lambda e: True if exp.match(e[1]) else False, self.entries))) def has_root_only_filters(self): return len(self.root_only_filters) > 0 def search_fragment_is_root(self): return len(self.prefix_parts) == 0 or self.is_help_request() def regexify(self, string): return re.compile(re.escape(string)) # TODO: This is way hacky, not sure of a cleaner way to handle # multi-spaced search terms, of which help is the only one now. def is_help_request(self): return self.prefix_parts[0] in ['help'] def add_space_escaping(self, text): if self.space_escape_regex.match(text): parts = text.split(':', 1) if len(parts) > 1: return "%s:'%s'" % (parts[0], parts[1]) else: return "'%s'" % text else: return text def remove_space_escaping(self, text): return text.replace('\\ ', ' ') def parse_text(self, text, edit_pos): full_prefix = text[:edit_pos] self.prefix_parts = list(map(self.add_space_escaping, util.string_to_args_on_whitespace(full_prefix))) if not self.prefix_parts: self.search_fragment = self.prefix = full_prefix self.suffix = text[(edit_pos + 1):] elif self.is_help_request(): self.search_fragment = full_prefix self.prefix = self.suffix = '' else: self.search_fragment = self.prefix_parts.pop() self.prefix = ' '.join(self.prefix_parts) self.suffix = text[(edit_pos + 1):] self.search_fragment = self.remove_space_escaping(self.search_fragment) def can_tab(self, text, edit_pos): if edit_pos == 0: if text == '': self.root_search = True return True return False previous_pos = edit_pos - 1 next_pos = edit_pos + 1 return text[edit_pos:next_pos] in (' ', '') and text[previous_pos:edit_pos] not in (' ', '') def assemble(self, tab_option, solo_match=False): if not tab_option.endswith(":"): tab_option = self.add_space_escaping(tab_option) if solo_match: tab_option += ' ' parts = [self.prefix, tab_option, self.suffix] tabbed_text = ' '.join(filter(lambda p: True if p else False, parts)) parts.pop() edit_pos_parts = ' '.join(filter(lambda p: True if p else False, parts)) edit_pos_final = len(edit_pos_parts) return tabbed_text, edit_pos_final def partial_match(self): if self.partial: return ref_item = self.tab_options[0] ref_item_length = len(ref_item) tab_options_length = len(self.tab_options) pos = len(self.search_fragment) self.partial = self.search_fragment while pos < ref_item_length: pos += 1 exp = self.regexify(ref_item[:pos]) ref_result = list(filter(lambda o: True if exp.match(o) else False, self.tab_options)) if len(ref_result) == tab_options_length: self.partial = ref_item[:pos] else: break return self.partial != self.search_fragment def initial_idx(self, reverse): return len(self.tab_options) - 1 if reverse else 0 def increment_index(self, reverse): if self.idx == None: self.idx = self.initial_idx(reverse) else: if reverse: self.idx = self.idx - 1 if self.idx > 0 else len(self.tab_options) - 1 else: self.idx = self.idx + 1 if self.idx < len(self.tab_options) - 1 else 0 def next_tab_item(self, text, reverse): tabbed_text = '' edit_pos = None if self.root_search: self.increment_index(reverse) tabbed_text = self.tab_options[self.idx] else: if len(self.tab_options) == 0: tabbed_text = text elif len(self.tab_options) == 1: tabbed_text, edit_pos = self.assemble(self.tab_options[0], solo_match=True) else: if self.partial_match(): tabbed_text, edit_pos = self.assemble(self.partial) else: if self.idx == None and self.partial == self.tab_options[self.initial_idx(reverse)]: self.increment_index(reverse) self.increment_index(reverse) tabbed_text, edit_pos = self.assemble(self.tab_options[self.idx]) return tabbed_text, edit_pos vit-2.3.2/vit/base_list_box.py000066400000000000000000000120421451336657600163300ustar00rootroot00000000000000import re import urwid BRACKETS_REGEX = re.compile("[<>]") class BaseListBox(urwid.ListBox): """Maps task list shortcuts to default ListBox class. """ def __init__(self, body, event=None, request_reply=None, action_manager=None): self.previous_focus_position = None self.list_walker = body self.event = event self.request_reply = request_reply self.action_manager = action_manager self.key_cache = self.request_reply.request('application:key_cache') self.register_managed_actions() return super().__init__(body) def get_top_middle_bottom_rows(self, size): #try: if len(self.list_walker) == 0: return None, None, None ((_, focused, _, _, _), (_, top_list), (_, bottom_list)) = self.calculate_visible(size) top = top_list[len(top_list) - 1][0] if len(top_list) > 0 else None bottom = bottom_list[len(bottom_list) - 1][0] if len(bottom_list) > 0 else None top_list_reversed = [] # Neither top_list.reverse() nor reversed(top_list) works here, WTF? while True: if len(top_list) > 0: row = top_list.pop() top_list_reversed.append(row) else: break assembled_list = top_list_reversed + [(focused, )] + (bottom_list if bottom else []) middle_list_position = (len(assembled_list) // 2) + 1 middle_list = assembled_list[:middle_list_position] if len(assembled_list) > 1 else assembled_list middle = middle_list.pop()[0] return (top or focused), middle, (bottom or focused) #except: # # TODO: Log this? # return None, None, None def register_managed_actions(self): self.action_manager_registrar = self.action_manager.get_registrar() self.action_manager_registrar.register('LIST_UP', self.keypress_up) self.action_manager_registrar.register('LIST_DOWN', self.keypress_down) self.action_manager_registrar.register('LIST_PAGE_UP', self.keypress_page_up) self.action_manager_registrar.register('LIST_PAGE_DOWN', self.keypress_page_down) self.action_manager_registrar.register('LIST_HOME', self.keypress_home) self.action_manager_registrar.register('LIST_END', self.keypress_end) self.action_manager_registrar.register('LIST_SCREEN_TOP', self.keypress_screen_top) self.action_manager_registrar.register('LIST_SCREEN_MIDDLE', self.keypress_screen_middle) self.action_manager_registrar.register('LIST_SCREEN_BOTTOM', self.keypress_screen_bottom) self.action_manager_registrar.register('LIST_FOCUS_VALIGN_CENTER', self.keypress_focus_valign_center) # NOTE: The non-standard key presses work around infinite recursion while # allowing the up, down, page up, and page down keys to be controlled # from the keybinding file. def keypress_up(self, size): self.keypress(size, '') def keypress_down(self, size): self.keypress(size, '') def keypress_page_up(self, size): self.keypress(size, '') def keypress_page_down(self, size): self.keypress(size, '') def keypress_home(self, size): if len(self.body) > 0: self.set_focus(0) def keypress_end(self, size): 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) if top: self.set_focus(top.position) def keypress_screen_middle(self, size): _, middle, _ = self.get_top_middle_bottom_rows(size) if middle: self.set_focus(middle.position) def keypress_screen_bottom(self, size): _, _, bottom = self.get_top_middle_bottom_rows(size) if bottom: self.set_focus(bottom.position) def keypress_focus_valign_center(self, size): 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 # keys to be managed via keybinding configuration. They are # converted back to standard key presses here. if key in ['', '', '', '']: key = re.sub(BRACKETS_REGEX, '', key).lower() return key def list_action_executed(self, size, key): pass def eat_other_keybindings(self): return False def keypress(self, size, key): keys = self.key_cache.get(key) if self.action_manager_registrar.execute_handler(keys, size): self.list_action_executed(size, key) return None if self.eat_other_keybindings() and self.key_cache.is_keybinding(keys): return None key = self.transform_special_keys(key) return super().keypress(size, key) vit-2.3.2/vit/color.py000066400000000000000000000275311451336657600146420ustar00rootroot00000000000000import re import urwid from functools import cmp_to_key, wraps from vit.color_mappings import task_256_to_urwid_256, task_bright_to_color VALID_COLOR_MODIFIERS = [ 'bold', 'underline', ] INVALID_COLOR_MODIFIERS = [ 'inverse', ] class TaskColorConfig: """Colorized task output. """ def __init__(self, config, task_config, theme, theme_alt_backgrounds): self.config = config self.task_config = task_config self.theme = theme self.theme_alt_backgrounds = theme_alt_backgrounds self.include_subprojects = self.config.get('color', 'include_subprojects') self.task_256_to_urwid_256 = task_256_to_urwid_256() # NOTE: Because Taskwarrior disables color on piped commands, and I don't # see any portable way to get output from a system command in Python # 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(r'^color\.')) self.project_display_attrs = self.get_project_display_attrs() if self.include_subprojects: self.add_project_children() self.inject_alt_background_display_attrs() def inject_alt_background_display_attrs(self): for display_attr in self.display_attrs.copy(): name, display_attr_foreground_16, display_attr_background_16, _mono, display_attr_foreground_256, display_attr_background_256 = display_attr for modifier, alt_backgrounds in self.theme_alt_backgrounds.items(): display_attr_modifier = name + modifier if self.has_display_attr(name): self.display_attrs_available[display_attr_modifier] = True alt_background_16, alt_background_256 = alt_backgrounds new_background_16 = alt_background_16 if display_attr_background_16 == '' else display_attr_background_16 new_background_256 = alt_background_256 if display_attr_background_256 == '' else display_attr_background_256 self.display_attrs.append(self.make_display_attr(display_attr_modifier, display_attr_foreground_256, new_background_256, foreground_16=display_attr_foreground_16, background_16=new_background_16)) else: self.display_attrs_available[display_attr_modifier] = False def add_project_children(self): color_prefix = 'color.project.' for (display_attr, fg16, bg16, m, fg256, bg256) in self.project_display_attrs: for entry in self.task_config.projects: attr = '%s%s' % (color_prefix, entry) if not self.has_display_attr(attr) and attr.startswith('%s.' % display_attr): self.display_attrs_available[attr] = True self.display_attrs.append((attr, fg16, bg16, m, fg256, bg256)) def has_display_attr(self, display_attr): return display_attr in self.display_attrs_available and self.display_attrs_available[display_attr] def get_project_display_attrs(self): return sorted([(a, fg16, bg16, m, fg256, bg256) for (a, fg16, bg16, m, fg256, bg256) in self.display_attrs if self.display_attrs_available[a] and self.is_project_display_attr(a)], reverse=True) def is_project_display_attr(self, display_attr): return display_attr[0:14] == 'color.project.' def convert_color_config(self, color_config): display_attrs_available = {} display_attrs = [] for key, value in color_config.items(): foreground, background = self.convert_colors(value) available = self.has_color_config(foreground, background) display_attrs_available[key] = available if available: display_attrs.append(self.make_display_attr(key, foreground, background)) return display_attrs_available, display_attrs def make_display_attr(self, display_attr, foreground, background, foreground_16=None, background_16=None): # TODO: 256 colors need to be translated down to 16 color mode. foreground_16 = '' if foreground_16 is None else foreground_16 background_16 = '' if background_16 is None else background_16 return (display_attr, foreground_16, background_16, '', foreground, background) def has_color_config(self, foreground, background): return foreground != '' or background != '' def convert_colors(self, color_config): # TODO: Maybe a fancy regex eventually... color_config = task_bright_to_color(color_config).strip() starts_with_on = color_config[0:3] == 'on ' parts = list(map(lambda p: p.strip(), color_config.split('on '))) foreground, background = (parts[0], parts[1]) if len(parts) > 1 else (None, parts[0]) if starts_with_on else (parts[0], None) foreground_parts, background_parts = self.make_color_parts(foreground, background) return self.convert_color_parts(foreground_parts), self.convert_color_parts(background_parts) def convert_color_parts(self, color_parts): sorted_parts = self.sort_color_parts(color_parts) 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]] return color_parts 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): parts = color_parts.split() if color_parts else [] return parts def is_modifier(self, elem): return elem in VALID_COLOR_MODIFIERS def sort_color_parts(self, color_parts): def comparator(first, second): if self.is_modifier(first) and not self.is_modifier(second): return 1 elif not self.is_modifier(first) and self.is_modifier(second): return -1 else: return 0 return sorted(color_parts, key=cmp_to_key(comparator)) class TaskColorizer: class Decorator: def color_enabled(func): @wraps(func) def verify_color_enabled(self, *args, **kwargs): return func(self, *args, **kwargs) if self.color_enabled else None return verify_color_enabled def __init__(self, color_config): self.color_config = color_config self.color_enabled = self.color_config.color_enabled self.theme_alt_backgrounds = self.color_config.theme_alt_backgrounds self.background_modifier = '' self.init_keywords() def init_keywords(self): try: self.keywords = self.color_config.task_config.subtree('color.')['keyword'] self.any_keywords_regex = re.compile('(%s)' % '|'.join(self.keywords.keys())) except KeyError: self.keywords = [] self.any_keywords_regex = None def has_keywords(self, text): return self.any_keywords_regex and self.any_keywords_regex.search(text) def extract_keyword_parts(self, text): if self.has_keywords(text): parts = self.any_keywords_regex.split(text) first_part = parts.pop(0) return first_part, parts return None, None def set_background_modifier(self, modifier=''): self.background_modifier = modifier if modifier in self.theme_alt_backgrounds else '' def add_background_modifier(self, display_attr): return display_attr + self.background_modifier def make_display_attr(self, display_attr): return self.add_background_modifier(display_attr) def get_display_attr(self, display_attr): return self.make_display_attr(display_attr) if self.color_config.has_display_attr(display_attr) else None @Decorator.color_enabled def project_none(self): return self.get_display_attr('color.project.none') @Decorator.color_enabled def project(self, project): return self.get_display_attr('color.project.%s' % project) @Decorator.color_enabled def tag_none(self): return self.get_display_attr('color.tag.none') @Decorator.color_enabled def tag(self, tag): custom_value = 'color.tag.%s' % tag if self.color_config.has_display_attr(custom_value): return self.make_display_attr(custom_value) elif self.color_config.has_display_attr('color.tagged'): return self.make_display_attr('color.tagged') return None @Decorator.color_enabled def uda_none(self, name): return self.get_display_attr('color.uda.%s.none' % name) @Decorator.color_enabled def uda_common(self, name, value): custom_value = 'color.uda.%s' % name if self.color_config.has_display_attr(custom_value): return self.make_display_attr(custom_value) elif self.color_config.has_display_attr('color.uda'): return self.make_display_attr('color.uda') return None @Decorator.color_enabled def uda_string(self, name, value): if not value: return self.uda_none(name) else: custom_value = 'color.uda.%s.%s' % (name, value) if self.color_config.has_display_attr(custom_value): return self.make_display_attr(custom_value) return self.uda_common(name, value) @Decorator.color_enabled def uda_numeric(self, name, value): return self.uda_string(name, value) @Decorator.color_enabled def uda_duration(self, name, value): return self.uda_string(name, value) @Decorator.color_enabled def uda_date(self, name, value): # TODO: Maybe some special string indicators here? return self.uda_common(name, value) if value else self.uda_none(name) @Decorator.color_enabled def uda_indicator(self, name, value): return self.uda_string(name, value) @Decorator.color_enabled def keyword(self, text): return self.get_display_attr('color.keyword.%s' % text) @Decorator.color_enabled def blocking(self): return self.get_display_attr('color.blocking') @Decorator.color_enabled def due(self, state): return self.get_display_attr('color.%s' % state) if state else None @Decorator.color_enabled def status(self, status): if status == 'completed' or status == 'deleted': value = 'color.%s' % status if self.color_config.has_display_attr(value): return self.make_display_attr(value) return None @Decorator.color_enabled def blocked(self, depends): return self.get_display_attr('color.blocked') @Decorator.color_enabled def active(self, active): return self.get_display_attr('color.active') if active else None @Decorator.color_enabled def recurring(self, recur): return self.get_display_attr('color.recurring') @Decorator.color_enabled def scheduled(self, scheduled): return self.get_display_attr('color.scheduled') if scheduled else None @Decorator.color_enabled def until(self, until): return self.get_display_attr('color.until') if until else None vit-2.3.2/vit/color_mappings.py000066400000000000000000000046731451336657600165420ustar00rootroot00000000000000import re BRIGHT_REGEX = re.compile('.*bright.*') def task_256_to_urwid_256(): manual_map = { 'red': 'dark red', 'green': 'dark green', 'blue': 'dark blue', 'cyan': 'dark cyan', 'magenta': 'dark magenta', 'gray': 'light gray', 'yellow': 'brown', 'color0': 'black', 'color1': 'dark red', 'color2': 'dark green', 'color3': 'brown', 'color4': 'dark blue', 'color5': 'dark magenta', 'color6': 'dark cyan', 'color7': 'light gray', 'color8': 'dark gray', 'color9': 'light red', 'color10': 'light green', 'color11': 'yellow', 'color12': 'light blue', 'color13': 'light magenta', 'color14': 'light cyan', 'color15': 'white', } manual_map.update(task_color_gray_to_g()) manual_map.update(task_color_to_h()) manual_map.update(task_rgb_to_h()) return manual_map def task_bright_to_color(color_string): color_map = { 'bright black': 'color8', 'bright red': 'color9', 'bright green': 'color10', 'bright yellow': 'color11', 'bright blue': 'color12', 'bright magenta': 'color13', 'bright cyan': 'color14', 'bright white': 'color15', } if BRIGHT_REGEX.match(color_string): for bright_color in color_map: color_string = color_string.replace(bright_color, color_map[bright_color]) return color_string def task_color_gray_to_g(): color_map = {} for i in range(0, 24): gray_key = 'gray%d' % i color_key = 'color%d' % (i + 232) # NOTE: This is an approximation of the conversion, close enough! value = 'g%d' % (i * 4) color_map[gray_key] = value color_map[color_key] = value return color_map def task_color_to_h(): color_map = {} for i in range(16, 232): key = 'color%d' % i value = 'h%d' % i color_map[key] = value return color_map def task_rgb_to_h(): index_to_hex = [ '0', '6', '8', 'a', 'd', 'f', ] color_map = {} count = 0 for r in range(0, 6): for g in range(0, 6): for b in range(0, 6): key = 'rgb%d%d%d' % (r, g, b) value = '#%s%s%s' % (index_to_hex[r], index_to_hex[g], index_to_hex[b]) color_map[key] = value count += 1 return color_map vit-2.3.2/vit/command_bar.py000066400000000000000000000125321451336657600157610ustar00rootroot00000000000000import urwid from vit.readline import Readline class CommandBar(urwid.Edit): """Custom urwid.Edit class for the command bar. """ def __init__(self, **kwargs): self.event = kwargs.pop('event') self.autocomplete = kwargs.pop('autocomplete') self.abort_backspace = kwargs.pop('abort_backspace') self.metadata = None self.history = CommandBarHistory() self.readline = Readline(self) return super().__init__(**kwargs) def keypress(self, size, key): """Overrides Edit.keypress method. """ if key not in ('tab', 'shift tab'): self.autocomplete.deactivate() if 'choices' in self.metadata: self.quit({'choice': self.metadata['choices'].get(key)}) return None elif key in ('up',): self.readline.keypress('ctrl p') return None elif key in ('down',): self.readline.keypress('ctrl n') return None elif key in ('enter', 'esc'): text = self.get_edit_text().strip() if text and key == 'enter': self.history.add(self.metadata['history'], text) self.quit({'key': key, 'text': text}) return None elif key in ('tab', 'shift tab'): if self.is_autocomplete_op(): text = self.get_edit_text() kwargs = {} if key in ('shift tab',): kwargs['reverse'] = True self.autocomplete.activate(text, self.edit_pos, **kwargs) return None elif key in self.readline.keys(): return self.readline.keypress(key) elif self.is_aborting_backspace(key): self.quit({'key': key}) return None return super().keypress(size, key) def is_aborting_backspace(self, key): return key == 'backspace' and self.abort_backspace and not self.get_edit_text() def is_autocomplete_op(self): return self.metadata['op'] not in ['search-forward', 'search-reverse'] def set_edit_text(self, text, edit_pos=None): ret = super().set_edit_text(text) if not edit_pos: edit_pos = len(text) self.set_edit_pos(edit_pos) return ret def set_command_prompt(self, caption, edit_text=None): self.set_caption(caption) if edit_text is not None: self.set_edit_text(edit_text) def activate(self, caption, metadata, edit_text=None): self.set_metadata(metadata) self.set_command_prompt(caption, edit_text) def deactivate(self): self.set_command_prompt('', '') self.history.cleanup(self.metadata['op']) self.set_metadata(None) def quit(self, metadata_args={}): data = {'metadata': self.get_metadata(), **metadata_args} self.deactivate() self.event.emit('command-bar:keypress', data) # remove focus from command bar def get_metadata(self): return self.metadata.copy() if self.metadata else None def prepare_metadata(self, metadata): if metadata: if 'history' not in metadata: metadata['history'] = metadata['op'] return metadata def set_metadata(self, metadata): self.metadata = self.prepare_metadata(metadata) def set_edit_text_callback(self): return self.set_edit_text class CommandBarHistory: """Holds command-specific history for the command bar. """ def __init__(self): self.commands = {} self.scrolling = False def add(self, command, text): if not self.exists(command): self.commands[command] = {'items': ['']} self.commands[command]['items'].insert(len(self.get_items(command)) - 1, text) self.set_scrolling(command, False) def previous(self, command): if self.exists(command): if not self.scrolling: self.set_scrolling(command, True) return self.current(command) elif self.get_idx(command) > 0: self.move_idx(command, -1) return self.current(command) return False def next(self, command): if self.exists(command) and self.scrolling and self.get_idx(command) < self.last_idx(command): self.move_idx(command, 1) return self.current(command) return False def cleanup(self, command): self.set_scrolling(command, False) def get_items(self, command): return self.commands[command]['items'] def get_idx(self, command): return self.commands[command]['idx'] def set_idx(self, command, idx): self.commands[command]['idx'] = idx def set_scrolling(self, command, scroll): if self.exists(command): # Don't count the ending empty string when setting the initial # index for scrolling to start. self.set_idx(command, self.last_idx(command) - 1) self.scrolling = scroll def move_idx(self, command, increment): self.set_idx(command, self.get_idx(command) + increment) def exists(self, command): return command in self.commands def current(self, command): return self.get_items(command)[self.get_idx(command)] if self.exists(command) else False def last_idx(self, command): return len(self.get_items(command)) - 1 if self.exists(command) else None vit-2.3.2/vit/command_line.py000066400000000000000000000002501451336657600161360ustar00rootroot00000000000000from vit import parse_options, Application def main(): options, filters = parse_options() Application(options, filters) if __name__ == '__main__': main() vit-2.3.2/vit/config/000077500000000000000000000000001451336657600144075ustar00rootroot00000000000000vit-2.3.2/vit/config/config.sample.ini000066400000000000000000000230251451336657600176370ustar00rootroot00000000000000# This is the user configuration file for VIT. # All configuration options are listed here, commented out, and showing their # default value when not otherwise set. # The format is standard INI file format. Configuration sections are enclosed # by brackets. Configuration values should be placed in their relevant section, # using a 'name = value' format. Boolean values can be expressed by the # following: # True values: 1, yes, true (case insensitive) # False values: All other values. [taskwarrior] # Full path to the Taskwarrior configuration file. Tilde will be expanded to # the user's home directory. # NOTE: This setting is overridden by the TASKRC environment variable. #taskrc = ~/.taskrc [vit] # The keybinding map to use. This maps actions registered with VIT to be fired # when the user presses the specific keys configured in the keybindings file. # Possible keybindings are in the 'keybinding' directory, and the setting's # value should be the filename minus the .ini extension. The default keybinding # configuration is modeled heavily on the legacy VIT keybindings, and inspired # by vi/vim. #default_keybindings = vi # The theme to use. This allows control over the colors used in the # application itself. Possible themes are in the 'theme' directory, and the # setting's value should be the filename minus the .py extension. # Note that the theme does not control any coloring related to tasks -- this # is controlled via the color settings in the Taskwarrior configuration. #theme = default # Boolean. If true, VIT will ask for confirmation before marking a task as done, # deleting a task, or quitting VIT. Set to false to disable the prompts. #confirmation = True # Boolean. If true, VIT will show the output of the task command and wait for # enter. If false, VIT will not show output of the task command after # modifications to a task are made. #wait = True # Boolean. If true, VIT will enable mouse support for actions such as selecting # list items. #mouse = False # Boolean. If true, hitting backspace against an empty prompt aborts the prompt. #abort_backspace = False # Boolean. If true, VIT will focus on the newly added task. Note: the new task must be #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 # or filters are passed at the command line. #default_report = next # The default Taskwarrior report to load when VIT first starts, if filters are # passed at the command line with no report. #default_filter_only_report = next # Boolean. If true, reports with the primary sort of project ascending will # indent subprojects. If you use deeply nested subprojects, you'll probably # like this setting. #indent_subprojects = True # Boolean. If true, display report rows with alternating background colors. #row_striping = True [marker] # Boolean. Enables markers. Markers are configurable labels that appear on the # left side of a report to indicate information about a task when the displayed # report does not contain the related column. # For example, let's suppose you have a 'notes' UDA configured. You'd like to # see some indication that a task has a note, without displaying the full note # column in reports. You could configure a marker for that custom UDA as # follows: # uda.notes.label = (N) # Then, when a listed task has a note associated with it, you'll see the # marker '(N)' displayed in the leftmost column of any report that displays the # task in question. #enabled = True # What columns to generate markers for. Can either be 'all' for all columns, or # a comma separated list of columns to enable markers for. Possible columns # are: # depends,description,due,project,recur,scheduled,start,status,tags,until #columns = all # The header label for the markers column when it is displayed. #header_label = # Boolean. If true, an associated color value must be configured in the # Taskwarrior configuration in order for the marker to be displayed. If false, # and no Taskwarrior color configuration is present for the matching marker, # then it is not displayed. # For example, if this is set to True, then for the above-mentioned 'notes' # marker to be displayed, a matching Taskwarrior color configuration for the # 'notes' UDA must be present, e.g.: # color.uda.notes=yellow #require_color = True # Boolean. If true, subprojects of a project will also display the configured # root project's marker, if the subproject itself does not have its own marker # configured. # For example, given the following projects: # Foo # Foo.Bar # If this value is set to True, and the Foo project has a configured marker, # then Foo.Bar would also display Foo's marker. #include_subprojects = True # Below are listed all of the available markers, with their default label. # To disable a specific marker, set its label to empty. Any section enclosed # in brackets should be replaced by the appropriate identifier, eg. # [project_name] with the actual name of a project. #active.label = (A) #blocked.label = (BD) #blocking.label = (BG) #completed.label = (C) #deleted.label = (X) #due.label = (D) #due.today.label = (DT) #keyword.label = (K) #keyword.[keyword_name].label = #overdue.label = (OD) #project.label = (P) #project.none.label = #project.[project_name].label = #recurring.label = (R) #scheduled.label = (S) #tag.label = (T) #tag.none.label = #tag.[tag_name].label = #uda.label = #uda.priority.label = (PR) #uda.[uda_name].label = [color] # Boolean. If true, use the colors in Taskwarrior's configuration to colorize # reports. Note that VIT uses a fundamentally different paradigm for # colorization, which combines tying coloring to associated report columns in # combination with markers (see above). This setting works independently of # Taskwarriors 'color' config setting. #enabled = True # Boolean. If true, subprojects of a project will also display the configured # root project's color, if the subproject itself does not have its own color # configured. # For example, given the following projects: # Foo # Foo.Bar # If this value is set to True, and the Foo project has a configured color, # then Foo.Bar would also display Foo's color. #include_subprojects = True # For the Taskwarrior color configuration, there are three special values: # color.project.none # color.tag.none # color.uda.[uda_name].none # If any of these are configured for color, then the label below will be used # in the related column to display the color configuration. #none_label = [NONE] [keybinding] # This section allows you to override the configured keybindings, associate # additional keybindings with VIT actions, and set up macros triggered by a # keybinding. # Meta keys are enclosed in angle brackets, variables are enclosed in curly # brackets. Keybindings here can either be: # - Associated with a single VIT action # - A macro that describes a series of key presses to replay # For VIT actions, the form is: # keys[,keys] = {ACTION_NAME} # For example, to associate the keybinding 'zz' with the undo action: # zz = {ACTION_TASK_UNDO} # To only disable a keybinding, use the special noop action: # w = {ACTION_NOOP} # wa = {ACTION_TASK_WAIT} # The above would disable the task wait action for the 'w' key, and instead # assign it to the 'wa' keybinding. # For capital letter keybindings, use the letter directly: # D = {ACTION_TASK_DONE} # For a list of available actions, run 'vit --list-actions'. # A great reference for many of the available meta keys, and understanding the # default keybindings is the 'keybinding/vi.ini' file. # For macros, the form is: # keys[,keys] = keypresses # For example, to map the 'o' key to opening the OneNote script, passing it # the currently focused task UUID: # o = :!wr onenote {TASK_UUID} # The special '{TASK_[attribute]}' variable can be used in any macro, and it # will be replaced with the value of the attribute for the currently # highlighted task. Any attribute listed in 'task _columns' is supported, e.g. # o = :!wr echo project is {TASK_PROJECT} # Multiple keybindings can be associated with the same action/macro, simply # separate the keybindings with a comma: # z,zz = {ACTION_TASK_UNDO} # 'Special' keys are indicated by enclosing them in brackets. VIT supports the # following special keys on either side of the keybinding declaration, by # internally translating them into the single character: # # # # # # # Under the hood, VIT uses the Urwid mappings for keyboard input: # http://urwid.org/manual/userinput.html # # Any modifier, navigation, or function keys can be described in the VIT # keybinding configuration by wrapping them in angle brackets, matching the # correct Urwid keyboard input structure: # # e = :!wr echo do something # = :!wr echo you used a function key vit-2.3.2/vit/config_parser.py000066400000000000000000000410501451336657600163350ustar00rootroot00000000000000from shutil import copyfile import os import re import shlex import datetime from functools import reduce try: import configparser except ImportError: import ConfigParser as configparser from vit import env, xdg from vit.process import Command SORT_ORDER_CHARACTERS = ['+', '-'] SORT_COLLATE_CHARACTERS = ['/'] VIT_CONFIG_FILE = 'config.ini' 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 # violate this convention, and transform them into a single additional branch # of value CONFIG_STRING_LEAVES_DEFAULT_BRANCH CONFIG_STRING_LEAVES = [ 'color.calendar.due', 'color.due', 'color.label', 'dateformat', ] CONFIG_STRING_LEAVES_DEFAULT_BRANCH = 'default' DEFAULTS = { 'taskwarrior': { 'taskrc': '~/.taskrc', }, 'vit': { 'default_keybindings': 'vi', 'theme': 'default', 'confirmation': True, 'wait': True, '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', 'default_filter_only_report': 'next', 'indent_subprojects': True, 'row_striping': True, }, 'marker': { 'enabled': True, 'header_label': '', 'columns': 'all', 'require_color': True, 'include_subprojects': True, }, 'color': { 'enabled': True, 'include_subprojects': True, 'none_label': '[NONE]', }, } # strftime() works differently on Windows, test here. test_datetime = datetime.datetime(1900, 1, 1) NO_PAD_FORMAT_CODE = '-' if test_datetime.strftime('%-d') == '1' else '#' if test_datetime.strftime('%#d') == '1' else '' DATE_FORMAT_MAPPING = { 'm': '%%%sm' % NO_PAD_FORMAT_CODE, # 1 or 2 digit month number, eg '1', '12' 'M': '%m', # 2 digit month number, eg '01', '12' 'd': '%%%sd' % NO_PAD_FORMAT_CODE, # 1 or 2 digit day of month number¸ eg '1', '12' 'D': '%d', # 2 digit day of month number, eg '01', '30' 'y': '%y', # 2 digit year, eg '12', where the century is assumed to be '20', therefore '2012' 'Y': '%Y', # 4 digit year, eg '2015' 'h': '%%%sH' % NO_PAD_FORMAT_CODE, # 1 or 2 digit hours, eg '1', '23' 'H': '%H', # 2 digit hours, eg '01', '23' 'n': '%%%sM' % NO_PAD_FORMAT_CODE, # 1 or 2 digit minutes, eg '1', '59' 'N': '%M', # 2 digit minutes, eg '01', '59' 's': '%%%sS' % NO_PAD_FORMAT_CODE, # 1 or 2 digit seconds, eg '1', '59' 'S': '%S', # 2 digit seconds, eg '01', '59' 'v': '%%%sU' % NO_PAD_FORMAT_CODE, # 1 or 2 digit week number, eg '1', '52' 'V': '%U', # 2 digit week number, eg '01', '52' 'a': '%a', # 3-character English day name abbreviation, eg 'mon', 'tue' 'A': '%A', # Complete English day name, eg 'monday', 'tuesday' 'b': '%b', # 3-character English month name abbreviation, eg 'jan', 'feb' 'B': '%B', # Complete English month name, eg 'january', 'february' 'j': '%%%sj' % NO_PAD_FORMAT_CODE, # 1, 2 or 3 digit day-of-year number, sometimes referred to as a Julian date, eg '1', '11', or '365' 'J': '%j', # 3 digit day of year number, sometimes referred to as a Julian date, eg '001', '011', or '365' } class ConfigParser: def __init__(self, loader): self.loader = loader self.config = configparser.ConfigParser() self.config.optionxform = str self.user_config_dir = self.loader.user_config_dir self.user_config_filepath = '%s/%s' % (self.user_config_dir, VIT_CONFIG_FILE) if not self.config_file_exists(self.user_config_filepath): self.optional_create_config_file(self.user_config_filepath) self.config.read(self.user_config_filepath) self.taskrc_path = self.get_taskrc_path() self.validate_taskrc() self.defaults = DEFAULTS self.set_config_data() def set_config_data(self): self.subproject_indentable = self.is_subproject_indentable() self.row_striping_enabled = self.is_row_striping_enabled() self.confirmation_enabled = self.is_confirmation_enabled() self.wait_enabled = self.is_wait_enabled() self.mouse_enabled = self.is_mouse_enabled() def validate_taskrc(self): try: open(self.taskrc_path, 'r').close() except FileNotFoundError: message = """ %s not found. VIT requires a properly configured TaskWarrior instance in the current environment. Execute the 'task' binary with no arguments to initialize a new configuration. """ % (self.taskrc_path) print(message) exit(1) def config_file_exists(self, filepath): try: open(filepath, 'r').close() return True except FileNotFoundError: return False def optional_create_config_file(self, filepath): prompt = "%s doesn't exist, create? (y/n): " % filepath try: answer = input(prompt) except: answer = raw_input(prompt) if answer in ['y', 'Y']: self.create_config_file(filepath) prompt = "\n%s created. This is the default user configuration file. \n\nIt is heavily commented with explanations and lists the default values for all available user configuration variables. Check it out!\n\nPress enter to continue..." % filepath try: input(prompt) except: raw_input(prompt) def create_config_file(self, filepath): dirname = os.path.dirname(filepath) try: os.mkdir(dirname) except FileExistsError: pass basedir = os.path.dirname(os.path.realpath(__file__)) copyfile('%s/config/config.sample.ini' % basedir, filepath) def get(self, section, key): default = DEFAULTS[section][key] try: value = self.config.get(section, key) return self.transform(key, value, default) except (configparser.NoSectionError, configparser.NoOptionError, ValueError): return default def items(self, section): try: return self.config.items(section) except configparser.NoSectionError: return [] def has_section(self, section): return self.config.has_section(section) 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')) if not os.path.exists(taskrc_path): xdg_dir = xdg.get_xdg_config_dir(taskrc_path, "task") if xdg_dir: taskrc_path = os.path.join(xdg_dir, "taskrc") return taskrc_path def is_subproject_indentable(self): return self.get('report', 'indent_subprojects') def is_row_striping_enabled(self): return self.get('report', 'row_striping') def is_confirmation_enabled(self): return self.get('vit', 'confirmation') def is_wait_enabled(self): return self.get('vit', 'wait') def is_mouse_enabled(self): return self.get('vit', 'mouse') 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 = [] self.projects = [] self.contexts = {} self.reports = {} self.disallowed_reports = [ 'timesheet', ] self.command = Command(self.config) self.get_task_config() self.get_projects() self.set_config_data() def set_config_data(self): self.print_empty_columns = self.is_truthy(self.subtree('print.empty.columns')) self.priority_values = self.get_priority_values() def get_task_config(self): self.task_config = [] returncode, stdout, stderr = self.command.run('task _show', capture_output=True) if returncode == 0: lines = list(filter(lambda x: True if x else False, stdout.split("\n"))) for line in lines: hierarchy, values = line.split('=', maxsplit=1) self.task_config.append((hierarchy, values)) else: raise RuntimeError('Error parsing task config: %s' % stderr) def get_active_context(self): returncode, stdout, stderr = self.command.run('task _get rc.context', capture_output=True) if returncode == 0: return stdout.strip() else: raise RuntimeError('Error retrieving active context: %s' % stderr) def get_projects(self): returncode, stdout, stderr = self.command.run('task _projects', capture_output=True) if returncode == 0: self.projects = stdout.split("\n") # Ditch the trailing newline. self.projects.pop() else: raise RuntimeError('Error parsing task projects: %s' % stderr) def get_priority_values(self): return self.subtree('uda.priority.values').split(',') def transform_string_leaves(self, hierarchy): if hierarchy in CONFIG_STRING_LEAVES: hierarchy += '.%s' % CONFIG_STRING_LEAVES_DEFAULT_BRANCH return hierarchy def filter_to_dict(self, matcher_regex): lines = self.filter(matcher_regex) return {key: value for key, value in lines} def filter(self, matcher_regex): return list(filter(lambda config_pair: re.match(matcher_regex, config_pair[0]), self.task_config)) def subtree(self, matcher, walk_subtree=True): matcher_regex = matcher if walk_subtree: matcher_regex = r'%s' % (('^%s' % matcher).replace(r'.', r'\.')) full_tree = {} lines = self.filter(matcher_regex) for (hierarchy, value) in lines: # NOTE: This is necessary in order to convert Taskwarrior's dotted # config syntax into a full tree, as some leaves are both branches # and leaves. hierarchy = self.transform_string_leaves(hierarchy) parts = hierarchy.split('.') tree_location = full_tree while True: if len(parts): part = parts.pop(0) if part not in tree_location: tree_location[part] = {} if len(parts) else value tree_location = tree_location[part] else: break if walk_subtree: parts = matcher.split('.') subtree = full_tree while True: if len(parts): part = parts.pop(0) if part in subtree: subtree = subtree[part] else: return subtree else: return full_tree def parse_sort_column(self, column_string): order = collate = None parts = list(column_string) while True: if len(parts): letter = parts.pop() if letter in SORT_ORDER_CHARACTERS: order = letter == '+' and 'ascending' or 'descending' elif letter in SORT_COLLATE_CHARACTERS: collate = True else: parts.append(letter) break else: break column = ''.join(parts) return (column, order, collate) def translate_date_markers(self, string): return reduce(lambda accum, code: accum.replace(code[0], code[1]), list(DATE_FORMAT_MAPPING.items()), string) def get_contexts(self): contexts = {} self.get_task_config() subtree = self.subtree('context.') for context, filters in list(subtree.items()): contexts[context] = { 'filter': self.parse_context_filters(context, filters), } self.contexts = contexts return self.contexts def parse_context_filters(self, context, filters): # Filters can be a string (pre-2.6.0 definition, context.work=+work) # or a dict (2.6.0 and newer, context.work.read=+work and # context.work.write=+work). if type(filters) is dict: if 'read' in filters: filters = filters['read'] else: return [] # Only contexts with read component defined should be considered. filters = shlex.split(re.sub(FILTER_PARENS_REGEX, r' \1 ', filters)) final_filters = [f for f in filters if not FILTER_EXCLUSION_REGEX.match(f)] return final_filters def get_reports(self): reports = {} subtree = self.subtree('report.') for report, attrs in list(subtree.items()): if report in self.disallowed_reports: continue reports[report] = { 'name': report, 'subproject_indentable': False, } if 'columns' in attrs: reports[report]['columns'] = attrs['columns'].split(',') if 'context' in attrs: reports[report]['context'] = int(attrs['context']) if 'description' in attrs: reports[report]['description'] = attrs['description'] if 'filter' in attrs: # Allows quoted strings. # Adjust for missing spaces around parentheses. filters = shlex.split(re.sub(FILTER_PARENS_REGEX, r' \1 ', attrs['filter'])) reports[report]['filter'] = [f for f in filters if not FILTER_EXCLUSION_REGEX.match(f)] if 'labels' in attrs: reports[report]['labels'] = attrs['labels'].split(',') else: reports[report]['labels'] = [column.title() for column in attrs['columns'].split(',')] if 'sort' in attrs: columns = attrs['sort'].split(',') reports[report]['sort'] = [self.parse_sort_column(c) for c in columns] if 'dateformat' in attrs: reports[report]['dateformat'] = self.translate_date_markers(attrs['dateformat']) self.reports = reports # Another pass is needed after all report data has been parsed. for report_name, report in self.reports.items(): self.reports[report_name] = self.rectify_report(report_name, report) return self.reports def rectify_report(self, report_name, report): report['subproject_indentable'] = self.has_project_column(report_name) and self.has_primary_project_ascending_sort(report) return report def is_truthy(self, value): value = str(value) return value.lower() in ['y', 'yes', 'on', 'true', '1'] def has_project_column(self, report_name): return self.get_column_index(report_name, 'project') is not None def has_primary_project_ascending_sort(self, report): try: primary_sort = report['sort'][0] except KeyError: return False return primary_sort[0] == 'project' and primary_sort[1] == 'ascending' def get_column_index(self, report_name, column): return self.reports[report_name]['columns'].index(column) if column in self.reports[report_name]['columns'] else None def get_column_label(self, report_name, column): column_index = self.get_column_index(report_name, column) return self.reports[report_name]['labels'][column_index] vit-2.3.2/vit/debug.py000066400000000000000000000004501451336657600146010ustar00rootroot00000000000000import pprint import tempfile LOG_FILE = '%s/%s' % (tempfile.gettempdir(), 'vit-debug.log') pp = pprint.PrettyPrinter() pf = pprint.PrettyPrinter(stream=open(LOG_FILE,'w')) def console(*args, **kwargs): pp.pprint(*args, **kwargs) def file(*args, **kwargs): pf.pprint(*args, **kwargs) vit-2.3.2/vit/denotation.py000066400000000000000000000145231451336657600156650ustar00rootroot00000000000000import urwid from vit import util from vit.base_list_box import BaseListBox OVERLAY_WIDTH = 50 OVERLAY_HEIGHT = 15 class AnnotationFrame(urwid.Frame): def __init__(self, body, **kwargs): self.listbox = body.original_widget return super().__init__(body, **kwargs) def keypress(self, size, key): """Overrides Frame.keypress method. """ if key in ('tab', 'shift tab'): self.focus_position = self.focus_position == 'body' and 'footer' or 'body' self.listbox.update_focus_blur(self.focus_position == 'body' and 'focus' or 'blur') return None return super().keypress(size, key) class AnnotationListBox(BaseListBox): """Maps denotation list shortcuts to default ListBox class. """ def __init__(self, body, event=None, request_reply=None, action_manager=None): super().__init__(body, event=event, request_reply=request_reply, action_manager=action_manager) self.init_event_listeners() def list_action_executed(self, size, key): data = { 'size': size, } self.event.emit('denotation-list:keypress', data) def init_event_listeners(self): def signal_handler(): self.update_focus() self.modified_signal = urwid.connect_signal(self.list_walker, 'modified', signal_handler) def get_selected_annotation(self): return self.list_walker[self.focus_position].annotation def update_focus(self): if self.previous_focus_position != self.focus_position: if self.previous_focus_position is not None: self.update_focus_attr({}, position=self.previous_focus_position) self.update_focus_attr('reveal focus') self.previous_focus_position = self.focus_position def update_focus_attr(self, attr, position=None): attr = attr if isinstance(attr, dict) else {None: attr} if position is None: position = self.focus_position self.list_walker[position].row.set_attr_map(attr) def update_focus_blur(self, op): attr = 'reveal focus' if op == 'focus' else 'button cancel' self.update_focus_attr(attr) class SelectableRow(urwid.WidgetWrap): """Wraps 'urwid.Columns' to make it selectable. """ def __init__(self, annotation, position, widths, formatter, align="left", on_select=None, space_between=2): self.annotation = annotation self.position = position self._columns = urwid.Columns([ (widths['entry'], urwid.Text(annotation['entry'].strftime(formatter.annotation), align=align)), (widths['description'], urwid.Text(annotation['description'], align=align)), ], dividechars=space_between) self.row = urwid.AttrMap(self._columns, '') # Wrap 'urwid.Columns'. super().__init__(self.row) # A hook which defines the behavior that is executed when a specified key is pressed. self.on_select = on_select def __repr__(self): return "{}(entry={}, description={})".format(self.__class__.__name__, self.annotation['entry'], self.annotation['description']) def selectable(self): return True def keypress(self, size, key): if self.on_select: key = self.on_select(self, size, key) return key class DenotationPopUpDialog(urwid.WidgetWrap): """A dialog for denotating tasks.""" # TODO: Is this necessary? Copied from examples. signals = ['close'] def __init__(self, task, listbox, formatter, event=None): self.task = task self.listbox = listbox self.formatter = formatter self.event = event self.uuid = self.task['uuid'] denotate_button = urwid.AttrWrap(urwid.Button("Denotate"), '', 'button action') cancel_button = urwid.AttrWrap(urwid.Button("Cancel"), '', 'button cancel') # TODO: Calculate these dynamically? widths = { 'entry': 10, 'description': 32, } annotations = [SelectableRow(a, idx, widths, self.formatter) for idx, a in enumerate(self.task['annotations'])] self.listbox.list_walker[:] = annotations self.listbox.focus_position = 0 def denotate(button): self._emit("close") data = { 'uuid': self.uuid, 'annotation': self.listbox.get_selected_annotation(), } self.event.emit("task:denotate", data) urwid.connect_signal(denotate_button.original_widget, 'click', denotate) urwid.connect_signal(cancel_button.original_widget, 'click', lambda button:self._emit("close")) frame = AnnotationFrame( urwid.Padding(self.listbox, left=1, right=1), header=urwid.Text("Select the annotation, then select 'Denotate'\nTab to move focus between list/buttons\n"), footer=urwid.Columns([urwid.Padding(denotate_button, left=5, right=6), urwid.Padding(cancel_button, left=6, right=5)]), ) padded_frame = urwid.Padding(frame, left=1, right=1) box = urwid.LineBox(padded_frame, "Denotate task %s" % util.task_id_or_uuid_short(task)) super().__init__(urwid.AttrWrap(box, 'pop_up')) class DenotationPopupLauncher(urwid.PopUpLauncher): def __init__(self, original_widget, formatter, screen, event=None, request_reply=None, action_manager=None): self.formatter = formatter self.screen = screen self.event = event self.request_reply = request_reply self.action_manager = action_manager self.listbox = AnnotationListBox(urwid.SimpleFocusListWalker([]), event=self.event, request_reply=self.request_reply, action_manager=self.action_manager) super().__init__(original_widget) def set_task(self, task): self.task = task def open(self, task): self.set_task(task) self.open_pop_up() def create_pop_up(self): pop_up = DenotationPopUpDialog(self.task, self.listbox, self.formatter, event=self.event) urwid.connect_signal(pop_up, 'close', lambda button: self.close_pop_up()) return pop_up def get_pop_up_parameters(self): screen_width, _ = self.screen.get_cols_rows() left = round((screen_width - OVERLAY_WIDTH) / 2) return {'left':left, 'top':1, 'overlay_width':OVERLAY_WIDTH, 'overlay_height':OVERLAY_HEIGHT} vit-2.3.2/vit/env.py000066400000000000000000000001111451336657600142750ustar00rootroot00000000000000import os os.environ['IS_VIT_INSTANCE'] = "1" user = os.environ.copy() vit-2.3.2/vit/event.py000066400000000000000000000007571451336657600146460ustar00rootroot00000000000000# TODO: Use urwid signals instead of custom event emitter? class Emitter: """Simple event listener/emitter. """ def __init__(self): self.listeners = {} def listen(self, event, listener): if event not in self.listeners: self.listeners[event] = [] self.listeners[event].append(listener) def emit(self, event, data=None): if event in self.listeners: for listener in self.listeners[event]: listener(data) vit-2.3.2/vit/exception.py000066400000000000000000000001341451336657600155100ustar00rootroot00000000000000class VitException(Exception): """Base class for exceptions in this module.""" pass vit-2.3.2/vit/formatter/000077500000000000000000000000001451336657600151455ustar00rootroot00000000000000vit-2.3.2/vit/formatter/__init__.py000066400000000000000000000151751451336657600172670ustar00rootroot00000000000000import math from datetime import datetime try: from zoneinfo import ZoneInfo except ImportError: from backports.zoneinfo import ZoneInfo from vit.util import unicode_len TIME_UNIT_MAP = { 'seconds': { 'label': 's', 'divisor': 1, 'threshold': 60, }, 'minutes': { 'label': 'm', 'divisor': 60, 'threshold': 3600, }, 'hours': { 'label': 'h', 'divisor': 3600, 'threshold': 86400, }, 'days': { 'label': 'd', 'divisor': 86400, 'threshold': 86400 * 14, }, 'weeks': { 'label': 'w', 'divisor': 86400 * 7, 'threshold': 86400 * 90, }, 'months': { 'label': 'mo', 'divisor': 86400 * 30, 'threshold': 86400 * 365, }, 'years': { 'label': 'y', 'divisor': 86400 * 365, }, } class Formatter: def __init__(self, column, report, formatter_base, blocking_task_uuids, **kwargs): self.column = column self.report = report self.formatter = formatter_base self.colorizer = self.formatter.task_colorizer self.blocking_task_uuids = blocking_task_uuids def format(self, obj, task): if not obj: return self.empty() obj = str(obj) return (unicode_len(obj), self.markup_element(obj)) def empty(self): return (0, '') def markup_element(self, obj): return (self.colorize(obj), obj) def markup_none(self, color): if color: return (unicode_len(self.formatter.none_label), (color, self.formatter.none_label)) else: return self.empty() def colorize(self, obj): return None def filter_by_blocking_task_uuids(self, depends): return [ task for task in depends if task['uuid'] in self.blocking_task_uuids ] class Marker(Formatter): def __init__(self, report, defaults, report_marker_columns, blocking_task_uuids): super().__init__(None, report, defaults, blocking_task_uuids) self.columns = report_marker_columns self.labels = self.formatter.markers.labels self.udas = self.formatter.markers.udas self.require_color = self.formatter.markers.require_color self.set_column_attrs() def set_column_attrs(self): any(setattr(self, 'mark_%s' % column, column in self.columns) for column in self.formatter.markers.markable_columns) class Number(Formatter): pass class String(Formatter): pass class Duration(Formatter): def format(self, obj, task): if not obj: return self.empty() formatted_duration = self.format_duration(obj) return (unicode_len(formatted_duration), self.markup_element(obj, formatted_duration)) def format_duration(self, obj): return obj def markup_element(self, obj, formatted_duration): return (self.colorize(obj), formatted_duration) class DateTime(Formatter): def __init__(self, column, report, defaults, blocking_task_uuids, **kwargs): self.custom_formatter = None if not 'custom_formatter' in kwargs else kwargs['custom_formatter'] super().__init__(column, report, defaults, blocking_task_uuids) def format(self, dt, task): if not dt: return self.empty() formatted_date = self.format_datetime(dt, 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) def markup_element(self, dt, formatted_date, task): return (self.colorize(dt, task), formatted_date) def colorize(self, dt, task): return None def format_duration_vague(self, seconds): test = seconds sign = '' unit = 'seconds' if seconds < 0: test = -seconds sign = '-' if test <= TIME_UNIT_MAP['seconds']['threshold']: # Handled by defaults pass elif test <= TIME_UNIT_MAP['minutes']['threshold']: unit = 'minutes' elif test <= TIME_UNIT_MAP['hours']['threshold']: unit = 'hours' elif test <= TIME_UNIT_MAP['days']['threshold']: unit = 'days' elif test <= TIME_UNIT_MAP['weeks']['threshold']: unit = 'weeks' elif test <= TIME_UNIT_MAP['months']['threshold']: unit = 'months' else: unit = 'years' age = test // TIME_UNIT_MAP[unit]['divisor'] return '%s%d%s' % (sign, age, TIME_UNIT_MAP[unit]['label']) def age(self, dt): if dt == None: return '' now = datetime.now().astimezone() seconds = (now - dt).total_seconds() return self.format_duration_vague(seconds) def countdown(self, dt): if dt == None: return '' now = datetime.now().astimezone() if dt < now: return '' seconds = (dt - now).total_seconds() return self.format_duration_vague(seconds) def relative(self, dt): if dt == None: return '' now = datetime.now().astimezone() seconds = (dt - now).total_seconds() return self.format_duration_vague(seconds) def remaining(self, dt): if dt == None: return '' now = datetime.now().astimezone() if dt < now: return '' seconds = (dt - now).total_seconds() return self.format_duration_vague(seconds) def epoch(self, dt): if dt == None: return '' return str(round((dt - self.formatter.epoch_datetime).total_seconds())) # Taken from https://github.com/dannyzed/julian def julian(self, dt): if dt == None: return '' a = math.floor((14-dt.month)/12) y = dt.year + 4800 - a m = dt.month + 12*a - 3 jdn = dt.day + math.floor((153*m + 2)/5) + 365*y + math.floor(y/4) - math.floor(y/100) + math.floor(y/400) - 32045 jd = jdn + (dt.hour - 12) / 24 + dt.minute / 1440 + dt.second / 86400 + dt.microsecond / 86400000000 return str(jd) def iso(self, dt): if dt == None: return '' dt = dt.replace(tzinfo=ZoneInfo('UTC')) return dt.isoformat() class List(Formatter): def format(self, obj, task): if not obj: return self.empty() formatted = self.format_list(obj, task) return (unicode_len(formatted), self.markup_element(obj, formatted)) def markup_element(self, obj, formatted): return (self.colorize(obj), formatted) def format_list(self, obj, task): return ','.join(obj) if obj else '' vit-2.3.2/vit/formatter/depends.py000066400000000000000000000005261451336657600171440ustar00rootroot00000000000000from vit import util from vit.formatter import List class Depends(List): def format_list(self, depends, task): return ','.join(list(map(lambda t: str(util.task_id_or_uuid_short(t)), self.filter_by_blocking_task_uuids(depends)))) if depends else '' def colorize(self, depends): return self.colorizer.blocked(depends) vit-2.3.2/vit/formatter/depends_count.py000066400000000000000000000003161451336657600203510ustar00rootroot00000000000000from vit.formatter.depends import Depends class DependsCount(Depends): def format_list(self, depends, task): return '[%d]' % len(self.filter_by_blocking_task_uuids(depends)) if depends else '' vit-2.3.2/vit/formatter/depends_indicator.py000066400000000000000000000003401451336657600211720ustar00rootroot00000000000000from vit.formatter.depends import Depends class DependsIndicator(Depends): def format_list(self, depends, task): return self.formatter.indicator_dependency if self.filter_by_blocking_task_uuids(depends) else '' vit-2.3.2/vit/formatter/depends_list.py000066400000000000000000000001201451336657600201650ustar00rootroot00000000000000from vit.formatter.depends import Depends class DependsList(Depends): pass vit-2.3.2/vit/formatter/description.py000066400000000000000000000046751451336657600200560ustar00rootroot00000000000000from 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 = unicode_len(description) colorized_description = self.colorize_description(description) if task['annotations']: annotation_width, colorized_description = self.format_combined(colorized_description, task) if annotation_width > width: width = annotation_width return (width, colorized_description) def format_description_truncated(self, 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) return annotation_width, colorized_description + [(None, "\n"), (None, formatted_annotations)] def format_annotations(self, task): def reducer(accum, annotation): width, formatted_list = accum formatted = self.format_annotation(annotation) new_width = unicode_len(formatted) if new_width > width: width = new_width formatted_list.append(formatted) return (width, formatted_list) width, formatted_annotations = reduce(reducer, task['annotations'], (0, [])) return width, "\n".join(formatted_annotations) def format_annotation(self, annotation): return ' %s %s' % (annotation['entry'].strftime(self.formatter.annotation), annotation['description']) def colorize(self, part): return self.colorizer.keyword(part) def colorize_description(self, description): first_part, rest = self.colorizer.extract_keyword_parts(description) if first_part is None: return [(None, description)] def reducer(accum, part): if part: last_color, last_part = accum[-1] color, part = self.markup_element(part) if color == last_color: accum[-1] = (last_color, last_part + part) return accum else: return accum + [(color, part)] return accum return reduce(reducer, rest, [self.markup_element(first_part)]) vit-2.3.2/vit/formatter/description_combined.py000066400000000000000000000001441451336657600217010ustar00rootroot00000000000000from vit.formatter.description import Description class DescriptionCombined(Description): pass vit-2.3.2/vit/formatter/description_count.py000066400000000000000000000015771451336657600212640ustar00rootroot00000000000000from 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 = unicode_len(description) colorized_description = self.colorize_description(description) if not task['annotations']: return (width, colorized_description) else: count_width, colorized_description = self.format_count(colorized_description, task) return (width + count_width, colorized_description) def format_count(self, colorized_description, task): count_string = self.format_annotation_count(task) return unicode_len(count_string), colorized_description + [(None, count_string)] def format_annotation_count(self, task): return " [%d]" % len(task['annotations']) vit-2.3.2/vit/formatter/description_desc.py000066400000000000000000000005451451336657600210440ustar00rootroot00000000000000from 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 (unicode_len(description), colorized_description) vit-2.3.2/vit/formatter/description_oneline.py000066400000000000000000000011751451336657600215570ustar00rootroot00000000000000from vit.formatter.description import Description class DescriptionOneline(Description): def format_combined(self, colorized_description, task): formatted_annotations = self.format_annotations(task) return 0, colorized_description + [(None, formatted_annotations)] def format_annotations(self, task): formatted_annotations = [self.format_annotation(annotation) for annotation in task['annotations']] return "".join(formatted_annotations) def format_annotation(self, annotation): return ' %s %s' % (annotation['entry'].strftime(self.formatter.annotation), annotation['description']) vit-2.3.2/vit/formatter/description_truncated.py000066400000000000000000000007431451336657600221170ustar00rootroot00000000000000from 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 = unicode_len(truncated_description) colorized_description = self.colorize_description(truncated_description) return (width, colorized_description) vit-2.3.2/vit/formatter/description_truncated_count.py000066400000000000000000000013161451336657600233240ustar00rootroot00000000000000from 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 = unicode_len(truncated_description) colorized_description = self.colorize_description(truncated_description) if not task['annotations']: return (width, colorized_description) else: count_width, colorized_description = self.format_count(colorized_description, task) return (width + count_width, colorized_description) vit-2.3.2/vit/formatter/due.py000066400000000000000000000003751451336657600163010ustar00rootroot00000000000000from vit.formatter import DateTime class Due(DateTime): def get_due_state(self, due, task): return self.formatter.get_due_state(due, task) def colorize(self, due, task): return self.colorizer.due(self.get_due_state(due, task)) vit-2.3.2/vit/formatter/due_age.py000066400000000000000000000001751451336657600171130ustar00rootroot00000000000000from vit.formatter.due import Due class DueAge(Due): def format_datetime(self, due, task): return self.age(due) vit-2.3.2/vit/formatter/due_countdown.py000066400000000000000000000002111451336657600203660ustar00rootroot00000000000000from vit.formatter.due import Due class DueCountdown(Due): def format_datetime(self, due, task): return self.countdown(due) vit-2.3.2/vit/formatter/due_epoch.py000066400000000000000000000002011451336657600174430ustar00rootroot00000000000000from vit.formatter.due import Due class DueEpoch(Due): def format_datetime(self, due, task): return self.epoch(due) vit-2.3.2/vit/formatter/due_formatted.py000066400000000000000000000001051451336657600203350ustar00rootroot00000000000000from vit.formatter.due import Due class DueFormatted(Due): pass vit-2.3.2/vit/formatter/due_iso.py000066400000000000000000000001751451336657600171510ustar00rootroot00000000000000from vit.formatter.due import Due class DueIso(Due): def format_datetime(self, due, task): return self.iso(due) vit-2.3.2/vit/formatter/due_julian.py000066400000000000000000000002031451336657600176310ustar00rootroot00000000000000from vit.formatter.due import Due class DueJulian(Due): def format_datetime(self, due, task): return self.julian(due) vit-2.3.2/vit/formatter/due_relative.py000066400000000000000000000002071451336657600201660ustar00rootroot00000000000000from vit.formatter.due import Due class DueRelative(Due): def format_datetime(self, due, task): return self.relative(due) vit-2.3.2/vit/formatter/due_remaining.py000066400000000000000000000002111451336657600203170ustar00rootroot00000000000000from vit.formatter.due import Due class DueRemaining(Due): def format_datetime(self, due, task): return self.remaining(due) vit-2.3.2/vit/formatter/end.py000066400000000000000000000001021451336657600162560ustar00rootroot00000000000000from vit.formatter import DateTime class End(DateTime): pass vit-2.3.2/vit/formatter/end_age.py000066400000000000000000000001641451336657600171020ustar00rootroot00000000000000from vit.formatter.end import End class EndAge(End): def format(self, end, task): return self.age(end) vit-2.3.2/vit/formatter/end_countdown.py000066400000000000000000000002001451336657600203550ustar00rootroot00000000000000from vit.formatter.end import End class EndCountdown(End): def format(self, end, task): return self.countdown(end) vit-2.3.2/vit/formatter/end_epoch.py000066400000000000000000000001701451336657600174410ustar00rootroot00000000000000from vit.formatter.end import End class EndEpoch(End): def format(self, end, task): return self.epoch(end) vit-2.3.2/vit/formatter/end_iso.py000066400000000000000000000001641451336657600171400ustar00rootroot00000000000000from vit.formatter.end import End class EndIso(End): def format(self, end, task): return self.iso(end) vit-2.3.2/vit/formatter/end_julian.py000066400000000000000000000001721451336657600176270ustar00rootroot00000000000000from vit.formatter.end import End class EndJulian(End): def format(self, end, task): return self.julian(end) vit-2.3.2/vit/formatter/end_relative.py000066400000000000000000000001761451336657600201640ustar00rootroot00000000000000from vit.formatter.end import End class EndRelative(End): def format(self, end, task): return self.relative(end) vit-2.3.2/vit/formatter/end_remaining.py000066400000000000000000000002001451336657600203060ustar00rootroot00000000000000from vit.formatter.end import End class EndRemaining(End): def format(self, end, task): return self.remaining(end) vit-2.3.2/vit/formatter/entry.py000066400000000000000000000001041451336657600166530ustar00rootroot00000000000000from vit.formatter import DateTime class Entry(DateTime): pass vit-2.3.2/vit/formatter/entry_age.py000066400000000000000000000002001451336657600174640ustar00rootroot00000000000000from vit.formatter.entry import Entry class EntryAge(Entry): def format(self, entry, task): return self.age(entry) vit-2.3.2/vit/formatter/entry_countdown.py000066400000000000000000000002141451336657600207550ustar00rootroot00000000000000from vit.formatter.entry import Entry class EntryCountdown(Entry): def format(self, entry, task): return self.countdown(entry) vit-2.3.2/vit/formatter/entry_epoch.py000066400000000000000000000002041451336657600200320ustar00rootroot00000000000000from vit.formatter.entry import Entry class EntryEpoch(Entry): def format(self, entry, task): return self.epoch(entry) vit-2.3.2/vit/formatter/entry_formatted.py000066400000000000000000000001151451336657600207220ustar00rootroot00000000000000from vit.formatter.entry import Entry class EntryFormatted(Entry): pass vit-2.3.2/vit/formatter/entry_iso.py000066400000000000000000000002001451336657600175220ustar00rootroot00000000000000from vit.formatter.entry import Entry class EntryIso(Entry): def format(self, entry, task): return self.iso(entry) vit-2.3.2/vit/formatter/entry_julian.py000066400000000000000000000002061451336657600202200ustar00rootroot00000000000000from vit.formatter.entry import Entry class EntryJulian(Entry): def format(self, entry, task): return self.julian(entry) vit-2.3.2/vit/formatter/entry_relative.py000066400000000000000000000002121451336657600205460ustar00rootroot00000000000000from vit.formatter.entry import Entry class EntryRelative(Entry): def format(self, entry, task): return self.relative(entry) vit-2.3.2/vit/formatter/entry_remaining.py000066400000000000000000000002141451336657600207060ustar00rootroot00000000000000from vit.formatter.entry import Entry class EntryRemaining(Entry): def format(self, entry, task): return self.remaining(entry) vit-2.3.2/vit/formatter/id.py000066400000000000000000000000751451336657600161150ustar00rootroot00000000000000from vit.formatter import Number class Id(Number): pass vit-2.3.2/vit/formatter/id_number.py000066400000000000000000000000761451336657600174660ustar00rootroot00000000000000from vit.formatter.id import Id class IdNumber(Id): pass vit-2.3.2/vit/formatter/markers.py000066400000000000000000000144301451336657600171650ustar00rootroot00000000000000import unicodedata from vit.formatter import Marker from vit.util import unicode_len class Markers(Marker): def format(self, _, task): text_markup = [] width = 0 if self.mark_tags: width, text_markup = self.format_tags(width, text_markup, task['tags']) if self.mark_project: width, text_markup = self.format_project(width, text_markup, task['project']) if self.mark_due and task['due']: width, text_markup = self.format_due(width, text_markup, task['due'], task) if self.mark_status: width, text_markup = self.format_status(width, text_markup, task['status']) if self.mark_depends and self.filter_by_blocking_task_uuids(task['depends']): width, text_markup = self.format_blocked(width, text_markup, task['depends']) if self.mark_start and task['start']: width, text_markup = self.format_active(width, text_markup, task['start'], task) if self.mark_recur and task['recur']: width, text_markup = self.format_recurring(width, text_markup, task['recur']) if self.mark_scheduled and task['scheduled']: width, text_markup = self.format_scheduled(width, text_markup, task['scheduled'], task) if self.mark_until and task['until']: width, text_markup = self.format_until(width, text_markup, task['until'], task) for uda_name, uda_type in self.udas.items(): if getattr(self, 'mark_%s' % uda_name): width, text_markup = self.format_uda(width, text_markup, uda_name, uda_type, task[uda_name]) if task['uuid'] in self.blocking_task_uuids: width, text_markup = self.format_blocking(width, text_markup) return (width, '' if width == 0 else text_markup) def color_required(self, color): return self.require_color and not color def add_label(self, color, label, width, text_markup): if self.color_required(color) or not label: return width, text_markup width += unicode_len(label) text_markup += [(color, label)] return width, text_markup def format_tags(self, width, text_markup, tags): if not tags: color = self.colorizer.tag_none() label = self.labels['tag.none.label'] return self.add_label(color, label, width, text_markup) elif len(tags) == 1: tag = list(tags)[0] color = self.colorizer.tag(tag) custom_label = 'tag.%s.label' % tag label = self.labels['tag.label'] if not custom_label in self.labels else self.labels[custom_label] return self.add_label(color, label, width, text_markup) else: color = self.colorizer.tag('') label = self.labels['tag.label'] return self.add_label(color, label, width, text_markup) def format_project(self, width, text_markup, project): if not project: color = self.colorizer.project_none() label = self.labels['project.none.label'] return self.add_label(color, label, width, text_markup) else: color = self.colorizer.project(project) custom_label = 'project.%s.label' % project label = self.labels['project.label'] if not custom_label in self.labels else self.labels[custom_label] return self.add_label(color, label, width, text_markup) def format_due(self, width, text_markup, due, task): due_state = self.formatter.get_due_state(due, task) if due_state: color = self.colorizer.due(due_state) label = self.labels['%s.label' % due_state] return self.add_label(color, label, width, text_markup) return width, text_markup def format_uda(self, width, text_markup, uda_name, uda_type, value): if not value: color = self.colorizer.uda_none(uda_name) label = self.labels['uda.%s.none.label' % uda_name] return self.add_label(color, label, width, text_markup) else: color = getattr(self.colorizer, 'uda_%s' % uda_type)(uda_name, value) custom_label = 'uda.%s.label' % uda_name label = self.labels['uda.label'] if not custom_label in self.labels else self.labels[custom_label] return self.add_label(color, label, width, text_markup) def format_blocking(self, width, text_markup): color = self.colorizer.blocking() label = self.labels['blocking.label'] return self.add_label(color, label, width, text_markup) def format_status(self, width, text_markup, status): if status == 'completed' or status == 'deleted': color = self.colorizer.status(status) label = self.labels['%s.label' % status] return self.add_label(color, label, width, text_markup) return width, text_markup def format_blocked(self, width, text_markup, depends): color = self.colorizer.blocked(depends) label = self.labels['blocked.label'] return self.add_label(color, label, width, text_markup) def format_active(self, width, text_markup, start, task): active = self.formatter.get_active_state(start, task) if active: color = self.colorizer.active(active) label = self.labels['active.label'] return self.add_label(color, label, width, text_markup) return width, text_markup def format_recurring(self, width, text_markup, recur): color = self.colorizer.recurring(recur) label = self.labels['recurring.label'] return self.add_label(color, label, width, text_markup) def format_scheduled(self, width, text_markup, scheduled, task): scheduled = self.formatter.get_scheduled_state(scheduled, task) if scheduled: color = self.colorizer.scheduled(scheduled) label = self.labels['scheduled.label'] return self.add_label(color, label, width, text_markup) return width, text_markup def format_until(self, width, text_markup, until, task): until = self.formatter.get_until_state(until, task) if until: color = self.colorizer.until(until) label = self.labels['until.label'] return self.add_label(color, label, width, text_markup) return width, text_markup vit-2.3.2/vit/formatter/modified.py000066400000000000000000000001071451336657600172750ustar00rootroot00000000000000from vit.formatter import DateTime class Modified(DateTime): pass vit-2.3.2/vit/formatter/modified_age.py000066400000000000000000000002221451336657600201070ustar00rootroot00000000000000from vit.formatter.modified import Modified class ModifiedAge(Modified): def format(self, modified, task): return self.age(modified) vit-2.3.2/vit/formatter/modified_countdown.py000066400000000000000000000002361451336657600214000ustar00rootroot00000000000000from vit.formatter.modified import Modified class ModifiedCountdown(Modified): def format(self, modified, task): return self.countdown(modified) vit-2.3.2/vit/formatter/modified_epoch.py000066400000000000000000000002261451336657600204550ustar00rootroot00000000000000from vit.formatter.modified import Modified class ModifiedEpoch(Modified): def format(self, modified, task): return self.epoch(modified) vit-2.3.2/vit/formatter/modified_iso.py000066400000000000000000000002221451336657600201450ustar00rootroot00000000000000from vit.formatter.modified import Modified class ModifiedIso(Modified): def format(self, modified, task): return self.iso(modified) vit-2.3.2/vit/formatter/modified_julian.py000066400000000000000000000002301451336657600206340ustar00rootroot00000000000000from vit.formatter.modified import Modified class ModifiedJulian(Modified): def format(self, modified, task): return self.julian(modified) vit-2.3.2/vit/formatter/modified_relative.py000066400000000000000000000002341451336657600211710ustar00rootroot00000000000000from vit.formatter.modified import Modified class ModifiedRelative(Modified): def format(self, modified, task): return self.relative(modified) vit-2.3.2/vit/formatter/modified_remaining.py000066400000000000000000000002361451336657600213310ustar00rootroot00000000000000from vit.formatter.modified import Modified class ModifiedRemaining(Modified): def format(self, modified, task): return self.remaining(modified) vit-2.3.2/vit/formatter/parent.py000066400000000000000000000002141451336657600170050ustar00rootroot00000000000000from vit.formatter import String class Parent(String): def format(self, parent, task): return parent['uuid'] if parent else '' vit-2.3.2/vit/formatter/parent_long.py000066400000000000000000000001141451336657600200230ustar00rootroot00000000000000from vit.formatter.parent import Parent class ParentLong(Parent): pass vit-2.3.2/vit/formatter/parent_short.py000066400000000000000000000002761451336657600202340ustar00rootroot00000000000000from vit import util from vit.formatter.parent import Parent class ParentShort(Parent): def format(self, parent, task): return util.uuid_short(parent['uuid']) if parent else '' vit-2.3.2/vit/formatter/project.py000066400000000000000000000021671451336657600171730ustar00rootroot00000000000000from vit.formatter import String from vit.util import unicode_len class Project(String): def __init__(self, column, report, defaults, blocking_task_uuids, **kwargs): super().__init__(column, report, defaults, blocking_task_uuids) self.indent_subprojects = self.is_subproject_indentable() def format(self, project, task): 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 (unicode_len(project), self.markup_element(project)) def format_subproject_indented(self, project, task): parts = project.split('.') (width, spaces, marker, subproject) = self.formatter.format_subproject_indented(parts) return (width, [spaces, marker, (self.colorize(project), subproject)]) def is_subproject_indentable(self): return self.formatter.config.subproject_indentable and self.report['subproject_indentable'] def colorize(self, project): return self.colorizer.project(project) vit-2.3.2/vit/formatter/project_full.py000066400000000000000000000001201451336657600202000ustar00rootroot00000000000000from vit.formatter.project import Project class ProjectFull(Project): pass vit-2.3.2/vit/formatter/project_indented.py000066400000000000000000000002421451336657600210350ustar00rootroot00000000000000from vit.formatter.project import Project class ProjectIndented(Project): def format(self, obj, task): return 'N/A, use indent_subprojects setting' vit-2.3.2/vit/formatter/project_parent.py000066400000000000000000000005261451336657600205410ustar00rootroot00000000000000from 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 (unicode_len(parent), self.markup_element(parent)) if parent else self.markup_none(self.colorizer.project_none()) vit-2.3.2/vit/formatter/recur.py000066400000000000000000000002111451336657600166310ustar00rootroot00000000000000from vit.formatter import Duration class Recur(Duration): def colorize(self, recur): return self.colorizer.recurring(recur) vit-2.3.2/vit/formatter/recur_duration.py000066400000000000000000000001141451336657600205400ustar00rootroot00000000000000from vit.formatter.recur import Recur class RecurDuration(Recur): pass vit-2.3.2/vit/formatter/recur_indicator.py000066400000000000000000000002621451336657600206730ustar00rootroot00000000000000from vit.formatter.recur import Recur class RecurIndicator(Recur): def format_duration(self, recur): return '' if not recur else self.formatter.indicator_recurrence vit-2.3.2/vit/formatter/scheduled.py000066400000000000000000000004631451336657600174620ustar00rootroot00000000000000from vit.formatter import DateTime class Scheduled(DateTime): def get_scheduled_state(self, scheduled, task): return self.formatter.get_scheduled_state(scheduled, task) def colorize(self, scheduled, task): return self.colorizer.scheduled(self.get_scheduled_state(scheduled, task)) vit-2.3.2/vit/formatter/scheduled_age.py000066400000000000000000000002411451336657600202700ustar00rootroot00000000000000from vit.formatter.scheduled import Scheduled class ScheduledAge(Scheduled): def format_datetime(self, scheduled, task): return self.age(scheduled) vit-2.3.2/vit/formatter/scheduled_countdown.py000066400000000000000000000002551451336657600215610ustar00rootroot00000000000000from vit.formatter.scheduled import Scheduled class ScheduledCountdown(Scheduled): def format_datetime(self, scheduled, task): return self.countdown(scheduled) vit-2.3.2/vit/formatter/scheduled_epoch.py000066400000000000000000000002451451336657600206360ustar00rootroot00000000000000from vit.formatter.scheduled import Scheduled class ScheduledEpoch(Scheduled): def format_datetime(self, scheduled, task): return self.epoch(scheduled) vit-2.3.2/vit/formatter/scheduled_formatted.py000066400000000000000000000001351451336657600215230ustar00rootroot00000000000000from vit.formatter.scheduled import Scheduled class ScheduledFormatted(Scheduled): pass vit-2.3.2/vit/formatter/scheduled_iso.py000066400000000000000000000002411451336657600203260ustar00rootroot00000000000000from vit.formatter.scheduled import Scheduled class ScheduledIso(Scheduled): def format_datetime(self, scheduled, task): return self.iso(scheduled) vit-2.3.2/vit/formatter/scheduled_julian.py000066400000000000000000000002471451336657600210240ustar00rootroot00000000000000from vit.formatter.scheduled import Scheduled class ScheduledJulian(Scheduled): def format_datetime(self, scheduled, task): return self.julian(scheduled) vit-2.3.2/vit/formatter/scheduled_relative.py000066400000000000000000000002531451336657600213520ustar00rootroot00000000000000from vit.formatter.scheduled import Scheduled class ScheduledRelative(Scheduled): def format_datetime(self, scheduled, task): return self.relative(scheduled) vit-2.3.2/vit/formatter/scheduled_remaining.py000066400000000000000000000002551451336657600215120ustar00rootroot00000000000000from vit.formatter.scheduled import Scheduled class ScheduledRemaining(Scheduled): def format_datetime(self, scheduled, task): return self.remaining(scheduled) vit-2.3.2/vit/formatter/start.py000066400000000000000000000004231451336657600166530ustar00rootroot00000000000000from vit.formatter import DateTime class Start(DateTime): def get_active_state(self, start, task): return self.formatter.get_active_state(start, task) def colorize(self, start, task): return self.colorizer.active(self.get_active_state(start, task)) vit-2.3.2/vit/formatter/start_active.py000066400000000000000000000003121451336657600202030ustar00rootroot00000000000000from vit.formatter.start import Start class StartActive(Start): def format_datetime(self, start, task): return self.formatter.indicator_active if self.get_active_state(start, task) else '' vit-2.3.2/vit/formatter/start_age.py000066400000000000000000000002111451336657600174620ustar00rootroot00000000000000from vit.formatter.start import Start class StartAge(Start): def format_datetime(self, start, task): return self.age(start) vit-2.3.2/vit/formatter/start_countdown.py000066400000000000000000000002251451336657600207530ustar00rootroot00000000000000from vit.formatter.start import Start class StartCountdown(Start): def format_datetime(self, start, task): return self.countdown(start) vit-2.3.2/vit/formatter/start_epoch.py000066400000000000000000000002151451336657600200300ustar00rootroot00000000000000from vit.formatter.start import Start class StartEpoch(Start): def format_datetime(self, start, task): return self.epoch(start) vit-2.3.2/vit/formatter/start_formatted.py000066400000000000000000000001151451336657600207160ustar00rootroot00000000000000from vit.formatter.start import Start class StartFormatted(Start): pass vit-2.3.2/vit/formatter/start_iso.py000066400000000000000000000002111451336657600175200ustar00rootroot00000000000000from vit.formatter.start import Start class StartIso(Start): def format_datetime(self, start, task): return self.iso(start) vit-2.3.2/vit/formatter/start_julian.py000066400000000000000000000002171451336657600202160ustar00rootroot00000000000000from vit.formatter.start import Start class StartJulian(Start): def format_datetime(self, start, task): return self.julian(start) vit-2.3.2/vit/formatter/start_relative.py000066400000000000000000000002231451336657600205440ustar00rootroot00000000000000from vit.formatter.start import Start class StartRelative(Start): def format_datetime(self, start, task): return self.relative(start) vit-2.3.2/vit/formatter/start_remaining.py000066400000000000000000000002251451336657600207040ustar00rootroot00000000000000from vit.formatter.start import Start class StartRemaining(Start): def format_datetime(self, start, task): return self.remaining(start) vit-2.3.2/vit/formatter/status.py000066400000000000000000000004701451336657600170430ustar00rootroot00000000000000from vit.formatter import String class Status(String): def markup_element(self, status): return (self.colorize(status), self.status_format(status)) def colorize(self, status): return self.colorizer.status(status) def status_format(self, status): return status.capitalize() vit-2.3.2/vit/formatter/status_long.py000066400000000000000000000001141451336657600200550ustar00rootroot00000000000000from vit.formatter.status import Status class StatusLong(Status): pass vit-2.3.2/vit/formatter/status_short.py000066400000000000000000000002131451336657600202550ustar00rootroot00000000000000from vit.formatter.status import Status class StatusShort(Status): def status_format(self, status): return status[0].upper() vit-2.3.2/vit/formatter/tags.py000066400000000000000000000014211451336657600164530ustar00rootroot00000000000000from vit.formatter import Formatter from vit.util import unicode_len class Tags(Formatter): def format(self, tags, task): if not tags: return self.markup_none(self.colorizer.tag_none()) elif len(tags) == 1: tag = list(tags)[0] return (unicode_len(tag), self.markup_element(tag)) else: last_tag = list(tags)[-1] width = 0 text_markup = [] for tag in tags: width += unicode_len(tag) text_markup += [self.markup_element(tag)] if tag != last_tag: width += 1 text_markup += [','] return (width, text_markup) def colorize(self, tag): return self.colorizer.tag(tag) vit-2.3.2/vit/formatter/tags_count.py000066400000000000000000000006701451336657600176700ustar00rootroot00000000000000from vit.formatter.tags import Tags class TagsCount(Tags): def format(self, tags, task): if not tags: return self.markup_none(self.colorizer.tag_none()) elif len(tags) == 1: return (3, (self.colorizer.tag(list(tags)[0]), '[1]')) else: tag_length = len(tags) indicator = '[%d]' % tag_length return (tag_length + 2, (self.colorizer.tag(''), indicator)) vit-2.3.2/vit/formatter/tags_indicator.py000066400000000000000000000004231451336657600205100ustar00rootroot00000000000000from vit.formatter.tags import Tags class TagsIndicator(Tags): def format(self, tags, task): if not tags: return self.markup_none(self.colorizer.tag_none()) else: return (1, (self.colorizer.tag(''), self.formatter.indicator_tag)) vit-2.3.2/vit/formatter/tags_list.py000066400000000000000000000001041451336657600175030ustar00rootroot00000000000000from vit.formatter.tags import Tags class TagsList(Tags): pass vit-2.3.2/vit/formatter/uda_date.py000066400000000000000000000014541451336657600172710ustar00rootroot00000000000000import 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 serializer = SerializingObject({}) class UdaDate(DateTime): def format(self, dt, task): if not dt: return self.markup_none(self.colorize()) # TODO: Remove this once tasklib bug is fixed. # 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 (unicode_len(formatted_date), (self.colorize(dt), formatted_date)) def colorize(self, dt=None): return self.colorizer.uda_date(self.column, dt) vit-2.3.2/vit/formatter/uda_duration.py000066400000000000000000000006011451336657600201720ustar00rootroot00000000000000from 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 (unicode_len(duration), self.markup_element(duration)) def colorize(self, duration=None): return self.colorizer.uda_duration(self.column, duration) vit-2.3.2/vit/formatter/uda_indicator.py000066400000000000000000000007271451336657600203320ustar00rootroot00000000000000from vit.formatter import Formatter from vit.util import unicode_len class UdaIndicator(Formatter): def format(self, value, task): if not value: return self.markup_none(self.colorize()) else: indicator = self.formatter.indicator_uda[self.column] return (unicode_len(indicator), (self.colorize(value), indicator)) def colorize(self, value=None): return self.colorizer.uda_indicator(self.column, value) vit-2.3.2/vit/formatter/uda_numeric.py000066400000000000000000000006241451336657600200140ustar00rootroot00000000000000from 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 (unicode_len(number), self.markup_element(number)) def colorize(self, number=None): return self.colorizer.uda_numeric(self.column, number) vit-2.3.2/vit/formatter/uda_string.py000066400000000000000000000005611451336657600176600ustar00rootroot00000000000000from 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 (unicode_len(string), self.markup_element(string)) def colorize(self, string=None): return self.colorizer.uda_string(self.column, string) vit-2.3.2/vit/formatter/until.py000066400000000000000000000004171451336657600166540ustar00rootroot00000000000000from vit.formatter import DateTime class Until(DateTime): def get_until_state(self, until, task): return self.formatter.get_until_state(until, task) def colorize(self, until, task): return self.colorizer.until(self.get_until_state(until, task)) vit-2.3.2/vit/formatter/until_age.py000066400000000000000000000002111451336657600174600ustar00rootroot00000000000000from vit.formatter.until import Until class UntilAge(Until): def format_datetime(self, until, task): return self.age(until) vit-2.3.2/vit/formatter/until_countdown.py000066400000000000000000000002251451336657600207510ustar00rootroot00000000000000from vit.formatter.until import Until class UntilCountdown(Until): def format_datetime(self, until, task): return self.countdown(until) vit-2.3.2/vit/formatter/until_epoch.py000066400000000000000000000002151451336657600200260ustar00rootroot00000000000000from vit.formatter.until import Until class UntilEpoch(Until): def format_datetime(self, until, task): return self.epoch(until) vit-2.3.2/vit/formatter/until_formatted.py000066400000000000000000000001151451336657600207140ustar00rootroot00000000000000from vit.formatter.until import Until class UntilFormatted(Until): pass vit-2.3.2/vit/formatter/until_iso.py000066400000000000000000000002111451336657600175160ustar00rootroot00000000000000from vit.formatter.until import Until class UntilIso(Until): def format_datetime(self, until, task): return self.iso(until) vit-2.3.2/vit/formatter/until_julian.py000066400000000000000000000002171451336657600202140ustar00rootroot00000000000000from vit.formatter.until import Until class UntilJulian(Until): def format_datetime(self, until, task): return self.julian(until) vit-2.3.2/vit/formatter/until_relative.py000066400000000000000000000002231451336657600205420ustar00rootroot00000000000000from vit.formatter.until import Until class UntilRelative(Until): def format_datetime(self, until, task): return self.relative(until) vit-2.3.2/vit/formatter/until_remaining.py000066400000000000000000000002251451336657600207020ustar00rootroot00000000000000from vit.formatter.until import Until class UntilRemaining(Until): def format_datetime(self, until, task): return self.remaining(until) vit-2.3.2/vit/formatter/urgency.py000066400000000000000000000002071451336657600171720ustar00rootroot00000000000000from vit.formatter import Number class Urgency(Number): def format(self, urgency, task): return "{0:.2f}".format(urgency) vit-2.3.2/vit/formatter/urgency_integer.py000066400000000000000000000002301451336657600207030ustar00rootroot00000000000000from vit.formatter.urgency import Urgency class UrgencyInteger(Urgency): def format(self, urgency, task): return "{0:.0f}".format(urgency) vit-2.3.2/vit/formatter/urgency_real.py000066400000000000000000000001201451336657600201670ustar00rootroot00000000000000from vit.formatter.urgency import Urgency class UrgencyReal(Urgency): pass vit-2.3.2/vit/formatter/uuid.py000066400000000000000000000000771451336657600164710ustar00rootroot00000000000000from vit.formatter import String class Uuid(String): pass vit-2.3.2/vit/formatter/uuid_long.py000066400000000000000000000001041451336657600174770ustar00rootroot00000000000000from vit.formatter.uuid import Uuid class UuidLong(Uuid): pass vit-2.3.2/vit/formatter/uuid_short.py000066400000000000000000000002301451336657600176770ustar00rootroot00000000000000from vit import util from vit.formatter.uuid import Uuid class UuidShort(Uuid): def format(self, uuid, task): return util.uuid_short(uuid) vit-2.3.2/vit/formatter/wait.py000066400000000000000000000001031451336657600164550ustar00rootroot00000000000000from vit.formatter import DateTime class Wait(DateTime): pass vit-2.3.2/vit/formatter/wait_age.py000066400000000000000000000001721451336657600172770ustar00rootroot00000000000000from vit.formatter.wait import Wait class WaitAge(Wait): def format(self, wait, task): return self.age(wait) vit-2.3.2/vit/formatter/wait_countdown.py000066400000000000000000000002061451336657600205610ustar00rootroot00000000000000from vit.formatter.wait import Wait class WaitCountdown(Wait): def format(self, wait, task): return self.countdown(wait) vit-2.3.2/vit/formatter/wait_epoch.py000066400000000000000000000001761451336657600176450ustar00rootroot00000000000000from vit.formatter.wait import Wait class WaitEpoch(Wait): def format(self, wait, task): return self.epoch(wait) vit-2.3.2/vit/formatter/wait_iso.py000066400000000000000000000001721451336657600173350ustar00rootroot00000000000000from vit.formatter.wait import Wait class WaitIso(Wait): def format(self, wait, task): return self.iso(wait) vit-2.3.2/vit/formatter/wait_julian.py000066400000000000000000000002001451336657600200150ustar00rootroot00000000000000from vit.formatter.wait import Wait class WaitJulian(Wait): def format(self, wait, task): return self.julian(wait) vit-2.3.2/vit/formatter/wait_relative.py000066400000000000000000000002041451336657600203520ustar00rootroot00000000000000from vit.formatter.wait import Wait class WaitRelative(Wait): def format(self, wait, task): return self.relative(wait) vit-2.3.2/vit/formatter/wait_remaining.py000066400000000000000000000002061451336657600205120ustar00rootroot00000000000000from vit.formatter.wait import Wait class WaitRemaining(Wait): def format(self, wait, task): return self.remaining(wait) vit-2.3.2/vit/formatter_base.py000066400000000000000000000124321451336657600165130ustar00rootroot00000000000000from importlib import import_module from datetime import datetime, timedelta try: from zoneinfo import ZoneInfo except ImportError: from backports.zoneinfo import ZoneInfo from vit import util from vit import uda from vit.util import unicode_len INDICATORS = [ 'active', 'dependency', 'recurrence', 'tag', ] UDA_DEFAULT_INDICATOR = 'U' DEFAULT_DESCRIPTION_TRUNCATE_LEN=20 class FormatterBase: def __init__(self, loader, config, task_config, markers, task_colorizer): self.loader = loader self.config = config self.task_config = task_config self.markers = markers self.task_colorizer = task_colorizer self.date_default = self.task_config.translate_date_markers(self.task_config.subtree('dateformat')["default"]) self.report = self.task_config.translate_date_markers(self.task_config.subtree('dateformat.report')) or self.date_default self.annotation = self.task_config.translate_date_markers(self.task_config.subtree('dateformat.annotation')) or self.date_default self.description_truncate_len = DEFAULT_DESCRIPTION_TRUNCATE_LEN self.epoch_datetime = datetime(1970, 1, 1, tzinfo=ZoneInfo('UTC')) self.due_days = int(self.task_config.subtree('due')) self.none_label = config.get('color', 'none_label') self.build_indicators() def build_indicators(self): for indicator in INDICATORS: label = self.task_config.subtree('%s.indicator' % indicator) setattr(self, 'indicator_%s' % indicator, label) self.indicator_uda = {} for uda_name in uda.get_configured(self.task_config).keys(): label = self.task_config.subtree('uda.%s.indicator' % uda_name) or UDA_DEFAULT_INDICATOR self.indicator_uda[uda_name] = label def get_module_class_from_parts(self, parts): formatter_module_name = '_'.join(parts) formatter_class_name = util.file_to_class_name(formatter_module_name) return formatter_module_name, formatter_class_name def get_user_formatter_class(self, parts): formatter_module_name, formatter_class_name = self.get_module_class_from_parts(parts) return self.loader.load_user_class('formatter', formatter_module_name, formatter_class_name) def get_formatter_class(self, parts): formatter_module_name, formatter_class_name = self.get_module_class_from_parts(parts) try: formatter_module = import_module('vit.formatter.%s' % formatter_module_name) formatter_class = getattr(formatter_module, formatter_class_name) return formatter_class except ImportError: return None def get(self, column_formatter): parts = column_formatter.split('.') name = parts[0] formatter_class = self.get_user_formatter_class(parts) if formatter_class: return name, formatter_class formatter_class = self.get_formatter_class(parts) if formatter_class: return name, formatter_class uda_metadata = uda.get(name, self.task_config) if uda_metadata: is_indicator = parts[-1] == 'indicator' if is_indicator: formatter_class = self.get_formatter_class(['uda', 'indicator']) return name, formatter_class else: uda_type = uda_metadata['type'] if 'type' in uda_metadata else 'string' formatter_class = self.get_formatter_class(['uda', uda_type]) if formatter_class: return name, formatter_class return name, Formatter def format_subproject_indented(self, project_parts): if len(project_parts) == 1: subproject = project_parts[0] return (unicode_len(subproject), '', '', subproject) else: subproject = project_parts.pop() space_padding = (len(project_parts) * 2) - 1 indicator = u'\u21aa ' width = space_padding + unicode_len(indicator) + unicode_len(subproject) return (width, ' ' * space_padding , indicator, subproject) def recalculate_due_datetimes(self): self.now = datetime.now().astimezone() # NOTE: For some reason using self.zone for the tzinfo below results # in the tzinfo object having a zone of 'LMT', which is wrong. Using # the tzinfo associated with self.now returns the correct value, no # idea why this glitch happens. self.end_of_day = datetime(self.now.year, self.now.month, self.now.day, 23, 59, 59, tzinfo=self.now.tzinfo) self.due_soon = self.end_of_day + timedelta(days=self.due_days) def get_due_state(self, due, task): if util.task_pending(task): if due < self.now: return 'overdue' elif due <= self.end_of_day: return 'due.today' elif due < self.due_soon: return 'due' return None def get_active_state(self, start, task): end = task['end'] return util.task_pending(task) and start and start <= self.now and (not end or end > self.now) def get_scheduled_state(self, scheduled, task): return scheduled and not util.task_completed(task) def get_until_state(self, until, task): return until and not util.task_completed(task) vit-2.3.2/vit/help.py000066400000000000000000000162611451336657600144520ustar00rootroot00000000000000import re import urwid from vit.base_list_box import BaseListBox from vit.util import unicode_len CURLY_BRACES_REGEX = re.compile("[{}]") SPECIAL_KEY_SUBSTITUTIONS = { '': ':', '': '=', } class SelectableHelpRow(urwid.WidgetWrap): """Wraps 'urwid.Columns' to make it selectable. This class has been slightly modified, but essentially corresponds to this class posted on stackoverflow.com: https://stackoverflow.com/questions/52106244/how-do-you-combine-multiple-tui-forms-to-write-more-complex-applications#answer-52174629""" def __init__(self, column_widths, data, position, *, space_between=2): self.data = data self.position = position self._columns = urwid.Columns([ (column_widths['type'], urwid.Text(self.data[0], align='left')), (column_widths['keys'], urwid.Text(self.data[1], align='left')), urwid.Text(self.data[2], align='left'), ], dividechars=space_between) self.row = urwid.AttrMap(self._columns, '', 'reveal focus') # Wrap 'urwid.Columns'. super().__init__(self.row) def __repr__(self): return "{}(type={}, keys={}, desc={})".format(self.__class__.__name__, self.data[0], self.data[1], self.data[2]) def selectable(self): return True def keypress(self, size, key): return key class HelpListBox(BaseListBox): """Custom ListBox class for help. """ def __init__(self, body, keybindings, event=None, request_reply=None, action_manager=None): self.keybindings = keybindings return super().__init__(body, event=event, request_reply=request_reply, action_manager=action_manager) def register_managed_actions(self): super().register_managed_actions() self.action_manager_registrar.register('GLOBAL_ESCAPE', self.exit_help) self.action_manager_registrar.register('QUIT_WITH_CONFIRM', self.exit_help) self.action_manager_registrar.register('QUIT', self.exit_help) def calculate_column_widths(self, entries): column_widths = { 'type': 0, 'keys': 0, } for entry in entries: 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']: column_widths['keys'] = keys_len return column_widths def reload_entries(self, entries): column_widths = self.calculate_column_widths(entries) rows = [SelectableHelpRow(column_widths, row, idx) for idx, row in enumerate(entries)] self.list_walker[:] = rows if len(self.list_walker) > 0: self.set_focus(0) def exit_help(self, data): self.event.emit("help:exit") def eat_other_keybindings(self): return True class Help: """Generates help list/display. """ def __init__(self, keybinding_parser, actions, event=None, request_reply=None, action_manager=None): self.keybinding_parser = keybinding_parser self.actions = actions self.event = event self.request_reply = request_reply self.action_manager = action_manager self.keybindings = self.keybinding_parser.get_keybindings() sections = self.build_default_keybinding_data() sections = self.add_custom_help(sections) self.compose_entries(sections) self.build_help_widget() def autocomplete_entries(self): return [ 'help', 'help command', 'help global', 'help help', 'help navigation', 'help report', ] def update(self, filter_args): entries = self.filter_entries(filter_args) self.listbox.reload_entries(entries) return self.widget def filter_entries(self, filter_args): if len(filter_args) > 0: args_regex = re.compile('.*(%s).*' % '|'.join(filter_args)) return [(section, keys, description) for section, keys, description, search_phrase in self.entries if args_regex.match(search_phrase)] else: return [(section, keys, description) for section, keys, description, _ in self.entries] def compose_entries(self, sections): self.entries = [] for section in self.keybinding_parser.sections: for keys, description in sections[section]: self.add_entry(section, keys, description) for keys, description in sections['help']: self.add_entry('help', keys, description) def add_entry(self, section, keys, description): self.entries.append((section, keys, description, ' '.join([section, keys, description]))) # TODO: This does not pull data from keybinding overrides in config.ini # yet. def build_default_keybinding_data(self): sections = {} for section in self.keybinding_parser.sections: sections[section] = [] for keys, action in self.keybinding_parser.items(section): action = re.sub(CURLY_BRACES_REGEX, '', action) keys = self.special_key_substitutions(keys) if action in self.actions: sections[section].append((keys, self.actions[action]['description'])) return sections def add_custom_help(self, sections): sections['command'] += [ (':q', 'Quit the application'), (':![rw] STRING', "Execute STRING in shell. 'r' re-reads, 'w' waits"), (':s/OLD/NEW/', "Change OLD to NEW in the current task's description"), (':%s/OLD/NEW/', "Change OLD to NEW in the current task's description"), ] sections['navigation'] += [ (':N', 'Move to task number N'), ] sections['report'] += [ (':REPORT', 'Display REPORT (supports tab completion)'), (':REPORT FILTER', 'Display REPORT with FILTER (supports tab completion)'), ] sections['help'] = [ (':help', 'View the whole help file'), (':help command', 'View help about commands'), (':help global', 'View help global actions'), (':help help', 'View help about help'), (':help navigation', 'View help about navigation'), (':help report', 'View help about reports'), (':help PATTERN', 'View help file lines matching PATTERN'), ] return sections def special_key_substitutions(self, keys): for key, sub in SPECIAL_KEY_SUBSTITUTIONS.items(): keys = keys.replace(key, sub) return keys def build_help_widget(self): self.listbox = HelpListBox(urwid.SimpleFocusListWalker([]), self.keybinding_parser.keybindings, event=self.event, request_reply=self.request_reply, action_manager=self.action_manager) self.widget = urwid.Frame( self.listbox, header=urwid.Pile([ urwid.Text("Press '' or 'q' to exit", align='center'), # TODO: Remove this when keybinding overrides are shown. urwid.Text("NOTE: Keybinding overrides in config.ini not shown", align='center'), urwid.Text(''), ]), ) vit-2.3.2/vit/key_cache.py000066400000000000000000000043741451336657600154370ustar00rootroot00000000000000from functools import reduce class KeyCacheError(Exception): pass class KeyCache: def __init__(self, keybindings): self.keybindings = keybindings self.cached_keys = '' self.build_multi_key_cache() def get(self, key=None): return '%s%s' % (self.cached_keys, key) if key else self.cached_keys def set(self, keys=''): self.cached_keys = keys def is_keybinding(self, keys): return keys in self.keybindings def sort_keybindings_by_len(self, keybindings, min_len=1): max_key_length = len(max(keybindings, key=len)) def reducer(accum, key_length): accum.append([v for k, v in enumerate(keybindings) if len(keybindings[k]) == key_length]) return accum sorted_keybindings = reduce(reducer, range(max_key_length, min_len, -1), []) return list(filter(None, sorted_keybindings)) def get_non_modified_keybindings(self): return [k for k in self.keybindings if self.keybindings[k]['has_special_keys'] or not self.keybindings[k]['has_modifier']] def add_keybinding_to_key_cache(self, to_cache, keybinding, existing_keybindings, key_cache): if to_cache in existing_keybindings: raise KeyCacheError("Invalid key binding '%s', '%s' already used in another key binding" % (keybinding, to_cache)) else: key_cache[to_cache] = True def build_multi_key_cache(self): keybindings = self.get_non_modified_keybindings() sorted_keybindings = self.sort_keybindings_by_len(keybindings) def sorted_keybindings_reducer(key_cache, keybinding_list): def keybinding_list_reducer(_, keybinding): keys = list(keybinding) keys.pop() def keybinding_reducer(processed_keys, key): processed_keys.append(key) to_cache = ''.join(processed_keys) self.add_keybinding_to_key_cache(to_cache, keybinding, keybindings, key_cache) return processed_keys reduce(keybinding_reducer, keys, []) reduce(keybinding_list_reducer, keybinding_list, []) return key_cache self.multi_key_cache = reduce(sorted_keybindings_reducer, sorted_keybindings, {}) vit-2.3.2/vit/keybinding/000077500000000000000000000000001451336657600152655ustar00rootroot00000000000000vit-2.3.2/vit/keybinding/vi.ini000066400000000000000000000030451451336657600164060ustar00rootroot00000000000000[global] = {ACTION_GLOBAL_ESCAPE} Q,ZZ = {ACTION_QUIT} q = {ACTION_QUIT_WITH_CONFIRM} S = {ACTION_TASK_SYNC} [command] # Command bar actions. # The parser will not allow colons on the left side of an assignment, the # special marker is used instead. = {ACTION_COMMAND_BAR_EX} t = {ACTION_COMMAND_BAR_EX_TASK_READ_WAIT} / = {ACTION_COMMAND_BAR_SEARCH_FORWARD} ? = {ACTION_COMMAND_BAR_SEARCH_REVERSE} n = {ACTION_COMMAND_BAR_SEARCH_NEXT} N = {ACTION_COMMAND_BAR_SEARCH_PREVIOUS} c = {ACTION_COMMAND_BAR_TASK_CONTEXT} # Task actions. a = {ACTION_TASK_ADD} u = {ACTION_TASK_UNDO} A = {ACTION_TASK_ANNOTATE} D = {ACTION_TASK_DELETE} E = {ACTION_TASK_DENOTATE} m = {ACTION_TASK_MODIFY} b = {ACTION_TASK_START_STOP} d = {ACTION_TASK_DONE} P = {ACTION_TASK_PRIORITY} p = {ACTION_TASK_PROJECT} T = {ACTION_TASK_TAGS} w = {ACTION_TASK_WAIT} e = {ACTION_TASK_EDIT} # The parser will not allow an equals sign on the left side of an assignment, # the special marker is used instead. , = {ACTION_TASK_SHOW} [navigation] ,k = {ACTION_LIST_UP} # The parser doesn't recognize a space on the left side of an assignment, # the special marker is used instead. ,j, = {ACTION_LIST_DOWN} , b = {ACTION_LIST_PAGE_UP} , f = {ACTION_LIST_PAGE_DOWN} gg,0 = {ACTION_LIST_HOME} G = {ACTION_LIST_END} H = {ACTION_LIST_SCREEN_TOP} M = {ACTION_LIST_SCREEN_MIDDLE} L = {ACTION_LIST_SCREEN_BOTTOM} C = {ACTION_LIST_FOCUS_VALIGN_CENTER} [report] f = {ACTION_REPORT_FILTER} l = {ACTION_REFRESH} vit-2.3.2/vit/keybinding_parser.py000066400000000000000000000152151451336657600172170ustar00rootroot00000000000000from functools import reduce try: import configparser except ImportError: import ConfigParser as configparser import os import re from vit import util BASE_DIR = os.path.dirname(os.path.realpath(__file__)) BRACKETS_REGEX = re.compile("[<>]") DEFAULT_KEYBINDINGS_SECTIONS = ('global', 'navigation', 'command', 'report') CONFIG_SPECIAL_KEY_SUBSTITUTIONS = { 'colon': ':', 'equals': '=', 'space': ' ', 'semicolon': ';', } class KeybindingError(Exception): pass class KeybindingParser: def __init__(self, loader, config, action_registry): self.loader = loader self.config = config self.action_registry = action_registry self.actions = self.action_registry.get_actions() self.noop_action_name = self.action_registry.make_action_name(self.action_registry.noop_action_name) self.default_keybinding_name = self.config.get('vit', 'default_keybindings') self.default_keybindings = configparser.ConfigParser() self.default_keybindings.optionxform=str self.sections = DEFAULT_KEYBINDINGS_SECTIONS self.keybindings = {} self.multi_key_cache = {} def is_keybinding(self, keys): return keys in self.keybindings def items(self, section): try: return self.default_keybindings.items(section) except configparser.NoSectionError: return [] def load_default_keybindings(self): name = self.default_keybinding_name template = '%s/keybinding/%s.ini' user_keybinding_file = template % (self.loader.user_config_dir, name) keybinding_file = template % (BASE_DIR, name) if util.file_readable(user_keybinding_file): self.default_keybindings.read(user_keybinding_file) elif util.file_readable(keybinding_file): self.default_keybindings.read(keybinding_file) else: raise KeybindingError("default_keybindings setting '%s' invalid, file not found" % name) for section in self.sections: bindings = self.items(section) self.add_keybindings(bindings) def keybinding_special_keys_substitutions(self, value): is_special_key = value in CONFIG_SPECIAL_KEY_SUBSTITUTIONS if is_special_key: value = CONFIG_SPECIAL_KEY_SUBSTITUTIONS[value] return value, is_special_key def parse_keybinding_keys(self, keys): has_modifier = bool(re.match(BRACKETS_REGEX, keys)) def reducer(accum, char): if char == '<': accum['in_brackets'] = True accum['bracket_string'] = '' elif char == '>': accum['in_brackets'] = False value, is_special_key = self.keybinding_special_keys_substitutions(accum['bracket_string'].lower()) accum['keys'] += value accum['has_special_keys'] = is_special_key else: if accum['in_brackets']: accum['bracket_string'] += char else: accum['keys'] += char return accum accum = reduce(reducer, keys, { 'keys': '', 'in_brackets': False, 'bracket_string': '', 'has_special_keys': False, }) return accum['keys'], has_modifier, accum['has_special_keys'] def parse_keybinding_value(self, value, replacements={}): def reducer(accum, char): if char == '<': accum['in_brackets'] = True accum['bracket_string'] = '' elif char == '>': accum['in_brackets'] = False value, is_special_key = self.keybinding_special_keys_substitutions(accum['bracket_string'].lower()) accum['keybinding'].append(value) elif char == '{': accum['in_variable'] = True accum['variable_string'] = '' elif char == '}': accum['in_variable'] = False if accum['variable_string'] in self.actions: accum['action_name'] = accum['variable_string'] else: for replacement in replacements: args = replacement['match_callback'](accum['variable_string']) if isinstance(args, list): accum['keybinding'].append((replacement['replacement_callback'], args)) return accum raise ValueError("unknown config variable '%s'" % accum['variable_string']) else: if accum['in_brackets']: accum['bracket_string'] += char elif accum['in_variable']: accum['variable_string'] += char else: accum['keybinding'].append(char) return accum accum = reduce(reducer, value, { 'keybinding': [], 'action_name': None, 'in_brackets': False, 'bracket_string': '', 'in_variable': False, 'variable_string': '', }) return accum['keybinding'], accum['action_name'] def validate_parsed_value(self, key_groups, bound_keys, action_name): if bound_keys and action_name: raise KeybindingError("keybindings '%s' unsupported configuration: ACTION_ variables must be used alone." % key_groups) def is_noop_action(self, keybinding): return True if 'action_name' in keybinding and keybinding['action_name'] == self.noop_action_name else False def filter_noop_actions(self, keybindings): return {keys:value for (keys, value) in keybindings.items() if not self.is_noop_action(keybindings[keys])} def get_keybindings(self): return self.keybindings def add_keybindings(self, bindings=[], replacements={}): for key_groups, value in bindings: bound_keys, action_name = self.parse_keybinding_value(value, replacements) self.validate_parsed_value(key_groups, bound_keys, action_name) for keys in key_groups.strip().split(','): parsed_keys, has_modifier, has_special_keys = self.parse_keybinding_keys(keys) self.keybindings[parsed_keys] = { 'label': keys, 'has_modifier': has_modifier, 'has_special_keys': has_special_keys, } if action_name: self.keybindings[parsed_keys]['action_name'] = action_name else: self.keybindings[parsed_keys]['keys'] = bound_keys self.keybindings = self.filter_noop_actions(self.keybindings) return self.get_keybindings() vit-2.3.2/vit/list_batcher.py000066400000000000000000000031761451336657600161660ustar00rootroot00000000000000DEFAULT_BATCH_SIZE=100 class ListBatchError(Exception): pass 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 self.batch_to_formatter = batch_to_formatter self.default_batch_size = default_batch_size self.last_position = 0 self.batching_complete = False def add(self, batch_size=None): if self.batching_complete: return True batch_size = self.get_batch_size(batch_size) self.load_batch(batch_size) if self.is_batching_complete(): self.batching_complete = True return self.batching_complete def get_last_position(self): return self.last_position def load_batch(self, batch_size): new_position = self.last_position + batch_size partial = self.batch_from[self.last_position:new_position] if self.batch_to_formatter: partial = self.batch_to_formatter(partial, self.last_position) self.batch_to += partial self.last_position = new_position def is_batching_complete(self): return self.last_position >= len(self.batch_from) def batch_remainder(self): return len(self.batch_from) - self.last_position def get_batch_size(self, batch_size): remainder = self.batch_remainder() if batch_size == 0: return remainder else: if batch_size is None: batch_size = self.default_batch_size return remainder if batch_size > remainder else batch_size vit-2.3.2/vit/loader.py000066400000000000000000000022361451336657600147650ustar00rootroot00000000000000import os try: import importlib.util except: import imp from vit import env, xdg DEFAULT_VIT_DIR = '~/.vit' 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) if not os.path.exists(self.user_config_dir): xdg_dir = xdg.get_xdg_config_dir(self.user_config_dir, "vit") if xdg_dir: self.user_config_dir = xdg_dir def load_user_class(self, module_type, module_name, class_name): module = '%s.%s' % (module_type, module_name) filepath = '%s/%s/%s.py' % (self.user_config_dir, module_type, module_name) try: mod = self.import_from_path(module, filepath) except SyntaxError as e: raise SyntaxError("User class: %s (%s) -- %s" % (class_name, filepath, e)) except: return None return getattr(mod, class_name) def import_from_path(self, module, filepath): spec = importlib.util.spec_from_file_location(module, filepath) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) return mod vit-2.3.2/vit/markers.py000066400000000000000000000061631451336657600151660ustar00rootroot00000000000000from vit import uda MARKABLE_COLUMNS = [ 'depends', 'description', 'due', 'project', 'recur', 'scheduled', 'start', 'status', 'tags', 'until', ] LABEL_DEFAULTS = { 'active.label': '(A)', 'blocked.label': '(BD)', 'blocking.label': '(BG)', 'completed.label': '(C)', 'deleted.label': '(X)', 'due.label': '(D)', 'due.today.label': '(DT)', 'keyword.label': '(K)', 'overdue.label': '(OD)', 'project.label': '(P)', 'project.none.label': '', 'recurring.label': '(R)', 'scheduled.label': '(S)', 'tag.label': '(T)', 'tag.none.label': '', 'uda.label': '', 'uda.priority.label': '(PR)', 'until.label': '(U)', } class Markers: def __init__(self, config, task_config): self.config = config self.task_config = task_config self.enabled = self.config.get('marker', 'enabled') if self.enabled: self.udas = uda.get_configured(self.task_config) self.markable_columns = MARKABLE_COLUMNS + list(self.udas.keys()) self.configured_columns = self.config.get('marker', 'columns') self.set_columns() self.header_label = self.config.get('marker', 'header_label') self.require_color = self.config.get('marker', 'require_color') self.include_subprojects = self.config.get('marker', 'include_subprojects') self.compose_labels() self.set_none_label_attributes() def set_columns(self): self.columns = self.markable_columns if self.configured_columns == 'all' else self.configured_columns.split(',') def compose_labels(self): self.labels = LABEL_DEFAULTS.copy() for uda in list(self.udas.keys()): self.labels['uda.%s.none.label' % uda] = '' for key, value in self.config.items('marker'): if key not in self.config.defaults['marker']: self.labels[key] = value if self.include_subprojects: self.add_project_children() def set_none_label_attributes(self): for label_type in ['project', 'tag', 'uda.priority']: label = '%s.none.label' % label_type attr = '%s_has_none_marker' % label_type.replace('.', '_') has_marker = self.labels[label] != '' setattr(self, attr, has_marker) def add_project_children(self): project_prefix = 'project.' label_suffix = '.label' for label, marker in self.get_project_labels(): for entry in self.task_config.projects: new_label = '%s%s' % (project_prefix, entry) if not self.has_label(new_label) and new_label.startswith(label[:-len(label_suffix)]): self.labels['%s%s' % (new_label, label_suffix)] = marker def get_project_labels(self): return sorted([(label, marker) for label, marker in self.labels.items() if self.is_project_label(label)], reverse=True) def is_project_label(self, label): return label.startswith('project.') and label not in ['project.label', 'project.none.label'] def has_label(self, label): return label in self.labels vit-2.3.2/vit/multi_widget.py000066400000000000000000000040021451336657600162050ustar00rootroot00000000000000import urwid class MultiWidget(urwid.Widget): """A widget container that presents one child widget at a time.""" #: A dict containing all widgets. widgets = {} #: Name of the current widget. current = None def __init__(self): self.widgets = {} self.current = None def add_widget(self, name, widget): """Adds a widget by name.""" self.widgets[name] = widget def show_widget(self, name): assert 0 < len(self.widgets) self.current = name self._invalidate() @property def widget_count(self): """The function name is pretty much self-explanatory.""" return len(self.widgets) @property def current_widget(self): """Returns a widget that is currently being rendered. If the widget list is empty, it returns None.""" if self.current and self.current in self.widgets: return self.widgets[self.current] else: return None def selectable(self): """It appears ``selectable()`` must return ``True`` in order to get any key input.""" return True def rows(self, size, focus=False): return self.current_widget.rows(size, focus) if self.current_widget is not None else 0 def render(self, size, focus=False): assert self.current_widget is not None return self.current_widget.render(size, focus) def keypress(self, size, key): """Passes key inputs to the current widget. If the current widget is ``None`` then it returns the given key input so that ``unhandled_input`` function can handle it.""" if self.current_widget is not None: return self.current_widget.keypress(size, key) else: return key def mouse_event(self, size, event, button, col, row, focus): if self.current_widget is not None: return self.current_widget.mouse_event( size, event, button, col, row, focus) else: return False vit-2.3.2/vit/option_parser.py000066400000000000000000000052041451336657600164010ustar00rootroot00000000000000import glob import sys import argparse from vit import version parser = argparse.ArgumentParser( description="VIT (Visual Interactive Taskwarrior)", usage='%(prog)s [options] [report] [filters]', formatter_class=argparse.RawTextHelpFormatter, allow_abbrev=False, epilog=""" VIT (Visual Interactive Taskwarrior) is a lightweight, curses-based front end for Taskwarrior that provides a convenient way to quickly navigate and process tasks. VIT allows you to interact with tasks in a Vi-intuitive way. A goal of VIT is to allow you to customize the way in which you use Taskwarrior's core commands as well as to provide a framework for easily dispatching external commands. While VIT is running, type :help followed by enter to review basic command/navigation actions. See https://github.com/vit-project/vit for more information. """ ) parser.add_argument('-v', '--version', action='version', version=version.VIT, ) parser.add_argument('--list-actions', dest="list_actions", default=False, 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): print("%s:" % item) print("\t%s\n" % description) def list_actions(): from vit.registry import ActionRegistry from vit.actions import Actions action_registry = ActionRegistry() 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 vit-2.3.2/vit/pid_manager.py000066400000000000000000000027231451336657600157660ustar00rootroot00000000000000import 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) vit-2.3.2/vit/process.py000066400000000000000000000040471451336657600151770ustar00rootroot00000000000000import os import re import subprocess import copy from vit import env from vit.util import clear_screen, string_to_args DEFAULT_CONFIRM = 'Press Enter to continue...' class Command: def __init__(self, config): self.config = config self.env = env.user.copy() self.env['TASKRC'] = self.config.taskrc_path def run(self, command, capture_output=False, custom_env={}): if isinstance(command, str): command = string_to_args(command) env = copy.copy(self.env) env.update(custom_env) kwargs = { 'env': env, } if capture_output: kwargs.update({ 'stdout': subprocess.PIPE, 'stderr': subprocess.PIPE, 'universal_newlines': True, }) try: proc = subprocess.Popen(command, **kwargs) stdout, stderr = proc.communicate() returncode = proc.returncode except Exception as e: stdout = '' stderr = e.message if hasattr(e, 'message') else str(e) returncode = 1 return returncode, stdout, self.filter_errors(returncode, stderr) def result(self, command, confirm=DEFAULT_CONFIRM, capture_output=False, print_output=False, clear=True, custom_env={}): if clear: clear_screen() returncode, stdout, stderr = self.run(command, capture_output, custom_env) output = returncode == 0 and stdout or stderr if print_output: print(output) if confirm: try: input(confirm) except: raw_input(confirm) if clear: clear_screen() return returncode, output def filter_errors(self, returncode, error_string): if not error_string: return '' if returncode == 0 else 'unknown' regex = '(TASKRC override)|(^$)' filtered_lines = list(filter(lambda s: False if len(re.findall(regex, s)) else True, error_string.split("\n"))) return "\n".join(filtered_lines) vit-2.3.2/vit/readline.py000066400000000000000000000127461451336657600153110ustar00rootroot00000000000000import string import re class Readline: def __init__(self, edit_obj): self.edit_obj = edit_obj word_chars = string.ascii_letters + string.digits + "_" self._word_regex1 = re.compile( "([%s]+)" % "|".join(re.escape(ch) for ch in word_chars) ) self._word_regex2 = re.compile( "([^%s]+)" % "|".join(re.escape(ch) for ch in word_chars) ) def keys(self): return ('ctrl p', 'ctrl n', 'ctrl a', 'ctrl e', 'ctrl b', 'ctrl f', 'ctrl h', 'ctrl d', 'ctrl t', 'ctrl u', 'ctrl k', 'meta b', 'meta f', 'ctrl w', 'meta d') def keypress(self, key): # Move to the previous line. if key in ('ctrl p',): text = self.edit_obj.history.previous(self.edit_obj.metadata['history']) if text != False: self.edit_obj.set_edit_text(text) return None # Move to the next line. elif key in ('ctrl n',): text = self.edit_obj.history.next(self.edit_obj.metadata['history']) if text != False: self.edit_obj.set_edit_text(text) return None # Jump to the beginning of the line. elif key in ('ctrl a',): self.edit_obj.set_edit_pos(0) return None # Jump to the end of the line. elif key in ('ctrl e',): self.edit_obj.set_edit_pos(len(self.edit_obj.get_edit_text())) return None # Jump backward one character. elif key in ('ctrl b',): self.edit_obj.set_edit_pos(self.edit_obj.edit_pos - 1) return None # Jump forward one character. elif key in ('ctrl f',): self.edit_obj.set_edit_pos(self.edit_obj.edit_pos + 1) return None # Delete previous character. elif key in ('ctrl h',): if self.edit_obj.edit_pos > 0: self.edit_obj.set_edit_pos(self.edit_obj.edit_pos - 1) self.edit_obj.set_edit_text( self.edit_obj.get_edit_text()[0 : self.edit_obj.edit_pos] + self.edit_obj.get_edit_text()[self.edit_obj.edit_pos + 1 :]) return None # Delete next character. elif key in ('ctrl d',): if self.edit_obj.edit_pos < len(self.edit_obj.get_edit_text()): edit_pos = self.edit_obj.edit_pos self.edit_obj.set_edit_text( self.edit_obj.get_edit_text()[0 : self.edit_obj.edit_pos] + self.edit_obj.get_edit_text()[self.edit_obj.edit_pos + 1 :]) self.edit_obj.set_edit_pos(edit_pos) return None # Transpose characters. elif key in ('ctrl t',): # Can't transpose if there are less than 2 chars if len(self.edit_obj.get_edit_text()) < 2: return None self.edit_obj.set_edit_pos(max(2, self.edit_obj.edit_pos + 1)) new_edit_pos = self.edit_obj.edit_pos edit_text = self.edit_obj.get_edit_text() self.edit_obj.set_edit_text( edit_text[: new_edit_pos - 2] + edit_text[new_edit_pos - 1] + edit_text[new_edit_pos - 2] + edit_text[new_edit_pos :]) self.edit_obj.set_edit_pos(new_edit_pos) return None # Delete backwards to the beginning of the line. elif key in ('ctrl u',): self.edit_obj.set_edit_text(self.edit_obj.get_edit_text()[self.edit_obj.edit_pos :]) self.edit_obj.set_edit_pos(0) return None # Delete forwards to the end of the line. elif key in ('ctrl k',): self.edit_obj.set_edit_text(self.edit_obj.get_edit_text()[: self.edit_obj.edit_pos]) return None # Jump backward one word. elif key in ('meta b',): self.jump_backward_word() return None # Jump forward one word. elif key in ('meta f',): self.jump_forward_word() return None # Delete backwards to the beginning of the current word. elif key in ('ctrl w',): old_edit_pos = self.edit_obj.edit_pos self.jump_backward_word() new_edit_pos = self.edit_obj.edit_pos self.edit_obj.set_edit_text(self.edit_obj.edit_text[: new_edit_pos] + self.edit_obj.edit_text[old_edit_pos :]) self.edit_obj.set_edit_pos(new_edit_pos) return None # Delete forwards to the end of the current word. elif key in ('meta d',): edit_pos = self.edit_obj.edit_pos self.jump_forward_word() self.edit_obj.set_edit_text(self.edit_obj.edit_text[: edit_pos] + self.edit_obj.edit_text[self.edit_obj.edit_pos :]) self.edit_obj.set_edit_pos(edit_pos) return None def jump_backward_word(self): for match in self._word_regex1.finditer( self.edit_obj.edit_text[: self.edit_obj.edit_pos][::-1] ): self.edit_obj.set_edit_pos(self.edit_obj.edit_pos - match.end(1)) return self.edit_obj.set_edit_pos(0) def jump_forward_word(self): for match in self._word_regex2.finditer( self.edit_obj.edit_text[self.edit_obj.edit_pos :] ): if match.start(1) > 0: self.edit_obj.set_edit_pos(self.edit_obj.edit_pos + match.start(1)) return self.edit_obj.set_edit_pos(len(self.edit_obj.edit_text)) vit-2.3.2/vit/registry.py000066400000000000000000000036721451336657600153740ustar00rootroot00000000000000import uuid class ActionRegistrar: def __init__(self, registry): self.registry = registry self.uuid = uuid.uuid4() def register(self, name, description): self.registry.register(self.uuid, name, description) def deregister(self, name=None): if name: self.registry.deregister(name) else: any(self.registry.deregister(action) for _, action in self.actions().items()) def actions(self): return self.registry.get_registered(self.uuid) class ActionRegistry: def __init__(self): self.actions = {} self.noop_action_name = 'NOOP' def get_registrar(self): return ActionRegistrar(self) def get_registered(self, registration_id): return list(filter(lambda action: self.actions[action]['registration_id'] == registration_id, self.actions)) def register(self, registration_id, name, description): self.actions[self.make_action_name(name)] = { 'name': name, 'registration_id': registration_id, 'description': description, } def deregister(self, name_or_action): name = name_or_action['name'] if isinstance(name_or_action, dict) else name_or_action self.actions.pop(self.make_action_name(name)) def get_actions(self): return self.actions.keys() def make_action_name(self, name): return 'ACTION_%s' % name def noop(self): pass class RequestReply: def __init__(self): self.handlers = {} def set_handler(self, name, description, callback): self.handlers[name] = { 'description': description, 'callback': callback, } def unset_handler(self, name): self.handlers.pop(name) def request(self, name, *args): try: return self.handlers[name]['callback'](*args) except KeyError: raise KeyError("No handler '%s' has been set" % name) vit-2.3.2/vit/task.py000066400000000000000000000121361451336657600144610ustar00rootroot00000000000000import os from functools import reduce import tasklib from tasklib.task import Task from tasklib.backends import TaskWarriorException from vit import util from vit.exception import VitException class TaskListModel: def __init__(self, task_config, reports, report=None, data_location=None): if not data_location: data_location = task_config.subtree('data.location') self.data_location = os.path.expanduser(data_location) self.tw = tasklib.TaskWarrior(self.data_location) self.reports = reports self.report = report self.tasks = [] if report: self.update_report(report) def add(self, contact): pass def active_report(self): return self.reports[self.report] def parse_error(self, err): messages = filter(lambda l : l.startswith('Error:'), str(err).splitlines()) return "\n".join(messages) def update_report(self, report, context_filters=[], extra_filters=[]): self.report = report active_report = self.active_report() report_filters = active_report['filter'] if 'filter' in active_report else [] filters = self.build_task_filters(context_filters, report_filters, extra_filters) try: self.tasks = self.tw.tasks.filter(filters) if filters else self.tw.tasks.all() # NOTE: tasklib uses lazy loading and some operation is necessary # for self.tasks to actually be populated here. # See https://github.com/robgolding/tasklib/issues/81 len(self.tasks) except TaskWarriorException as err: raise VitException(self.parse_error(err)) def build_task_filters(self, *all_filters): def reducer(accum, filters): if filters: accum.append('( %s )' % ' '.join(filters)) return accum filter_parts = reduce(reducer, all_filters, []) return ' '.join(filter_parts) if filter_parts else '' def get_task(self, uuid): try: return self.tw.tasks.get(uuid=uuid) except Task.DoesNotExist: return False def task_id(self, uuid): try: task = self.tw.tasks.get(uuid=uuid) return util.task_id_or_uuid_short(task) except Task.DoesNotExist: return False def task_description(self, uuid, description): task = self.get_task(uuid) if task: task['description'] = description task.save() return task return False def task_annotate(self, uuid, description): task = self.get_task(uuid) if task: task.add_annotation(description) return task return False def task_denotate(self, uuid, annotation): task = self.get_task(uuid) if task: task.remove_annotation(annotation) return task return False def task_priority(self, uuid, priority): task = self.get_task(uuid) if task: task['priority'] = priority task.save() return task return False def task_project(self, uuid, project): task = self.get_task(uuid) if task: task['project'] = project task.save() return task return False def task_done(self, uuid): task = self.get_task(uuid) if task: try: task.done() return True, task except (Task.CompletedTask, Task.DeletedTask) as e: return False, e return False, None def task_delete(self, uuid): task = self.get_task(uuid) if task: try: task.delete() return True, task except Task.DeletedTask as e: return False, e return False, None def task_start_stop(self, uuid): task = self.get_task(uuid) if task: try: task.stop() if task.active else task.start() return True, task except (Task.CompletedTask, Task.DeletedTask, Task.ActiveTask, Task.InactiveTask) as e: return False, e return False, None def task_tags(self, uuid, tags): task = self.get_task(uuid) if task: for tag in tags: marker = tag[0] if marker in ('+', '-'): tag = tag[1:] if marker == '+': task['tags'].add(tag) elif tag in task['tags']: task['tags'].remove(tag) else: task['tags'].add(tag) task.save() return task return False # def get_summary(self, report=None): # report = report or self.report # self.update_report(report) # summary = [] # for task in self.tasks: # summary.append(self.build_task_row(task)) # return summary # # def _reload_list(self, new_value=None): # self._list_view.options = self._model.get_summary() # self._list_view.value = new_value vit-2.3.2/vit/task_list.py000066400000000000000000000560701451336657600155210ustar00rootroot00000000000000from operator import itemgetter from collections import OrderedDict from itertools import repeat from functools import partial, reduce from time import sleep import re import math from functools import cmp_to_key import urwid from vit import util 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: 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 self.task_config = task_config self.formatter = formatter self.screen = screen self.on_select = on_select self.event = event self.action_manager = action_manager self.request_reply = request_reply self.markers = markers self.draw_screen = draw_screen_callback self.list_walker = urwid.SimpleFocusListWalker([]) self.row_striping = self.config.row_striping_enabled self.listbox = TaskListBox(self.list_walker, self.screen, event=self.event, request_reply=self.request_reply, action_manager=self.action_manager) self.init_event_listeners() self.set_request_callbacks() def init_event_listeners(self): def signal_handler(): self.update_focus() urwid.connect_signal(self.list_walker, 'modified', signal_handler) def task_list_keypress(data): self.update_header(data['size']) self.event.listen('task-list:keypress', task_list_keypress) self.event.listen('task-list:size:change', self.size_changed) self.event.listen('task-list:keypress:down', self.task_list_keypress_down) self.event.listen('task-list:keypress:page_down', self.task_list_keypress_page_down) self.event.listen('task-list:keypress:end', self.task_list_keypress_end) self.event.listen('task-list:keypress:focus_valign_center', self.task_list_keypress_focus_valign_center) def set_request_callbacks(self): self.request_reply.set_handler('task-table:batch:next', 'Render next batch of tasks', lambda: self.get_next_task_batch()) def get_next_task_batch(self): return self.batcher.add() def task_list_keypress_down(self, size): self.batcher.add(1) def task_list_keypress_page_down(self, size): _, rows = size self.batcher.add(rows) def task_list_keypress_end(self, size): self.batcher.add(0) def task_list_keypress_focus_valign_center(self, size): _, rows = size half_rows = math.ceil(rows / 2) self.batcher.add(half_rows) def get_blocking_task_uuids(self): return self.request_reply.request('application:blocking_task_uuids') def update_data(self, report, tasks): self.report = report self.tasks = tasks self.list_walker.clear() self.columns = [] self.column_names = [] self.rows = [] self.sort() self.set_column_metadata() if self.markers.enabled: self.set_marker_columns() self.add_markers_column() self.indent_subprojects = self.subproject_indentable() self.project_cache = {} # TODO: This is for the project placeholders, feels sloppy. self.project_formatter = ProjectFormatter('project', self.report, self.formatter, self.get_blocking_task_uuids()) self.build_rows() self.clean_columns() self.project_column_idx = self.get_project_column_idx() self.reconcile_column_width_for_label() self.resize_columns() self.build_table() self.listbox.set_focus_position() self.update_focus() def update_header(self, size): if self.project_column_idx is not None: self.update_project_column_header(size) def get_project_column_idx(self): for idx, column in enumerate(self.columns): if column['name'] == 'project': return idx return None def get_project_from_row(self, row): return row.task['project'] if isinstance(row, SelectableRow) else row.project def update_project_column_header(self, size): if self.indent_subprojects: top, _, _ = self.listbox.get_top_middle_bottom_rows(size) if top: project = self.get_project_from_row(top) if project: _, parents = util.project_get_subproject_and_parents(project) self.set_project_column_header(parents) else: self.set_project_column_header() def set_project_column_header(self, parents=None): column_index = self.project_column_idx (columns_widget, _) = self.header.original_widget.contents[column_index] (widget, _) = columns_widget.contents[0] label = self.project_label_for_parents(parents) widget.original_widget.original_widget.set_text(label) def project_label_for_parents(self, parents): return '.'.join(parents) if parents else self.task_config.get_column_label(self.report['name'], 'project') def update_focus(self): if self.listbox.focus: if self.listbox.previous_focus_position != self.listbox.focus_position: if self.listbox.previous_focus_position is not None and self.listbox.previous_focus_position < len(self.list_walker): self.list_walker[self.listbox.previous_focus_position].reset_attr_map() if self.listbox.focus_position is not None: self.update_focus_attr('reveal focus') self.listbox.previous_focus_position = self.listbox.focus_position else: self.update_focus_attr('reveal focus') else: self.listbox.previous_focus_position = None def update_focus_attr(self, attr, position=None): if position is None: position = self.listbox.focus_position self.list_walker[position].row.set_attr_map({None: attr}) 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 and repeat_times > 0: self.update_focus_attr('flash on', position) self.draw_screen() for i in repeat(None, repeat_times): sleep(pause_seconds) self.update_focus_attr('flash off', position) self.draw_screen() sleep(pause_seconds) self.update_focus_attr('flash on', position) self.draw_screen() sleep(pause_seconds) self.update_focus_attr('reveal focus', position) self.draw_screen() def sort(self): if 'sort' in self.report: for column, order, collate in reversed(self.report['sort']): def comparator(first, second): if first[column] is not None and second[column] is not None: return -1 if first[column] < second[column] else 1 if first[column] > second[column] else 0 elif first[column] is None and second[column] is None: return 0 elif first[column] is not None and second[column] is None: return -1 elif first[column] is None and second[column] is not None: return 1 if order and order == 'descending': self.tasks = sorted(self.tasks, key=cmp_to_key(comparator), reverse=True) else: self.tasks = sorted(self.tasks, key=cmp_to_key(comparator)) def column_formatter_kwargs(self): kwargs = {} if 'dateformat' in self.report: kwargs['custom_formatter'] = self.report['dateformat'] return kwargs def set_marker_columns(self): self.report_marker_columns = [c for c in self.markers.columns if c not in self.column_names] def add_markers_column(self): name, formatter_class = self.formatter.get(MARKER_COLUMN_NAME) self.columns.insert(0, { 'name': name, 'label': self.markers.header_label, 'formatter': formatter_class(self.report, self.formatter, self.report_marker_columns, self.get_blocking_task_uuids()), 'width': 0, 'align': 'right', }) self.column_names.insert(0, name) def add_column(self, name, label, formatter_class, align='left'): self.columns.append({ 'name': name, 'label': label, 'formatter': formatter_class, 'width': 0, 'align': align, }) self.column_names.append(name) def set_column_metadata(self): kwargs = self.column_formatter_kwargs() for idx, column_formatter in enumerate(self.report['columns']): name, formatter_class = self.formatter.get(column_formatter) self.add_column(name, self.report['labels'][idx], formatter_class(name, self.report, self.formatter, self.get_blocking_task_uuids(), **kwargs)) def is_marker_column(self, column): return column == MARKER_COLUMN_NAME def has_marker_column(self): return MARKER_COLUMN_NAME in self.column_names def build_rows(self): self.task_row_striping_reset() for task in self.tasks: row_data = [] self.inject_project_placeholders(task) alt_row = self.task_row_striping() for idx, column in enumerate(self.columns): formatted_value = column['formatter'].format(task[column['name']], task) width, text_markup = self.build_row_column(formatted_value) self.update_column_width(idx, column['width'], width) row_data.append(text_markup) self.rows.append(TaskRow(task, row_data, alt_row)) def update_column_width(self, idx, current_width, new_width): if new_width > current_width: self.columns[idx]['width'] = new_width def build_row_column(self, formatted_value): if isinstance(formatted_value, tuple): return formatted_value else: width = unicode_len(formatted_value) if formatted_value else 0 return width, formatted_value def subproject_indentable(self): return self.config.subproject_indentable and self.report['subproject_indentable'] def inject_project_placeholders(self, task): project = task['project'] if self.indent_subprojects and project: parents = self.project_may_need_placeholders(project) if parents: to_inject = self.build_project_placeholders_to_inject(parents, []) for project_parts in to_inject: self.inject_project_placeholder(project_parts) def build_project_placeholders_to_inject(self, parents, to_inject): project = '.'.join(parents) if project in self.project_cache: return to_inject else: self.project_cache[project] = True to_inject.append(parents.copy()) parents.pop() if len(parents) > 0: return self.build_project_placeholders_to_inject(parents, to_inject) else: to_inject.reverse() return to_inject def project_may_need_placeholders(self, project): if project not in self.project_cache: self.project_cache[project] = True _, parents = util.project_get_subproject_and_parents(project) return parents def inject_project_placeholder(self, project_parts): project = '.'.join(project_parts) (width, spaces, indicator, subproject) = self.formatter.format_subproject_indented(project_parts) # TODO: This is pretty ugly... alt_row = self.task_row_striping() self.rows.append(ProjectRow(project, [spaces, indicator, (self.project_formatter.colorize(project), subproject)], alt_row)) def clean_columns(self): self.clean_markers_column() if self.task_config.print_empty_columns else self.clean_empty_columns() def clean_markers_column(self): self.non_filtered_columns = [False if (c['name'] == MARKER_COLUMN_NAME and c['width'] == 0) else c for c in self.columns] self.columns = [c for c in self.columns if not (c['name'] == MARKER_COLUMN_NAME and c['width'] == 0)] def clean_empty_columns(self): self.non_filtered_columns = [c if c['width'] > 0 else False for c in self.columns] self.columns = [c for c in self.columns if c['width'] > 0] def resize_columns(self): cols, _ = self.listbox.size padding = (len(self.columns) - 1) * COLUMN_PADDING total_width = padding to_adjust= [] for idx, column in enumerate(self.columns): width = column['width'] total_width += width if width > REDUCE_COLUMN_WIDTH_LIMIT: to_adjust.append({'idx': idx, 'width': width}) if total_width > cols: self.adjust_oversized_columns(total_width - cols, to_adjust) if to_adjust: # This is called recursively to account for cases when further # reduction is necessary because one or more column's reductions # were limited to REDUCE_COLUMN_WIDTH_LIMIT. self.resize_columns() def adjust_oversized_columns(self, reduce_by, to_adjust): to_adjust = list(map(lambda c: c.update({'ratio': (c['width'] - REDUCE_COLUMN_WIDTH_LIMIT) / c['width']}) or c, to_adjust)) ratio_total = reduce(lambda acc, c: acc + c['ratio'], to_adjust, 0) to_adjust = list(map(lambda c: c.update({'percentage': c['ratio'] / ratio_total}) or c, to_adjust)) for c in to_adjust: adjusted_width = c['width'] - math.ceil(reduce_by * c['percentage']) self.columns[c['idx']]['width'] = adjusted_width if adjusted_width > REDUCE_COLUMN_WIDTH_LIMIT else REDUCE_COLUMN_WIDTH_LIMIT def reconcile_column_width_for_label(self): for idx, column in enumerate(self.columns): label_len = unicode_len(column['label']) if column['width'] < label_len: self.columns[idx]['width'] = label_len def get_alt_row_background_modifier(self): return '.striped-table-row' def task_row_striping_reset(self): self.task_alt_row = False def task_row_striping(self): if self.row_striping: self.task_alt_row = not self.task_alt_row modifier = self.task_alt_row and self.get_alt_row_background_modifier() or '' self.formatter.task_colorizer.set_background_modifier(modifier) return self.task_alt_row def format_task_batch(self, partial, start_idx): return [SelectableRow(self.non_filtered_columns, obj, start_idx + idx, on_select=self.on_select) if isinstance(obj, TaskRow) else ProjectPlaceholderRow(self.columns, obj, start_idx + idx) for idx, obj in enumerate(partial)] def build_table(self): self.make_header() self.batcher = ListBatcher(self.rows, self.list_walker, batch_to_formatter=self.format_task_batch) _, rows = self.listbox.size self.batcher.add(rows) def make_header(self): columns = [] if len(self.columns) > 0: last_column = self.columns[-1] columns = [self.make_header_column(column, column == last_column) for column in self.columns] columns.append(self.make_padding('list-header-column')) list_header = urwid.Columns(columns) self.header = urwid.AttrMap(list_header, 'list-header') def make_header_column(self, column, is_last, space_between=COLUMN_PADDING): padding_width = 0 if is_last else space_between total_width = column['width'] + padding_width column_content = urwid.AttrMap(urwid.Padding(urwid.Text(column['label'], align='left')), 'list-header-column') padding_content = self.make_padding(is_last and 'list-header-column' or 'list-header-column-separator') columns = urwid.Columns([(column['width'], column_content), (padding_width, padding_content)]) return (total_width, columns) def make_padding(self, display_attr): return urwid.AttrMap(urwid.Padding(urwid.Text('')), display_attr) def rows_size_grew(self, data): _, old_rows = data['old_size'] _, new_rows = data['new_size'] if new_rows > old_rows: return new_rows - old_rows return 0 def size_changed(self, data): self.update_header(data['new_size']) grew = self.rows_size_grew(data) if grew > 0: self.batcher.add(grew) class TaskRow: def __init__(self, task, data, alt_row): self.task = task self.data = data self.alt_row = alt_row self.uuid = self.task['uuid'] self.id = self.task['id'] class ProjectRow: def __init__(self, project, placeholder, alt_row): self.project = project self.placeholder = placeholder self.alt_row = alt_row class SelectableRow(urwid.WidgetWrap): """Wraps 'urwid.Columns' to make it selectable. This class has been slightly modified, but essentially corresponds to this class posted on stackoverflow.com: https://stackoverflow.com/questions/52106244/how-do-you-combine-multiple-tui-forms-to-write-more-complex-applications#answer-52174629""" def __init__(self, columns, row, position, *, on_select=None, space_between=COLUMN_PADDING): self.task = row.task self.uuid = row.uuid self.id = row.id self.alt_row = row.alt_row self.position = position self._columns = urwid.Columns([(column['width'], urwid.Text(row.data[idx], align=column['align'])) for idx, column in enumerate(columns) if column], dividechars=space_between) self.set_display_attr() self.row = urwid.AttrMap(self._columns, self.display_attr) # Wrap 'urwid.Columns'. super().__init__(self.row) # A hook which defines the behavior that is executed when a specified key is pressed. self.on_select = on_select def set_display_attr(self): self.display_attr = self.alt_row and 'striped-table-row' or '' def reset_attr_map(self): self.row.set_attr_map({None: self.display_attr}) def __repr__(self): return "{}(id={}, uuid={})".format(self.__class__.__name__, self.id, self.uuid) def selectable(self): return True def keypress(self, size, key): if self.on_select: key = self.on_select(self, size, key) return key class ProjectPlaceholderRow(urwid.WidgetWrap): """Wraps 'urwid.Columns' for a project placeholder row. """ def __init__(self, columns, row, position, space_between=COLUMN_PADDING): self.uuid = None self.id = None self.alt_row = row.alt_row self.project = row.project self.placeholder = row.placeholder self.position = position self._columns = urwid.Columns([(column['width'], urwid.Text(row.placeholder if isinstance(column['formatter'], ProjectFormatter) else '', align=column['align'])) for column in columns], dividechars=space_between) self.set_display_attr() self.row = urwid.AttrMap(self._columns, self.display_attr) # Wrap 'urwid.Columns'. super().__init__(self.row) def set_display_attr(self): self.display_attr = self.alt_row and 'striped-table-row' or '' def reset_attr_map(self): self.row.set_attr_map({None: self.display_attr}) pass def __repr__(self): return "{}(placeholder={})".format(self.__class__.__name__, self.placeholder) class TaskListBox(BaseListBox): """Maps task list shortcuts to default ListBox class. """ def __init__(self, body, screen, event=None, request_reply=None, action_manager=None): # TODO: Any way to get the actual listbox size here? It doesn't seem # to be accessible before the first render() call. self.screen = screen self.size = self.screen.get_cols_rows() return super().__init__(body, event, request_reply, action_manager) def render(self, size, focus=False): if size != self.size: data = { 'old_size': self.size, 'new_size': size, } self.size = size self.event.emit('task-list:size:change', data) return super().render(size, focus) def keypress_down(self, size): self.event.emit('task-list:keypress:down', size) super().keypress_down(size) def keypress_page_down(self, size): self.event.emit('task-list:keypress:page_down', size) super().keypress_page_down(size) def keypress_end(self, size): self.event.emit('task-list:keypress:end', size) super().keypress_end(size) def keypress_focus_valign_center(self, size): self.event.emit('task-list:keypress:focus_valign_center', size) super().keypress_focus_valign_center(size) def set_focus_position(self, start_idx=0): for idx, widget in enumerate(self.body[start_idx:]): if widget.selectable(): self.set_focus(start_idx + idx) return def focus_by_batch(self, match_callback, start_idx): end_idx = start_idx for idx, row in enumerate(self.body[start_idx:]): end_idx = idx + start_idx if match_callback(row): self.focus_position = end_idx return True, end_idx return False, end_idx def focus_by_batch_loop(self, match_callback, previous_idx=0): start_idx = 0 while True: found, start_idx = self.focus_by_batch(match_callback, start_idx) complete = self.request_reply.request('task-table:batch:next') if found: return elif complete: found, end_idx = self.focus_by_batch(match_callback, start_idx) if not found: self.set_focus_position(end_idx if previous_idx > end_idx else previous_idx) return def focus_by_task_id(self, task_id): def match_callback(row): return row.id == task_id self.focus_by_batch_loop(match_callback) def focus_by_task_uuid(self, uuid, previous_idx=0): def match_callback(row): return row.uuid == uuid self.focus_by_batch_loop(match_callback, previous_idx) def list_action_executed(self, size, key): data = { 'size': size, } self.event.emit('task-list:keypress', data) vit-2.3.2/vit/theme/000077500000000000000000000000001451336657600142445ustar00rootroot00000000000000vit-2.3.2/vit/theme/__init__.py000066400000000000000000000000001451336657600163430ustar00rootroot00000000000000vit-2.3.2/vit/theme/classic.py000066400000000000000000000015121451336657600162360ustar00rootroot00000000000000theme = [ ('list-header', '', '', '', '', ''), ('list-header-column', 'underline', '', '', 'underline', ''), ('list-header-column-separator', '', '', '', '', ''), ('striped-table-row', 'white', 'dark gray', '', 'white', 'g27'), ('reveal focus', 'white', 'dark blue', 'standout', 'white', 'dark blue'), ('message status', '', '', '', '', ''), ('message error', 'white', 'dark red', 'standout', 'white', 'dark red'), ('status', 'dark blue', 'black', '', 'dark blue', 'black'), ('flash off', 'black', 'black', 'standout', 'black', 'black'), ('flash on', 'white', 'black', 'standout', 'white', 'black'), ('pop_up', 'white', 'black', '', 'white', 'black'), ('button action', 'white', 'light red', '', 'white', 'light red'), ('button cancel', 'black', 'light gray', '', 'black', 'light gray'), ] vit-2.3.2/vit/theme/default.py000066400000000000000000000016361451336657600162500ustar00rootroot00000000000000theme = [ ('list-header', '', '', '', '', ''), ('list-header-column', 'black', 'light gray', '', 'black', 'light gray'), ('list-header-column-separator', 'black', 'light gray', '', 'black', 'light gray'), ('striped-table-row', 'white', 'dark gray', '', 'white', 'g27'), ('reveal focus', 'black', 'dark cyan', 'standout', 'black', 'dark cyan'), ('message status', 'white', 'dark blue', 'standout', 'white', 'dark blue'), ('message error', 'white', 'dark red', 'standout', 'white', 'dark red'), ('status', 'dark magenta', 'black', '', 'dark magenta', 'black'), ('flash off', 'black', 'black', 'standout', 'black', 'black'), ('flash on', 'white', 'black', 'standout', 'white', 'black'), ('pop_up', 'white', 'black', '', 'white', 'black'), ('button action', 'white', 'light red', '', 'white', 'light red'), ('button cancel', 'black', 'light gray', '', 'black', 'light gray'), ] vit-2.3.2/vit/uda.py000066400000000000000000000006321451336657600142660ustar00rootroot00000000000000def get(name, task_config): subtree = task_config.subtree(r'^uda\.%s\.' % name, walk_subtree=False) if 'uda' in subtree and name in subtree['uda']: return subtree['uda'][name] return None def get_configured(task_config): subtree = task_config.subtree(r'^uda\.', walk_subtree=False) if 'uda' in subtree: return {k:v['type'] for k, v in subtree['uda'].items()} return {} vit-2.3.2/vit/util.py000066400000000000000000000027301451336657600144730ustar00rootroot00000000000000import os import sys import curses import shlex from urwid.util import calc_width curses.setupterm() e3_seq = curses.tigetstr('E3') or b'' clear_screen_seq = curses.tigetstr('clear') or b'' def clear_screen(): os.write(sys.stdout.fileno(), e3_seq + clear_screen_seq) def string_to_args(string): try: return shlex.split(string) except ValueError: return [] def string_to_args_on_whitespace(string): try: lex = shlex.shlex(string) lex.whitespace_split = True return list(lex) except ValueError: return [] def is_mouse_event(key): return not isinstance(key, str) def uuid_short(uuid): return uuid[0:8] def task_id_or_uuid_short(task): return task['id'] or uuid_short(task['uuid']) def task_pending(task): return task['status'] == 'pending' def task_completed(task): return task['status'] == 'completed' or task['status'] == 'deleted' def project_get_subproject_and_parents(project): parts = project.split('.') subproject = parts.pop() parents = parts if len(parts) > 0 else None return subproject, parents def project_get_root(project): return project.split('.')[0] if project else None def file_to_class_name(file_name): words = file_name.split('_') return ''.join((w.capitalize() for w in words)) 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)) vit-2.3.2/vit/version.py000066400000000000000000000000161451336657600151760ustar00rootroot00000000000000VIT = '2.3.2' vit-2.3.2/vit/xdg.py000066400000000000000000000007431451336657600143020ustar00rootroot00000000000000import os from vit import env def get_xdg_config_dir(user_config_dir, resource): xdg_config_home = env.user.get("XDG_CONFIG_HOME") or os.path.join( os.path.expanduser("~"), ".config" ) xdg_config_dirs = [xdg_config_home] + ( env.user.get("XDG_CONFIG_DIRS") or "/etc/xdg" ).split(":") for config_dir in xdg_config_dirs: path = os.path.join(config_dir, resource) if os.path.exists(path): return path return None