├── AUTHORS ├── LICENSE ├── Makefile ├── NEWS ├── README.md ├── doc └── man │ ├── taskopen.1 │ ├── taskopen.1.md │ ├── taskopenrc.5 │ └── taskopenrc.5.md ├── examples ├── 1.x │ └── taskreviewrc ├── default ├── markdown ├── templates └── utility_task ├── scripts ├── 1.x │ ├── do_review.sh │ ├── mess2task │ ├── mess2task2 │ └── mutt2task ├── addnote ├── archive ├── attach ├── editnote ├── rawedit ├── taskban ├── users │ └── artur-shaik │ │ └── attach_vifm ├── vims └── xdg-open-cli ├── src ├── config.nim ├── core.nim ├── exec.nim ├── output.nim ├── taskopen.nim ├── taskwarrior.nim └── types.nim └── taskopen.nimble /AUTHORS: -------------------------------------------------------------------------------- 1 | Authors: 2 | 3 | Johannes Schlatow (principal author) 4 | David J Patrick (creative mind) 5 | 6 | The following submitted patches, code or ideas and deserve special thanks: 7 | 8 | Jostein Berntsen 9 | John Hammond 10 | Alan Bowen 11 | Adam Schmalhofer 12 | Paul Beckingham 13 | Scott Kostyshak 14 | Nicolas Porcel 15 | Alex Thorne 16 | Prakash Surya 17 | Ricardo Ruiz 18 | Thomas Praxl 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PREFIX ?= /usr/local 2 | EDITOR ?= vim 3 | OPEN ?= open 4 | 5 | DESTDIR ?= 6 | VERSION ?= $(shell git describe --tags HEAD) 7 | 8 | SRCFILES = $(wildcard src/*.nim) 9 | MANFILES_MD = $(wildcard doc/man/*.1.md) $(wildcard doc/man/*.5.md) 10 | MANFILES = $(MANFILES_MD:.md=) 11 | MANFILES_GZ = $(addsuffix .gz, $(MANFILES)) 12 | 13 | EDITOR_PRESENT := $(shell which $(firstword ${EDITOR}) >/dev/null 2>&1 || echo "not found") 14 | ifneq (,${EDITOR_PRESENT}) 15 | EDITOR := nano 16 | endif 17 | 18 | OPEN_PRESENT := $(shell which $(firstword ${OPEN}) >/dev/null 2>&1 || echo "not found") 19 | ifneq (,${OPEN_PRESENT}) 20 | OPEN=run-mailcap 21 | endif 22 | 23 | OPEN_PRESENT := $(shell which $(firstword ${OPEN}) >/dev/null 2>&1 || echo "not found") 24 | ifneq (,${OPEN_PRESENT}) 25 | OPEN=xdg-open 26 | endif 27 | 28 | all: taskopen 29 | 30 | taskopen: $(SRCFILES) Makefile 31 | nim c -d:version:$(VERSION) -d:release -d:pathext:'${PREFIX}/share/taskopen/scripts' -d:editor:'${EDITOR}' -d:open:'${OPEN}' --outdir:./ src/taskopen.nim 32 | 33 | $(MANFILES_GZ): %.gz: % Makefile 34 | gzip -c $* > $@ 35 | 36 | manpages: $(MANFILES_MD) 37 | pandoc --standalone --to man doc/man/taskopen.1.md -o doc/man/taskopen.1 38 | pandoc --standalone --to man doc/man/taskopenrc.5.md -o doc/man/taskopenrc.5 39 | 40 | install: $(MANFILES_GZ) taskopen 41 | mkdir -p $(DESTDIR)$(PREFIX)/bin 42 | mkdir -p $(DESTDIR)$(PREFIX)/share/man/man1 43 | mkdir -p $(DESTDIR)$(PREFIX)/share/man/man5 44 | install -m 0755 taskopen $(DESTDIR)$(PREFIX)/bin/taskopen 45 | install -m 0644 doc/man/taskopen.1.gz $(DESTDIR)$(PREFIX)/share/man/man1/taskopen.1.gz 46 | install -m 0644 doc/man/taskopenrc.5.gz $(DESTDIR)$(PREFIX)/share/man/man5/taskopenrc.5.gz 47 | mkdir -p $(DESTDIR)$(PREFIX)/share/taskopen/scripts/ 48 | cp -r scripts/* $(DESTDIR)$(PREFIX)/share/taskopen/scripts/ 49 | chmod -R 755 $(DESTDIR)$(PREFIX)/share/taskopen/scripts 50 | mkdir -p $(DESTDIR)$(PREFIX)/share/taskopen/examples 51 | cp -r examples/* $(DESTDIR)$(PREFIX)/share/taskopen/examples 52 | chmod -R 755 $(DESTDIR)$(PREFIX)/share/taskopen/examples 53 | 54 | clean: 55 | rm -f $(MANFILES_GZ) 56 | rm taskopen 57 | 58 | .PHONY: install clean release 59 | -------------------------------------------------------------------------------- /NEWS: -------------------------------------------------------------------------------- 1 | New features in taskopen 2.0.3 2 | 3 | - Allow sorting by entry date of annotations or tasks (#173) 4 | - Add ENTRY environment variable (#173) 5 | 6 | Bugfixes in taskopen 2.0.2 7 | 8 | - Disable taskwarrior's garbage collection (#165) 9 | - Fix compile-time errors when EDITOR or OPEN contain spaces 10 | 11 | Bugfixes and minor improvements in taskopen 2.0.1 12 | 13 | - Fix default Notes regex (#159) 14 | - Fix Makefile on MacOS. (#151) 15 | - Improve documentation. 16 | - Add notes and url action in default config. 17 | - Remove pandoc dependency from install target. 18 | - Simplify config override and creation of default config. 19 | 20 | New features in taskopen 2.0.0 (beta) 21 | 22 | - Moved from perl to nim. 23 | - New command line interface. 24 | - New config file syntax. 25 | - Label-based actions. (#51) 26 | - Colored output. 27 | 28 | Bugfixes and minor improvements in taskopen 1.1.5 29 | 30 | - Fix raw mode bug. (#132) 31 | - Add TASKOPENRC environment variable. (#116, #120) 32 | - Minor documentation improvements. 33 | 34 | Bugfixes and minor improvements in taskopen 1.1.4 35 | 36 | - Suppress configuration override output from taskwarrior (#115) 37 | - Fix installation errors (#114) 38 | 39 | Bugfixes and minor improvements in taskopen 1.1.3 40 | 41 | - Add default PREFIX. (#108) 42 | - Fix parallel make. (#106) 43 | - Fix regular expression matching. (#105) 44 | - Add script 'attach_vifm' that uses vifm for attaching files. (#104) 45 | - Add script 'xdg-open-cli'. (#102) 46 | 47 | Bugfixes and minor improvements in taskopen 1.1.2 48 | 49 | - Allow xdg-open to handle all files. (#96) 50 | - Add environment variable $LAST_MATCH to provide access to the last submatch of a successful REGEX match. (#95) 51 | - Add helper script "addnote" that lets you select a file extension for your notes. (#95) 52 | - Fix file-type filtering, which was broken by introducing the -t/-T switches. 53 | 54 | Bugfixes and minor improvements in taskopen 1.1.1 55 | 56 | - Fix warnings when TEXT_REGEX or NO_ANNOTATION_HOOK are undefined (#93) 57 | - Fix comment in default taskopenrc (#92) 58 | 59 | New features in taskopen 1.1 60 | 61 | - Add FILE_CMD for better customisation (related to #92). 62 | - Implement TEXT_REGEX (#21) and command line switches -t/-T (#37). 63 | 64 | Bugfixes and minor improvements in taskopen 1.1 65 | 66 | - Allow arbitrary numbers of CUSTOM commands (#91). 67 | - Fix quoting of command line arguments (#89). 68 | 69 | Bugfixes in taskopen 1.0.2 70 | 71 | - Fix compatibility issue with taskwarrior 2.4.0 72 | 73 | New features in taskopen 1.0.1 74 | 75 | - Allowing the following environment variables in customizable commands: $FILE, $UUID, $ID, 76 | $ANNOTATION and $LABEL. 77 | - Better cygwin support (thanks to Jostein). 78 | - New argument "-p " allows piping taskopen's output into another command (like clog e.g.). 79 | - More examples. 80 | - Filter broken links, i.e. non existing files. 81 | - Support for auto-attaching annotations if none has been found (#81). 82 | - Added helper script "attach", which uses ranger as a file chooser (#33). 83 | - Added support for arbitrary taskwarrior attributes and UDAs in particular (#78). 84 | 85 | Bugfixes and minor improvements taskopen 1.0.1 86 | 87 | - Fixed handling of UTF8 characters (#73). 88 | - Improved selection of editor binary. 89 | - Open empty files with EDITOR. 90 | - Fixed install script for OSX (#76). 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Taskopen was original developed as a simple wrapper script for taskwarrior that enables interaction with annotations (e.g. open, edit, execute files). 2 | The current version is a pretty powerful customisable tool that supports a variety of use cases. 3 | This README serves as a basic getting-started guide including install instructions and examples. 4 | If your are interested in more details, please have a look at the [wiki] or the [man page]. 5 | 6 | [wiki]: https://github.com/jschlatow/taskopen/wiki/2.0 7 | [man page]: doc/man/taskopen.1.md 8 | 9 | # Dependencies 10 | 11 | This tool is an enhancement to taskwarrior, i.e. it depends on the task binary. See http://www.taskwarrior.org 12 | 13 | Taskopen is implemented in nim (requires at least version 1.4). Taskopen also requires make, xdg-open, and when run on the Windows Subsystem for Linux, wslu. 14 | 15 | The helper scripts are usually run by bash. Some of the scripts also depend on (g)awk. 16 | 17 | # What does it do? 18 | 19 | It allows you to link almost any file, webpage or command to a taskwarrior task by adding a filepath, web-link or uri as an annotation. Text notes, images, PDF files, web addresses, spreadsheets and many other types of links can then be filtered, listed and opened by using taskopen. 20 | 21 | Arbitrary actions can be configured with taskopen to filter and act on the annotations or other task attributes. 22 | 23 | Run `taskopen -h` or `man taskopen` for further details. 24 | The following sections show some (very) basic usage examples. 25 | 26 | 27 | ## Basic usage 28 | 29 | Add a task: 30 | 31 | $ task add Example 32 | 33 | Add an annotation which links to a file: 34 | 35 | $ task 1 annotate -- ~/checklist.txt 36 | 37 | (Note that the "--" instructs taskwarrior to take the following arguments as the description part 38 | without doing any parser magic.) 39 | 40 | Open the linked file by using the task's ID: 41 | 42 | $ taskopen 1 43 | 44 | Or by a filter expression: 45 | 46 | $ taskopen Example 47 | 48 | ## Add default notes 49 | 50 | Inspired by Alan Bowens 'tasknote' you can add a default notes file to a task. These files will be 51 | automatically created by the task's UUID and don't require to annotate the task with a specific file path. 52 | 53 | As soon as you annotate a task with 'Notes': 54 | 55 | $ task 1 annotate Notes 56 | 57 | ...you can open and edit this file by: 58 | 59 | $ taskopen 1 60 | 61 | ...which, by default, opens a file like "~/tasknotes/5727f1c7-2efe-fb6b-2bac-6ce073ba95ee". 62 | 63 | **Note:** You have to create the folder "~/tasknotes" before this works with the default folder. 64 | 65 | Automatically annotating tasks with 'Notes' can be achieved with 'NO_ANNOTATION_HOOK' as described in 66 | the manpage taskopenrc(5). 67 | 68 | Optionally, you may add any file extension to the annotation (e.g. 'Notes.txt'), which will instruct 69 | taskopen to add the same extension to the created file. 70 | 71 | ## Multiple annotations 72 | 73 | You can also add weblinks to a task and even mix all kinds of annotations: 74 | 75 | $ task 1 annotate www.taskwarrior.org 76 | $ task 1 annotate I want to consider this 77 | $ task 1 annotate -- ~/Documents/manual.pdf 78 | $ taskopen 1 79 | 80 | Taskopen will determine the actionable annotations and will show a menu to let the user choose what to do: 81 | 82 | Please select an annotation: 83 | 1) www.taskwarrior.org 84 | 2) ~/Documents/manual.pdf 85 | Type number: 86 | 87 | Note, that the default (`normal`) mode of taskopen is to only show the first applicable action for every annotation. 88 | There is also an `any` mode, which presents a menu with _every_ possible action for each annotation. 89 | In `batch` mode, it executes the first applicable action for all annotations. 90 | 91 | # Installation 92 | 93 | ## With Makefile 94 | 95 | From the command line, run the appropriate code for your OS: 96 | 97 | ### Linux 98 | 99 | ```bash 100 | git clone https://github.com/jschlatow/taskopen.git 101 | cd taskopen 102 | make PREFIX=/usr 103 | sudo make PREFIX=/usr install 104 | ``` 105 | 106 | ### Mac 107 | 108 | ```bash 109 | git clone https://github.com/jschlatow/taskopen.git 110 | cd taskopen 111 | make PREFIX=/usr/local 112 | sudo make PREFIX=/usr/local install 113 | ``` 114 | 115 | This will install the taskopen binary into the `PREFIX/bin` directory. 116 | For packaging, you can add `DESTDIR=/path/to/dir/` to the install command. 117 | 118 | By default, taskopen will recognise any filenames in annotations and open them with `xdg-open` or `open` (on OS X). 119 | Further actions must be specified in a configuration file at `~/.config/taskopen/taskopenrc` or `~/.taskopenrc`. 120 | 121 | A default configuration file can be created with `taskopen --config ~/.config/taskopen/taskopenrc`. 122 | 123 | ### Windows Subsystem for Linux 124 | 125 | In a Windows command prompt, enter 126 | 127 | ``` 128 | git clone https://github.com/jschlatow/taskopen.git 129 | sudo apt install make 130 | sudo apt install nim 131 | sudo apt install wslu 132 | sudo apt install xdg-utils 133 | cd taskopen 134 | make PREFIX=/usr 135 | sudo make PREFIX=/usr install 136 | ``` 137 | 138 | ## With nimble 139 | 140 | t.b.d. 141 | 142 | 143 | ## Migration from taskopen < 2.0 to taskopen >= 2.0 144 | 145 | Due to changes in the command line interface and the configuration file, manual intervention is required. 146 | Please have a look at [CLI migration] and [Config migration]. 147 | 148 | [CLI migration]: https://github.com/jschlatow/taskopen/wiki/CLI#migration 149 | [Config migration]: https://github.com/jschlatow/taskopen/wiki/Configuration#migration 150 | 151 | ## Configuration basics 152 | 153 | In order to customise taskopen to your needs, you may need to adapt its configuration file. 154 | 155 | Taskopen tries to find a configuration file at the following locations: 156 | 157 | * the path specified in `$TASKOPENRC` environment variable 158 | * `$XDG_CONFIG_HOME/taskopen/taskopenrc` if `$XDG_CONFIG_HOME` is set 159 | * `~/.config/taskopen/taskopenrc` 160 | * `~/.taskopenrc`. 161 | 162 | Please also take a look at the manpage [taskopenrc(5)] for the config file syntax. 163 | 164 | [taskopenrc(5)]: doc/man/taskopenrc.5.md 165 | 166 | # Feature highlights 167 | 168 | * Selecting multiple actions from a list 169 | * Arbitrary task filters 170 | * Customised annotation filtering 171 | * Label-based filtering 172 | * Filter command hook 173 | * Inline commands 174 | * Scripts 175 | 176 | ## Selecting multiple actions from a list 177 | 178 | When presented with a menu of actionable annotations, you can select multiple entries (separated by space) or even ranges, e.g. 179 | 180 | 181 | Please select one or multiple actions: 182 | 1) www.taskwarrior.org 183 | 2) ~/Documents/manual.pdf 184 | 3) Notes 185 | 3) ~/Documents/foobar.txt 186 | 3) https://www.github.com 187 | Type number(s): 1 3-5 188 | 189 | ## Arbitrary filters 190 | 191 | Instead of providing taskopen with an ID you can also pass arbitrary filters in taskwarrior 192 | notation, like: 193 | 194 | $ taskopen +next 195 | 196 | or 197 | 198 | $ taskopen +bug pro:taskwarrior 199 | 200 | ## Customised annotation filtering 201 | 202 | Taskopen determines the applicability of an action by matching the annotation against the action's regex. 203 | For instance, multiple file extensions for default notes may be supported by the following action: 204 | 205 | ``` 206 | [Actions] 207 | notes.regex = "^Notes\\.(.*)" 208 | notes.command = "$EDITOR ~/Notes/tasknotes/$UUID.$LAST_MATCH" 209 | ``` 210 | 211 | Note, that taskopen fills the environment variable `$LAST_MATCH` with the part that matches `(.*)`. 212 | 213 | 214 | ## Label-based filtering 215 | 216 | You can label your annotations by using the following syntax: 217 | 218 | $ task 1 annotate view: /path/to/file.html 219 | $ task 1 annotate edit: /path/to/file.html 220 | 221 | When specifying an action, a label regex can be distinguish/filter annotations by their label. 222 | A configuration example is found in [examples/label_regex]. 223 | 224 | [examples/label_regex]: ./examples/label_regex 225 | 226 | ##Filter command hook 227 | 228 | You can specify a filter command that will be executed after the regex matching to determine whether the action really applies to the annotion. 229 | A simple example is to check for file existence: 230 | 231 | ``` 232 | [Actions] 233 | files.regex = "^[\\.\\/~]+.*\\.(.*)" 234 | files.command = "$EDITOR $FILE" 235 | files.filtercommand = "test -e $FILE" 236 | ``` 237 | 238 | An override for all actions can also be provided at the command line. 239 | 240 | ##Inline commands 241 | 242 | Similar to the filter command, the inline command can be used for adding information to the menu. 243 | For instance, to peek show the first five lines of each file with each menu entry, you can add the following to your config file: 244 | 245 | ``` 246 | files.inlinecommand = "head -n5 $FILE" 247 | ``` 248 | 249 | An override for all actions can also be provided at the command line. 250 | 251 | ## Scripts 252 | 253 | Taskopen comes with a bunch of [scripts] that serve as examples to perform more advanced actions, inline commands or filter commands. 254 | Most notably are [addnote] and [editnote]. 255 | The former is used by default as the NO_ANNOTATION_HOOK and annotates the task with the given ID with 'Notes'. 256 | The latter can be used to automatically add a header to new notes files. 257 | Please have a look at the scripts or `man taskopen` to find more documentation. 258 | 259 | [scripts]: ./scripts 260 | [addnote]: ./scripts/addnote 261 | [editnote]: ./scripts/editnote 262 | 263 | ## Contributions 264 | 265 | Feel free to contribute to this project by opening issues or creating pull requests. 266 | If you are keen to fix any open issues, please have a look at ones labelled with _help wanted_. 267 | -------------------------------------------------------------------------------- /doc/man/taskopen.1: -------------------------------------------------------------------------------- 1 | '\" t 2 | .\" Automatically generated by Pandoc 2.18 3 | .\" 4 | .\" Define V font for inline verbatim, using C font in formats 5 | .\" that render this, and otherwise B font. 6 | .ie "\f[CB]x\f[]"x" \{\ 7 | . ftr V B 8 | . ftr VI BI 9 | . ftr VB B 10 | . ftr VBI BI 11 | .\} 12 | .el \{\ 13 | . ftr V CR 14 | . ftr VI CI 15 | . ftr VB CB 16 | . ftr VBI CBI 17 | .\} 18 | .TH "taskopen" "1" "" "Taskopen User Manual" "Version 2.0" 19 | .hy 20 | .SH NAME 21 | .PP 22 | taskopen - A companion application for taskwarrior that augments 23 | annotation handling. 24 | .SH SYNOPSIS 25 | .PP 26 | \f[V]taskopen [subcommand] [options] [filter1 filter2 .. filterN]\f[R] 27 | .SH DESCRIPTION 28 | .PP 29 | Any task in taskwarrior can have zero-or-more annotations. 30 | Originally, taskopen was created to extend the functionality of 31 | taskwarrior with respect to annotations. 32 | As taskwarrior lacks multi-line annotations, the idea was to 33 | \f[I]attach\f[R] a notes file to a task and make it accessible by 34 | taskopen instead. 35 | In more general terms, taskopen is able to perform arbitrary actions on 36 | tasks by executing external commands. 37 | The actions applicable to a task are determined by filtering its 38 | annotations (or any other task attribute). 39 | .PP 40 | In order to customise this, taskopen is configured with a set of 41 | filters, actions and operation modes, see \f[B]taskopenrc\f[R](5). 42 | When executed, it passes the given filter to taskwarrior, and presents 43 | the user with a set of actions that it considers applicable to the 44 | returned tasks. 45 | .SS Filters 46 | .PP 47 | Taskwarrior has a large feature set for 48 | filtering (https://taskwarrior.org/docs/filter.html) on a task-level 49 | basis for generating the reports (task list). 50 | However, as taskopen may offer multiple (and different) actions for 51 | individual task attributes and annotations, it must perform additional 52 | filtering. 53 | .PP 54 | In a first stage, the task filter is provided to taskopen on the command 55 | line and passed to taskwarrior to limit the set of considered tasks. 56 | In a second stage, taskopen checks what actions are applicable to these 57 | tasks (i.e.\ their attributes). 58 | .PP 59 | By default, taskopen operates on the task annotations. 60 | As a generalisation, other attributes can be processed and filtered to 61 | determine the applicable actions. 62 | .SS Annotations 63 | .PP 64 | An annotation in taskwarrior is a single-line of arbitrary text with a 65 | timestamp. 66 | A task can have multiple of these annotations. 67 | For taskopen, the following format of annotations has been established: 68 | .IP 69 | .nf 70 | \f[C] 71 | label: arbitrary text (e.g. url, file path) 72 | \f[R] 73 | .fi 74 | .PP 75 | The label part is optional and used for further filtering (e.g.\ to 76 | distinguish different actions on the same annotation). 77 | Taskopen used the following perl regex for this: 78 | .IP 79 | .nf 80 | \f[C] 81 | (?:(\[rs]S+):\[rs]s+)?(.*) 82 | \f[R] 83 | .fi 84 | .PP 85 | The first match is called the \f[I]label part\f[R] whereas the second 86 | match is the \f[I]file part\f[R]. 87 | Taskopen 2.0 allows filtering of both parts via regular expressions. 88 | .SS Arbitrary attributes 89 | .PP 90 | A task in taskwarrior has various attributes (e.g.\ project, tags, 91 | description). 92 | Taskwarrior even supports user-defined attributes (UDAs), which can for 93 | instance be used to link to an issue id of a bug tracker. 94 | It therefore appears natural to use taskopen also for defining actions 95 | on UDAs. 96 | In contrast to annotation filtering, there is only a single regular 97 | expression that is matched against the attribute\[cq]s value. 98 | .SS Actions 99 | .PP 100 | An action defines what taskopen shall do with the filtered 101 | annotations/attributes. 102 | It is defined by four parameters: \f[I]target\f[R], \f[I]regex\f[R], 103 | \f[I]command\f[R] and \f[I]modes\f[R]. 104 | The \f[I]target\f[R] specifies for what task attribute the action is 105 | defined. 106 | The \f[I]regex\f[R] defines the regular expression(s) used for 107 | determining valid annotations/attributes. 108 | The \f[I]command\f[R] determines the command line that implements the 109 | actual action. 110 | The following placeholders for defining the command line exist: 111 | .TP 112 | \f[V]$ANNOTATION\f[R] 113 | the undecoded (raw) annotation text 114 | .TP 115 | \f[V]$LABEL\f[R] 116 | the label part of the annotation text 117 | .TP 118 | \f[V]$FILE\f[R] 119 | the file part of the annotation text 120 | .TP 121 | \f[V]$UUID\f[R] 122 | the UUID of the task 123 | .TP 124 | \f[V]$ID\f[R] 125 | the running ID of the task 126 | .TP 127 | \f[V]$ENTRY\f[R] 128 | the entry date-time of the annotation 129 | .TP 130 | \f[V]$LAST_MATCH\f[R] 131 | the last sub-pattern of the regular expression used for filtering 132 | .TP 133 | \f[V]$TASK_*\f[R] 134 | any task attribute (e.g.\ \f[V]$TASK_PROJECT\f[R]) or UDA 135 | .TP 136 | \f[V]$ARGS\f[R] 137 | user-defined arguments specified on the taskopen command line 138 | .PP 139 | The \f[I]modes\f[R] of an action is a list of valid modes in which the 140 | action is applicable (see next section). 141 | Optionally (for extensibility), a \f[I]filtercommand\f[R] can be 142 | specified to implement additional filters such as file type checking. 143 | Furthermore, an \f[I]inlinecommand\f[R] can be defined to execute a 144 | particular command for every actionable annotation and display its 145 | output interleaved with the list of actionable annotations. 146 | If an action targets annotations, a \f[I]labelregex\f[R] may specify a 147 | regular expressions that is applied to the label part. 148 | For a detailed descripton of the configuration, please refer to 149 | \f[B]taskopenrc\f[R](5). 150 | .SS Operation modes 151 | .PP 152 | Taskopen implements multiple modes of operation: 153 | .TP 154 | normal mode 155 | In \f[I]normal mode\f[R], taskopen shows a list of actionable 156 | annotations and executes the first matching action for the annotation 157 | selected by the user. 158 | .TP 159 | batch mode 160 | In \f[I]batch mode\f[R], taskopen performs the first matching action on 161 | every actionable annotation. 162 | .TP 163 | any mode 164 | Taskopen 2.0 introduced an additional \f[I]any mode\f[R] similar to 165 | normal mode that presents the user with all matching actions to choose 166 | from instead of only the first matching action. 167 | .PP 168 | Note that earlier versions of taskopen also supported modes for editing 169 | annotations and deleting annotations. 170 | Since taskopen 2.0, these are implemented by actions. 171 | .SH OPTIONS 172 | .PP 173 | Options of taskopen are subdivided into four categories: output control, 174 | config overrides, includes/excludes, and filter control. 175 | .PP 176 | A special case for \f[V]--config\f[R] is when the config file does not 177 | exist. 178 | Taskopen will ask the user whether the config file with default values 179 | shall be created. 180 | .PP 181 | For some options, there exists a short variant (\f[V]-\f[R]) and a long 182 | variant (\f[V]--\f[R]). 183 | Provided values must be separated by a \f[V]:\f[R] or \f[V]=\f[R] when 184 | using the short variant. 185 | .SS Output control 186 | .TP 187 | \f[V]-v/--verbose\f[R] 188 | Prints additional info messages (e.g.\ the command line to be executed 189 | by taskopen). 190 | .TP 191 | \f[V]--debug\f[R] 192 | Prints debug messages (includes \f[V]-v\f[R]). 193 | .TP 194 | \f[V]-h/--help\f[R] 195 | Prints help message. 196 | .SS Config overrides 197 | .TP 198 | \f[V]-s=/--sort=key1+,key2-\f[R] 199 | Changes the default sort order of annotations. 200 | .TP 201 | \f[V]-c=/--config=filepath\f[R] 202 | Use a different config file. 203 | .TP 204 | \f[V]-a=/--active-tasks=filter\f[R] 205 | Changes the filter used by taskopen to determine active tasks. 206 | .TP 207 | \f[V]-x=/--execute=cmd\f[R] 208 | Overrides the command executed by taskopen for every action. 209 | .TP 210 | \f[V]-f=/--filter-command=cmd\f[R] 211 | Overrides filter command for every action. 212 | .TP 213 | \f[V]-i=/--inline-command=cmd\f[R] 214 | Overrides inline command for every action. 215 | .TP 216 | \f[V]--args=arguments\f[R] 217 | Allows definition of arguments that will be available as \f[V]$ARGS\f[R] 218 | in taskopen actions. 219 | .SS Includes/excludes 220 | .TP 221 | \f[V]--include=action1,action2\f[R] 222 | Only consider the listed actions. 223 | Also determines their priority. 224 | .TP 225 | \f[V]--exclude=action1,action2\f[R] 226 | Consider all but the listed actions. 227 | .SS Filter control 228 | .TP 229 | \f[V]-A/--All\f[R] 230 | Query all tasks, including completed and deleted tasks. 231 | .SH SUBCOMMANDS 232 | .PP 233 | The modes of taskopen are made accessible via subcommands. 234 | By default, taskopen operates in normal mode. 235 | In addition to the following subcommands, custom aliases can be defined 236 | in order to provide a short hand for common command line options. 237 | .TP 238 | \f[V]batch\f[R] 239 | Switches into batch mode. 240 | .TP 241 | \f[V]any\f[R] 242 | Switches into any mode. 243 | .TP 244 | \f[V]version\f[R] 245 | Prints version information. 246 | .TP 247 | \f[V]diagnostics\f[R] 248 | Prints diagnostics (e.g.\ configured actions, aliases, etc.) 249 | .SH MIGRATION FROM TASKOPEN 1.x 250 | .PP 251 | The following table compares command line arguments of taskopen 1.x with 252 | taskopen 2.0. 253 | Note that the \f[V]--include/--exclude\f[R] options require the 254 | definition of the appropriate actions in your config file. 255 | Moreover, you are able to define aliases for convenience (see 256 | \f[B]taskopenrc\f[R](5)). 257 | .PP 258 | .TS 259 | tab(@); 260 | lw(35.0n) lw(35.0n). 261 | T{ 262 | Taskopen 1.x 263 | T}@T{ 264 | Taskopen 2.0 265 | T} 266 | _ 267 | T{ 268 | \f[V]-h\f[R] 269 | T}@T{ 270 | \f[V]-h\f[R] or \f[V]--help\f[R] 271 | T} 272 | T{ 273 | \f[V]-v\f[R] 274 | T}@T{ 275 | \f[V]version\f[R] 276 | T} 277 | T{ 278 | \f[V]-V\f[R] 279 | T}@T{ 280 | \f[V]diagnostics\f[R] 281 | T} 282 | T{ 283 | \f[V]-l\f[R] 284 | T}@T{ 285 | \f[V]-x\f[R] or \f[V]--execute\f[R] 286 | T} 287 | T{ 288 | \f[V]-L\f[R] 289 | T}@T{ 290 | \f[V]-v\f[R] or \f[V]--verbose\f[R] 291 | T} 292 | T{ 293 | \f[V]-b\f[R] 294 | T}@T{ 295 | \f[V]batch\f[R] 296 | T} 297 | T{ 298 | \f[V]-n\f[R] 299 | T}@T{ 300 | \f[V]--include=notes\f[R] 301 | T} 302 | T{ 303 | \f[V]-N\f[R] 304 | T}@T{ 305 | \f[V]--exclude=notes\f[R] 306 | T} 307 | T{ 308 | \f[V]-f\f[R] 309 | T}@T{ 310 | \f[V]--include=files\f[R] 311 | T} 312 | T{ 313 | \f[V]-F\f[R] 314 | T}@T{ 315 | \f[V]--exclude=files\f[R] 316 | T} 317 | T{ 318 | \f[V]-B\f[R] 319 | T}@T{ 320 | \f[V]-f=\[aq]test ! -e $FILE\f[R] 321 | T} 322 | T{ 323 | \f[V]-t\f[R] 324 | T}@T{ 325 | \f[V]--include=text\f[R] 326 | T} 327 | T{ 328 | \f[V]-T\f[R] 329 | T}@T{ 330 | \f[V]--exclude=text\f[R] 331 | T} 332 | T{ 333 | \f[V]-a\f[R] 334 | T}@T{ 335 | \f[V]-a\f[R] 336 | T} 337 | T{ 338 | \f[V]-A\f[R] 339 | T}@T{ 340 | \f[V]-A\f[R] 341 | T} 342 | T{ 343 | \f[V]-D\f[R] 344 | T}@T{ 345 | \f[V]--include=delete\f[R] 346 | T} 347 | T{ 348 | \f[V]-r\f[R] 349 | T}@T{ 350 | \f[V]--include=raw\f[R] 351 | T} 352 | T{ 353 | \f[V]-m \[aq]regex\[aq]\f[R] 354 | T}@T{ 355 | \f[V]/regex/\f[R] 356 | T} 357 | T{ 358 | \f[V]--type \[aq]regex\[aq]\f[R] 359 | T}@T{ 360 | \f[V]-f=\[dq]file $FILE \[rs]| perl -ne \[aq]if($_ !\[ti] m/regex/){exit 1}\[aq]\[dq]\f[R] 361 | T} 362 | T{ 363 | \f[V]-s key1+,key2-\f[R] 364 | T}@T{ 365 | \f[V]-s=key1+,key2-\f[R] 366 | T} 367 | T{ 368 | \f[V]-e\f[R] 369 | T}@T{ 370 | \f[V]-x=\[aq]vim $FILE\[aq]\f[R] 371 | T} 372 | T{ 373 | \f[V]-x \[aq]cmd\[aq]\f[R] 374 | T}@T{ 375 | \f[V]-x=\[aq]cmd\[aq]\f[R] 376 | T} 377 | T{ 378 | \f[V]-i \[aq]cmd\[aq]\f[R] 379 | T}@T{ 380 | \f[V]-i=\[aq]cmd\[aq]\f[R] 381 | T} 382 | T{ 383 | \f[V]-c filepath\f[R] 384 | T}@T{ 385 | \f[V]-c=filepath\f[R] 386 | T} 387 | T{ 388 | \f[V]-p cmd\f[R] 389 | T}@T{ 390 | automatic detection 391 | T} 392 | .TE 393 | .SH FILES 394 | .TP 395 | \f[V]\[ti]/.taskopenrc\f[R] 396 | User configuration file - see also \f[B]taskopenrc\f[R](5). 397 | .TP 398 | \f[V]\[ti]/.config/taskopen/taskopenrc\f[R] 399 | Alternative location of user configuration file. 400 | Takes precedence over the locations listed above. 401 | .TP 402 | \f[V]\[ti]/$XDG_CONFIG_HOME/taskopen/taskopenrc\f[R] 403 | Alternative location of user configuration file. 404 | Takes precedence over the locations listed above. 405 | .TP 406 | \f[V]$TASKOPENRC\f[R] 407 | If set, the configuration file is loaded from the location specified by 408 | the environment variable \f[V]$TASKOPENRC\f[R]. 409 | .SH HISTORY 410 | .TP 411 | \f[B]2010 - 2012\f[R] 412 | The first release of taskopen was a quite simple bash script. 413 | .TP 414 | \f[B]early 2013\f[R] 415 | Re-implementation of taskopen in perl. 416 | .TP 417 | \f[B]early 2021\f[R] 418 | Re-implementation of taskopen in nim. 419 | .TP 420 | \f[B]mid 2022\f[R] 421 | Release of taskopen 2.0. 422 | .SH CREDITS & COPYRIGHTS 423 | .PP 424 | Copyright (C) 2010 - 2022, J. 425 | Schlatow 426 | .PP 427 | Taskopen is distributed under the GNU General Public License. 428 | See \f[I]http://www.opensource.org/licenses/gpl-2.0.php\f[R] for more 429 | information. 430 | .PP 431 | Please also refer to the \f[B]AUTHORS\f[R] file for a list of 432 | contributors. 433 | .SH SEE ALSO 434 | .PP 435 | \f[B]taskopenrc\f[R](5) 436 | .PP 437 | For more information regarding taskopen, see the following: 438 | .TP 439 | The official site at 440 | \f[I]\f[R] 441 | .TP 442 | The official code repository at 443 | \f[I]\f[R] 444 | .TP 445 | The wiki at 446 | \f[I]\f[R] 447 | .SH REPORTING BUGS 448 | .TP 449 | Bugs in taskopen may be reported to the issue-tracker at 450 | \f[I]\f[R] 451 | -------------------------------------------------------------------------------- /doc/man/taskopen.1.md: -------------------------------------------------------------------------------- 1 | % taskopen(1) Taskopen User Manual | Version 2.0 2 | 3 | # NAME 4 | 5 | taskopen - A companion application for taskwarrior that augments 6 | annotation handling. 7 | 8 | # SYNOPSIS 9 | 10 | `taskopen [subcommand] [options] [filter1 filter2 .. filterN]` 11 | 12 | # DESCRIPTION 13 | 14 | Any task in taskwarrior can have zero-or-more annotations. Originally, taskopen 15 | was created to extend the functionality of taskwarrior with respect to 16 | annotations. As taskwarrior lacks multi-line annotations, the idea was to 17 | _attach_ a notes file to a task and make it accessible by taskopen instead. In 18 | more general terms, taskopen is able to perform arbitrary actions on tasks by 19 | executing external commands. The actions applicable to a task are determined by 20 | filtering its annotations (or any other task attribute). 21 | 22 | In order to customise this, taskopen is configured with a set of filters, 23 | actions and operation modes, see **taskopenrc**(5). When executed, it passes the 24 | given filter to taskwarrior, and presents the user with a set of actions that 25 | it considers applicable to the returned tasks. 26 | 27 | ## Filters 28 | 29 | Taskwarrior has a large feature set for [filtering] on a task-level basis for 30 | generating the reports (task list). However, as taskopen may offer multiple 31 | (and different) actions for individual task attributes and annotations, it must 32 | perform additional filtering. 33 | 34 | In a first stage, the task filter is provided to taskopen on the command line 35 | and passed to taskwarrior to limit the set of considered tasks. In a second 36 | stage, taskopen checks what actions are applicable to these tasks (i.e. their 37 | attributes). 38 | 39 | By default, taskopen operates on the task annotations. As a generalisation, 40 | other attributes can be processed and filtered to determine the applicable 41 | actions. 42 | 43 | [filtering]: https://taskwarrior.org/docs/filter.html 44 | 45 | ### Annotations 46 | 47 | An annotation in taskwarrior is a single-line of arbitrary text with 48 | a timestamp. A task can have multiple of these annotations. For taskopen, the 49 | following format of annotations has been established: 50 | 51 | label: arbitrary text (e.g. url, file path) 52 | 53 | The label part is optional and used for further filtering (e.g. to distinguish 54 | different actions on the same annotation). Taskopen used the following perl 55 | regex for this: 56 | 57 | (?:(\S+):\s+)?(.*) 58 | 59 | The first match is called the _label part_ whereas the second match is the _file part_. 60 | Taskopen 2.0 allows filtering of both parts via regular expressions. 61 | 62 | ### Arbitrary attributes 63 | 64 | A task in taskwarrior has various attributes (e.g. project, tags, description). 65 | Taskwarrior even supports user-defined attributes (UDAs), which can for 66 | instance be used to link to an issue id of a bug tracker. It therefore appears 67 | natural to use taskopen also for defining actions on UDAs. In contrast to 68 | annotation filtering, there is only a single regular expression that is matched 69 | against the attribute's value. 70 | 71 | ## Actions 72 | 73 | An action defines what taskopen shall do with the filtered 74 | annotations/attributes. It is defined by four parameters: _target_, _regex_, 75 | _command_ and _modes_. The _target_ specifies for what task attribute the 76 | action is defined. The _regex_ defines the regular expression(s) used for 77 | determining valid annotations/attributes. The _command_ determines the command 78 | line that implements the actual action. The following placeholders for defining 79 | the command line exist: 80 | 81 | `$ANNOTATION` 82 | : the undecoded (raw) annotation text 83 | 84 | `$LABEL` 85 | : the label part of the annotation text 86 | 87 | `$FILE` 88 | : the file part of the annotation text 89 | 90 | `$UUID` 91 | : the UUID of the task 92 | 93 | `$ID` 94 | : the running ID of the task 95 | 96 | `$ENTRY` 97 | : the entry date-time of the annotation 98 | 99 | `$LAST_MATCH` 100 | : the last sub-pattern of the regular expression used for filtering 101 | 102 | `$TASK_*` 103 | : any task attribute (e.g. `$TASK_PROJECT`) or UDA 104 | 105 | `$ARGS` 106 | : user-defined arguments specified on the taskopen command line 107 | 108 | The _modes_ of an action is a list of valid modes in which the action is 109 | applicable (see next section). Optionally (for extensibility), 110 | a _filtercommand_ can be specified to implement additional filters such as file 111 | type checking. Furthermore, an _inlinecommand_ can be defined to execute 112 | a particular command for every actionable annotation and display its output 113 | interleaved with the list of actionable annotations. If an action targets 114 | annotations, a _labelregex_ may specify a regular expressions that is applied 115 | to the label part. For a detailed descripton of the configuration, please refer 116 | to **taskopenrc**(5). 117 | 118 | ## Operation modes 119 | 120 | Taskopen implements multiple modes of operation: 121 | 122 | normal mode 123 | : In _normal mode_, taskopen shows a list of actionable annotations and 124 | executes the first matching action for the annotation selected by the user. 125 | 126 | batch mode 127 | : In _batch mode_, taskopen performs the first matching action 128 | on every actionable annotation. 129 | 130 | any mode 131 | : Taskopen 2.0 introduced an additional _any mode_ similar to normal mode that 132 | presents the user with all matching actions to choose from instead of only the 133 | first matching action. 134 | 135 | Note that earlier versions of taskopen also supported modes for editing 136 | annotations and deleting annotations. Since taskopen 2.0, these are implemented 137 | by actions. 138 | 139 | 140 | # OPTIONS 141 | 142 | Options of taskopen are subdivided into four categories: output control, config 143 | overrides, includes/excludes, and filter control. 144 | 145 | A special case for `--config` is when the config file does not exist. Taskopen 146 | will ask the user whether the config file with default values shall be created. 147 | 148 | For some options, there exists a short variant (`-`) and a long variant (`--`). 149 | Provided values must be separated by a `:` or `=` when using the short variant. 150 | 151 | ## Output control 152 | 153 | `-v/--verbose` 154 | : Prints additional info messages (e.g. the command line to be executed by taskopen). 155 | 156 | `--debug` 157 | : Prints debug messages (includes `-v`). 158 | 159 | `-h/--help` 160 | : Prints help message. 161 | 162 | ## Config overrides 163 | 164 | `-s=/--sort=key1+,key2-` 165 | : Changes the default sort order of annotations. 166 | 167 | `-c=/--config=filepath` 168 | : Use a different config file. 169 | 170 | `-a=/--active-tasks=filter` 171 | : Changes the filter used by taskopen to determine active tasks. 172 | 173 | `-x=/--execute=cmd` 174 | : Overrides the command executed by taskopen for every action. 175 | 176 | `-f=/--filter-command=cmd` 177 | : Overrides filter command for every action. 178 | 179 | `-i=/--inline-command=cmd` 180 | : Overrides inline command for every action. 181 | 182 | `--args=arguments` 183 | : Allows definition of arguments that will be available as `$ARGS` in taskopen actions. 184 | 185 | ## Includes/excludes 186 | 187 | `--include=action1,action2` 188 | : Only consider the listed actions. Also determines their priority. 189 | 190 | `--exclude=action1,action2` 191 | : Consider all but the listed actions. 192 | 193 | ## Filter control 194 | 195 | `-A/--All` 196 | : Query all tasks, including completed and deleted tasks. 197 | 198 | 199 | # SUBCOMMANDS 200 | 201 | The modes of taskopen are made accessible via subcommands. By default, taskopen 202 | operates in normal mode. In addition to the following subcommands, custom aliases 203 | can be defined in order to provide a short hand for common command line options. 204 | 205 | `batch` 206 | : Switches into batch mode. 207 | 208 | `any` 209 | : Switches into any mode. 210 | 211 | `version` 212 | : Prints version information. 213 | 214 | `diagnostics` 215 | : Prints diagnostics (e.g. configured actions, aliases, etc.) 216 | 217 | 218 | # MIGRATION FROM TASKOPEN 1.x 219 | 220 | The following table compares command line arguments of taskopen 1.x with taskopen 2.0. 221 | Note that the `--include/--exclude` options require the definition of the appropriate actions 222 | in your config file. Moreover, you are able to define aliases for convenience 223 | (see **taskopenrc**(5)). 224 | 225 | 226 | | Taskopen 1.x | Taskopen 2.0 | 227 | | :--------------------------- | :--------------------------- | 228 | | `-h` | `-h` or `--help` | 229 | | `-v` | `version` | 230 | | `-V` | `diagnostics` | 231 | | `-l` | `-x` or `--execute` | 232 | | `-L` | `-v` or `--verbose` | 233 | | `-b` | `batch` | 234 | | `-n` | `--include=notes` | 235 | | `-N` | `--exclude=notes` | 236 | | `-f` | `--include=files` | 237 | | `-F` | `--exclude=files` | 238 | | `-B` | `-f='test ! -e $FILE` | 239 | | `-t` | `--include=text` | 240 | | `-T` | `--exclude=text` | 241 | | `-a` | `-a` | 242 | | `-A` | `-A` | 243 | | `-D` | `--include=delete` | 244 | | `-r` | `--include=raw` | 245 | | `-m 'regex'` | `/regex/` | 246 | | `--type 'regex'` | `-f="file $FILE \| perl -ne 'if($_ !~ m/regex/){exit 1}'"` | 247 | | `-s key1+,key2-` | `-s=key1+,key2-` | 248 | | `-e` | `-x='vim $FILE'` | 249 | | `-x 'cmd'` | `-x='cmd'` | 250 | | `-i 'cmd'` | `-i='cmd'` | 251 | | `-c filepath` | `-c=filepath` | 252 | | `-p cmd` | automatic detection | 253 | 254 | 255 | # FILES 256 | 257 | `~/.taskopenrc` 258 | 259 | : User configuration file - see also **taskopenrc**(5). 260 | 261 | `~/.config/taskopen/taskopenrc` 262 | 263 | : Alternative location of user configuration file. Takes precedence over the locations listed above. 264 | 265 | `~/$XDG_CONFIG_HOME/taskopen/taskopenrc` 266 | 267 | : Alternative location of user configuration file. Takes precedence over the locations listed above. 268 | 269 | `$TASKOPENRC` 270 | 271 | : If set, the configuration file is loaded from the location specified by the environment variable `$TASKOPENRC`. 272 | 273 | 274 | # HISTORY 275 | 276 | **2010 - 2012** 277 | 278 | : The first release of taskopen was a quite simple bash script. 279 | 280 | **early 2013** 281 | 282 | : Re-implementation of taskopen in perl. 283 | 284 | **early 2021** 285 | 286 | : Re-implementation of taskopen in nim. 287 | 288 | **mid 2022** 289 | 290 | : Release of taskopen 2.0. 291 | 292 | 293 | # CREDITS & COPYRIGHTS 294 | 295 | Copyright (C) 2010 - 2022, J. Schlatow 296 | 297 | Taskopen is distributed under the GNU General Public License. See 298 | *http://www.opensource.org/licenses/gpl-2.0.php* for more information. 299 | 300 | Please also refer to the **AUTHORS** file for a list of contributors. 301 | 302 | 303 | # SEE ALSO 304 | 305 | **taskopenrc**(5) 306 | 307 | For more information regarding taskopen, see the following: 308 | 309 | The official site at 310 | 311 | : *\* 312 | 313 | The official code repository at 314 | 315 | : *\* 316 | 317 | The wiki at 318 | 319 | : *\* 320 | 321 | 322 | # REPORTING BUGS 323 | 324 | Bugs in taskopen may be reported to the issue-tracker at 325 | 326 | : *\* 327 | -------------------------------------------------------------------------------- /doc/man/taskopenrc.5: -------------------------------------------------------------------------------- 1 | '\" t 2 | .\" Automatically generated by Pandoc 2.18 3 | .\" 4 | .\" Define V font for inline verbatim, using C font in formats 5 | .\" that render this, and otherwise B font. 6 | .ie "\f[CB]x\f[]"x" \{\ 7 | . ftr V B 8 | . ftr VI BI 9 | . ftr VB B 10 | . ftr VBI BI 11 | .\} 12 | .el \{\ 13 | . ftr V CR 14 | . ftr VI CI 15 | . ftr VB CB 16 | . ftr VBI CBI 17 | .\} 18 | .TH "taskopenrc" "5" "" "Taskopen User Manual" "Version 2.0" 19 | .hy 20 | .SH NAME 21 | .PP 22 | taskopenrc - Configuration file for the \f[B]taskopen\f[R](1) command 23 | .SH SYNOPSIS 24 | .PP 25 | \f[V]\[ti]/.taskopenrc\f[R] 26 | .PP 27 | \f[V]\[ti]/.config/taskopen/taskopenrc\f[R] 28 | .PP 29 | \f[V]\[ti]/$XDG_CONFIG_HOME/taskopen/taskopenrc\f[R] 30 | .PP 31 | \f[V]$TASKOPENRC\f[R] 32 | .PP 33 | \f[V]taskopen -c /path/to/taskopenrc\f[R] 34 | .SH DESCRIPTION 35 | .PP 36 | \f[B]taskopen\f[R] obtains its configuration data from a taskopenrc file 37 | from any of the locations above. 38 | .PP 39 | Since version 2.0, taskopen follows a \f[V].ini\f[R]-style configuration 40 | syntax with sections, e.g.: 41 | .IP 42 | .nf 43 | \f[C] 44 | [Section] 45 | key=value 46 | \f[R] 47 | .fi 48 | .PP 49 | The config file defines the settings and actions for taskopen. 50 | Undefined settings may be derived from environment variables 51 | (e.g.\ \f[V]$EDITOR\f[R]). 52 | Command line arguments always take precedence over the config file and 53 | environment variables. 54 | .PP 55 | The config file contains three sections specified below: General, 56 | Actions and CLI. 57 | .SS Section: General 58 | .PP 59 | This section defines general (default) settings of taskopen. 60 | The following keys are available in this section and where already 61 | present in the perl version of taskopen. 62 | .IP 63 | .nf 64 | \f[C] 65 | [General] 66 | EDITOR = vim 67 | path_ext = /usr/share/taskopen/scripts 68 | taskbin = task 69 | no_annotation_hook = addnote 70 | task_attributes = \[dq]priority,project,tags,description\[dq] 71 | \f[R] 72 | .fi 73 | .PP 74 | Note that the config file can be used to specify defaults for any 75 | command line option of taskopen, e.g.: 76 | .IP 77 | .nf 78 | \f[C] 79 | [General] 80 | --sort = \[dq]urgency-,label+,annot+\[dq] 81 | --active-tasks = \[dq]status.is:pending\[dq] 82 | --debug = 0 83 | \f[R] 84 | .fi 85 | .SS Section: Actions 86 | .PP 87 | This section specifies the actions as described \f[B]taskopen\f[R](1). 88 | An action consists of a name, a regex, a target, a command, valid modes 89 | and, optionally, a filter command, an inline command and a label regex. 90 | .PP 91 | In order to specify these attributes for different actions, taskopen 2.0 92 | uses \f[V].\f[R] in the config keys as separators. 93 | The first part then defines the name of the action, 94 | e.g.\ \f[V]name.attribute = value\f[R]. 95 | The default values are listed below. 96 | .IP 97 | .nf 98 | \f[C] 99 | .target = annotations 100 | .regex = \[dq].*\[dq] 101 | .labelregex = \[dq].*\[dq] 102 | .command = \[dq]\[dq] 103 | .modes = \[dq]normal,any,batch\[dq] 104 | .filtercommand = \[dq]\[dq] 105 | .inlinecommand = \[dq]\[dq] 106 | \f[R] 107 | .fi 108 | .PP 109 | Here is an example for specifying the default notes action from earlier 110 | taskopen versions: 111 | .IP 112 | .nf 113 | \f[C] 114 | [Actions] 115 | notes.regex = \[dq]Notes\[dq] 116 | notes.command = \[dq]$EDITOR $HOME/tasknotes/$UUID.txt\[dq] 117 | \f[R] 118 | .fi 119 | .PP 120 | Another example shows how to specify commands for editing and deleting 121 | annotations that reference non-existing files: 122 | .IP 123 | .nf 124 | \f[C] 125 | [Actions] 126 | edit.regex = \[dq].*\[dq] 127 | edit.command = \[dq]raw_edit $ANNOTATION\[dq] 128 | edit.filtercommand = \[dq]test ! -e $FILE\[dq] 129 | delete.regex = \[dq].*\[dq] 130 | delete.command = \[dq]task $UUID denotate -- \[rs]\[dq]$ANNOTATION\[rs]\[dq]\[dq] 131 | delete.filtercommand = \[dq]test ! -e $FILE\[dq] 132 | \f[R] 133 | .fi 134 | .PP 135 | Note that taskopen is sensitive to the order in which the actions are 136 | specified. 137 | This order determines the priority with which taskopen tries to apply 138 | the actions. 139 | This priority can be changed by using the \f[V]--include\f[R] option. 140 | .SS Section: CLI 141 | .PP 142 | This section can be used to define additional subcommands for the 143 | command line interface of taskopen. 144 | It also allows the definition action groups that simplify referencing 145 | multiple actions in the \f[V]--include\f[R] and \f[V]--exclude\f[R] 146 | options (see \f[B]taskopen\f[R](1)). 147 | .PP 148 | As an example, we can specify subcommands as aliases for simple access 149 | to the edit and delete actions via \f[V]taskopen edit [filter]\f[R] and 150 | \f[V]taskopen delete [filter]\f[R], or specify a subcommand 151 | \f[V]taskopen cleanup [filter]\f[R] that includes both: 152 | .IP 153 | .nf 154 | \f[C] 155 | [CLI] 156 | alias.edit = \[dq]normal --include=edit\[dq] 157 | alias.delete = \[dq]normal --include=delete\[dq] 158 | alias.cleanup = \[dq]any --include=edit,delete\[dq] 159 | \f[R] 160 | .fi 161 | .PP 162 | Regarding grouping, we can, e.g., define a group \f[I]cleanup\f[R] to 163 | combine the edit and delete action: 164 | .IP 165 | .nf 166 | \f[C] 167 | [CLI] 168 | group.cleanup = \[dq]edit,delete\[dq] 169 | \f[R] 170 | .fi 171 | .PP 172 | By doing this, we can type \f[V]taskopen --include=cleanup\f[R] instead 173 | of \f[V]taskopen --include=edit,delete\f[R]. 174 | .PP 175 | Moreover, the default subcommand can be changed: 176 | .IP 177 | .nf 178 | \f[C] 179 | [CLI] 180 | default = any 181 | \f[R] 182 | .fi 183 | .PP 184 | Note that aliases can be used as default subcommand. 185 | Yet, any \f[V]--config\f[R] within an aliases that is used as a default 186 | will be ignored. 187 | .SS Environment variables 188 | .PP 189 | Taskopen evaluates the following environment variables to determine 190 | default settings for some config keys. 191 | .IP \[bu] 2 192 | \f[V]$TASKOPENRC\f[R]: Overrides the default location 193 | (\f[V]$HOME/.taskopenrc\f[R]) of the config file. 194 | .SH MIGRATION 195 | .PP 196 | This sections lists examples of how old taskopen settings from before 197 | version 2.0 are converted into the new configuration format. 198 | Note that the general settings only need to be converted into lower 199 | case. 200 | .PP 201 | .TS 202 | tab(@); 203 | lw(34.4n) lw(35.6n). 204 | T{ 205 | Taskopen 1.x config variable 206 | T}@T{ 207 | Taskopen 2.0 config variable 208 | T} 209 | _ 210 | T{ 211 | \f[V]EDITOR\f[R] 212 | T}@T{ 213 | \f[V]EDITOR\f[R] 214 | T} 215 | T{ 216 | \f[V]TASK_BIN\f[R] 217 | T}@T{ 218 | \f[V]task_bin\f[R] 219 | T} 220 | T{ 221 | \f[V]PATH_EXT\f[R] 222 | T}@T{ 223 | \f[V]path_ext\f[R] 224 | T} 225 | T{ 226 | \f[V]DEBUG\f[R] 227 | T}@T{ 228 | \f[V]--debug\f[R] 229 | T} 230 | T{ 231 | \f[V]NO_ANNOTATION_HOOK\f[R] 232 | T}@T{ 233 | \f[V]no_annotation_hook\f[R] 234 | T} 235 | T{ 236 | \f[V]TASK_ATTRIBUTES\f[R] 237 | T}@T{ 238 | \f[V]task_attributes\f[R] 239 | T} 240 | T{ 241 | \f[V]DEFAULT_FILTER\f[R] 242 | T}@T{ 243 | \f[V]--active-tasks\f[R] 244 | T} 245 | T{ 246 | \f[V]DEFAULT_SORT\f[R] 247 | T}@T{ 248 | \f[V]--sort\f[R] 249 | T} 250 | T{ 251 | \f[V]BROWSER\f[R], \f[V]BROWSER_REGEX\f[R] 252 | T}@T{ 253 | defined in custom action 254 | T} 255 | T{ 256 | \f[V]FILE_CMD\f[R], \f[V]FILE_REGEX\f[R] 257 | T}@T{ 258 | defined in custom action 259 | T} 260 | T{ 261 | \f[V]NOTES_FOLDER\f[R], \f[V]NOTES_EXT\f[R], \f[V]NOTES_FILE\f[R], 262 | \f[V]NOTES_CMD\f[R], \f[V]NOTES_REGEX\f[R] 263 | T}@T{ 264 | defined in custom action 265 | T} 266 | T{ 267 | \f[V]TEXT_REGEX\f[R] 268 | T}@T{ 269 | defined in custom action 270 | T} 271 | T{ 272 | \f[V]CUSTOM[0-9]+_REGEX\f[R], \f[V]CUSTOM[0-9]+_CMD\f[R] 273 | T}@T{ 274 | defined in custom action 275 | T} 276 | T{ 277 | \f[V]DEFAULT-i\f[R] 278 | T}@T{ 279 | via action or alias 280 | T} 281 | T{ 282 | \f[V]DEFAULT-x\f[R] 283 | T}@T{ 284 | via action or alias 285 | T} 286 | .TE 287 | .SH CREDITS & COPYRIGHTS 288 | .PP 289 | Copyright (C) 2010 - 2022, J. 290 | Schlatow 291 | .PP 292 | Taskopen is distributed under the GNU General Public License. 293 | See \f[I]http://www.opensource.org/licenses/gpl-2.0.php\f[R] for more 294 | information. 295 | .PP 296 | Please also refer to the \f[B]AUTHORS\f[R] file for a list of 297 | contributors. 298 | .SH SEE ALSO 299 | .PP 300 | \f[B]taskopen\f[R](1) 301 | .PP 302 | For more information regarding taskopen, see the following: 303 | .TP 304 | The official site at 305 | \f[I]\f[R] 306 | .TP 307 | The official code repository at 308 | \f[I]\f[R] 309 | .TP 310 | The wiki at 311 | \f[I]\f[R] 312 | .SH REPORTING BUGS 313 | .TP 314 | Bugs in taskopen may be reported to the issue-tracker at 315 | \f[I]\f[R] 316 | -------------------------------------------------------------------------------- /doc/man/taskopenrc.5.md: -------------------------------------------------------------------------------- 1 | % taskopenrc(5) Taskopen User Manual | Version 2.0 2 | 3 | # NAME 4 | 5 | taskopenrc - Configuration file for the **taskopen**(1) command 6 | 7 | # SYNOPSIS 8 | 9 | `~/.taskopenrc` 10 | 11 | `~/.config/taskopen/taskopenrc` 12 | 13 | `~/$XDG_CONFIG_HOME/taskopen/taskopenrc` 14 | 15 | `$TASKOPENRC` 16 | 17 | `taskopen -c /path/to/taskopenrc` 18 | 19 | # DESCRIPTION 20 | 21 | **taskopen** obtains its configuration data from a taskopenrc file from any of 22 | the locations above. 23 | 24 | Since version 2.0, taskopen follows a `.ini`-style configuration syntax with 25 | sections, e.g.: 26 | 27 | ``` 28 | [Section] 29 | key=value 30 | ``` 31 | 32 | The config file defines the settings and actions for taskopen. Undefined 33 | settings may be derived from environment variables (e.g. `$EDITOR`). Command 34 | line arguments always take precedence over the config file and environment 35 | variables. 36 | 37 | The config file contains three sections specified below: General, Actions and 38 | CLI. 39 | 40 | 41 | ## Section: General 42 | 43 | This section defines general (default) settings of taskopen. The following keys 44 | are available in this section and where already present in the perl version of 45 | taskopen. 46 | 47 | ``` 48 | [General] 49 | EDITOR = vim 50 | path_ext = /usr/share/taskopen/scripts 51 | taskbin = task 52 | no_annotation_hook = addnote 53 | task_attributes = "priority,project,tags,description" 54 | ``` 55 | 56 | Note that the config file can be used to specify defaults for any command line 57 | option of taskopen, e.g.: 58 | 59 | ``` 60 | [General] 61 | --sort = "urgency-,label+,annot+" 62 | --active-tasks = "status.is:pending" 63 | --debug = 0 64 | ``` 65 | 66 | ## Section: Actions 67 | 68 | This section specifies the actions as described **taskopen**(1). An action 69 | consists of a name, a regex, a target, a command, valid modes and, optionally, 70 | a filter command, an inline command and a label regex. 71 | 72 | In order to specify these attributes for different actions, taskopen 2.0 uses 73 | `.` in the config keys as separators. The first part then defines the name of 74 | the action, e.g. `name.attribute = value`. The default values are listed below. 75 | 76 | ``` 77 | .target = annotations 78 | .regex = ".*" 79 | .labelregex = ".*" 80 | .command = "" 81 | .modes = "normal,any,batch" 82 | .filtercommand = "" 83 | .inlinecommand = "" 84 | ``` 85 | 86 | Here is an example for specifying the default notes action from earlier 87 | taskopen versions: 88 | 89 | ``` 90 | [Actions] 91 | notes.regex = "Notes" 92 | notes.command = "$EDITOR $HOME/tasknotes/$UUID.txt" 93 | ``` 94 | 95 | Another example shows how to specify commands for editing and deleting 96 | annotations that reference non-existing files: 97 | 98 | ``` 99 | [Actions] 100 | edit.regex = ".*" 101 | edit.command = "raw_edit $ANNOTATION" 102 | edit.filtercommand = "test ! -e $FILE" 103 | delete.regex = ".*" 104 | delete.command = "task $UUID denotate -- \"$ANNOTATION\"" 105 | delete.filtercommand = "test ! -e $FILE" 106 | ``` 107 | 108 | 109 | Note that taskopen is sensitive to the order in which the actions are specified. 110 | This order determines the priority with which taskopen tries to apply the actions. 111 | This priority can be changed by using the `--include` option. 112 | 113 | 114 | ## Section: CLI 115 | 116 | This section can be used to define additional subcommands for the command line 117 | interface of taskopen. It also allows the definition action groups that 118 | simplify referencing multiple actions in the `--include` and `--exclude` 119 | options (see **taskopen**(1)). 120 | 121 | As an example, we can specify subcommands as aliases for simple access to the 122 | edit and delete actions via `taskopen edit [filter]` and `taskopen delete 123 | [filter]`, or specify a subcommand `taskopen cleanup [filter]` that includes both: 124 | 125 | ``` 126 | [CLI] 127 | alias.edit = "normal --include=edit" 128 | alias.delete = "normal --include=delete" 129 | alias.cleanup = "any --include=edit,delete" 130 | ``` 131 | 132 | Regarding grouping, we can, e.g., define a group _cleanup_ to combine the edit 133 | and delete action: 134 | 135 | ``` 136 | [CLI] 137 | group.cleanup = "edit,delete" 138 | ``` 139 | 140 | By doing this, we can type `taskopen --include=cleanup` instead of `taskopen 141 | --include=edit,delete`. 142 | 143 | Moreover, the default subcommand can be changed: 144 | 145 | ``` 146 | [CLI] 147 | default = any 148 | ``` 149 | 150 | Note that aliases can be used as default subcommand. 151 | Yet, any `--config` within an aliases that is used as a default will be ignored. 152 | 153 | ## Environment variables 154 | 155 | Taskopen evaluates the following environment variables to determine default 156 | settings for some config keys. 157 | 158 | * `$TASKOPENRC`: Overrides the default location (`$HOME/.taskopenrc`) of the config file. 159 | 160 | # MIGRATION 161 | 162 | This sections lists examples of how old taskopen settings from before version 163 | 2.0 are converted into the new configuration format. Note that the general 164 | settings only need to be converted into lower case. 165 | 166 | | Taskopen 1.x config variable | Taskopen 2.0 config variable | 167 | | :--------------------------- | :----------------------------| 168 | | `EDITOR` | `EDITOR` | 169 | | `TASK_BIN` | `task_bin` | 170 | | `PATH_EXT` | `path_ext` | 171 | | `DEBUG` | `--debug` | 172 | | `NO_ANNOTATION_HOOK` | `no_annotation_hook` | 173 | | `TASK_ATTRIBUTES` | `task_attributes` | 174 | | `DEFAULT_FILTER` | `--active-tasks` | 175 | | `DEFAULT_SORT` | `--sort` | 176 | | `BROWSER`, `BROWSER_REGEX` | defined in custom action | 177 | | `FILE_CMD`, `FILE_REGEX` | defined in custom action | 178 | | `NOTES_FOLDER`, `NOTES_EXT`, `NOTES_FILE`, `NOTES_CMD`, `NOTES_REGEX` | defined in custom action | 179 | | `TEXT_REGEX` | defined in custom action | 180 | | `CUSTOM[0-9]+_REGEX`, `CUSTOM[0-9]+_CMD` | defined in custom action | 181 | | `DEFAULT-i` | via action or alias | 182 | | `DEFAULT-x` | via action or alias | 183 | 184 | 185 | # CREDITS & COPYRIGHTS 186 | 187 | Copyright (C) 2010 - 2022, J. Schlatow 188 | 189 | Taskopen is distributed under the GNU General Public License. See 190 | *http://www.opensource.org/licenses/gpl-2.0.php* for more information. 191 | 192 | Please also refer to the **AUTHORS** file for a list of contributors. 193 | 194 | # SEE ALSO 195 | 196 | **taskopen**(1) 197 | 198 | For more information regarding taskopen, see the following: 199 | 200 | The official site at 201 | 202 | : *\* 203 | 204 | The official code repository at 205 | 206 | : *\* 207 | 208 | The wiki at 209 | 210 | : *\* 211 | 212 | # REPORTING BUGS 213 | 214 | Bugs in taskopen may be reported to the issue-tracker at 215 | 216 | : *\* 217 | -------------------------------------------------------------------------------- /examples/1.x/taskreviewrc: -------------------------------------------------------------------------------- 1 | #- taskreviewrc - taskopen-review configuration file with instructions 2 | # 3 | # Intended as basis for a daily/ weekly/ project task review process. 4 | # Using UUID.review.txt files as review log files, and using their access/ modification times 5 | # to drive the review sequencing logic, as opposed to using review-specific date-stamps, UDAs, tags 6 | # or other taskwarrior attributes. 7 | # 8 | # Add an alias to taskrc: 9 | # alias.review=taskopen -c ~/.taskreviewrc -n 10 | # 11 | # Add a cronjob which executes: 12 | # 13 | # $ task (age.over:1wk AND annotation.not:Review) mod annotate Review 14 | # $ taskopen -c ~/.taskreviewrc -n -i '[ -e "$1" ] || touch "$1"' 15 | # 16 | # (Note that "-i" is used here as a variant of "-b -x" without user interaction.) 17 | # 18 | # Start a review, which can be restricted to a subset by using filters: 19 | # 20 | # $ task review project:taskopen 21 | 22 | # Reuse the NOTES feature 23 | NOTES_FOLDER="~/taskreview/" 24 | NOTES_EXT=".review.txt" 25 | NOTES_CMD="do_review.sh $FILE $UUID" 26 | NOTES_REGEX="Review" 27 | 28 | DEFAULT_SORT="priority-,atime-,mtime-" 29 | -------------------------------------------------------------------------------- /examples/default: -------------------------------------------------------------------------------- 1 | [General] 2 | no_annotation_hook="addnote $ID" 3 | # TODO adapt path_ext to your installation path 4 | #path_ext=/usr/share/taskopen/scripts 5 | 6 | [Actions] 7 | notes.regex = "^Notes\\.(.*)" 8 | notes.command = "editnote ~/Notes/tasknotes/$UUID.$LAST_MATCH \"$TASK_DESCRIPTION\" $UUID" 9 | 10 | files.regex = "^[\\.\\/~]+.*\\.(.*)" 11 | files.command = "xdg-open $FILE" 12 | files.filtercommand = "test -e $FILE" 13 | 14 | url.regex = "((?:www|http).*)" 15 | url.command = "xdg-open $LAST_MATCH 2>/dev/null" 16 | 17 | edit.regex = ".*" 18 | edit.command = "rawedit $UUID \"$ANNOTATION\"" 19 | delete.regex = ".*" 20 | delete.command = "task $UUID denotate -- \"$ANNOTATION\" 2>/dev/null" 21 | 22 | [CLI] 23 | default = default 24 | alias.default = "normal --exclude=edit,delete" 25 | 26 | alias.files = "normal --include=files" 27 | alias.notes = "normal --include=notes" 28 | alias.edit = "normal --include=edit" 29 | alias.delete = "normal --include=delete" 30 | alias.raw = "any --include=delete,edit" 31 | -------------------------------------------------------------------------------- /examples/markdown: -------------------------------------------------------------------------------- 1 | # EXAMPLE: markdown 2 | # 3 | # DESCRIPTION 4 | # This example requires pandoc installed. It defines three actions for 5 | # editing, compiling to PDF and viewing a compiled PDF file. 6 | # 7 | # SETUP 8 | # Copy all bu the General section to your config file. 9 | # Alternatively, you can test this configuration with a temporary data base, 10 | # by calling 11 | # export TASKOPENRC=/path/to/examples/markdown 12 | # taskopen ... 13 | # In order to operate on the temporary data base with taskwarrior, you may 14 | # define an alias like: 15 | # alias tx="task rc.data.location=/tmp" 16 | 17 | [General] 18 | # remove if used productively 19 | taskargs = "rc.data.location=/tmp" 20 | 21 | [Actions] 22 | 23 | # compile action: compile markdown file to pdf with pandoc 24 | mdcompile.regex = "^[\\.\\/~]+.*\\.(md|pandoc|mdwn|markdown)" 25 | mdcompile.command = "cd `dirname $FILE`; pandoc -o `basename ${FILE%.*}.pdf` $FILE" 26 | 27 | # edit action: open markdown file 28 | mdedit.regex = "^[\\.\\/~]+.*\\.(md|pandoc|mdwn|markdown)" 29 | mdedit.command = "$EDITOR $FILE" 30 | 31 | # view action: open compiled pdf 32 | mdview.regex = "^[\\.\\/~]+.*\\.(md|pandoc|mdwn|markdown)" 33 | mdview.command = "xdg-open ${FILE%.*}.pdf 2>/dev/null &" 34 | mdview.filtercommand = "test -e ${FILE%.*}.pdf" 35 | 36 | [CLI] 37 | alias.md = "any --include=mdcompile,mdedit,mdview" 38 | -------------------------------------------------------------------------------- /examples/templates: -------------------------------------------------------------------------------- 1 | # EXAMPLE: templates 2 | # 3 | # DESCRIPTION 4 | # Taskopen can be used to implement template-like tasks for taskwarrior. A 5 | # template task can be seen as a neat way to manage a whole set of tasks. The 6 | # idea is that a template task may be a recurring task like a montly "backup" 7 | # task which can be expanded into a chain of sub-tasks as soon as it pops up. 8 | # 9 | # SETUP 10 | # Copy all bu the General section to your config file. 11 | # Alternatively, you can test this configuration with a temporary data base, 12 | # by calling 13 | # export TASKOPENRC=/path/to/examples/markdown 14 | # taskopen ... 15 | # In order to operate on the temporary data base with taskwarrior, you may 16 | # define an alias like: 17 | # alias tx="task rc.data.location=/tmp" 18 | # 19 | # USAGE 20 | # Create a .tmpl file and make it executable, e.g. ~/taskopen/backup.tmpl: 21 | # #!/bin/bash 22 | # task del $1 # delete the "parent" task 23 | # task add backup VM # add sub-task 1 24 | # task add backup laptop # add sub-task 2 25 | # task add backup dropbox # add sub-task 3 26 | # 27 | # Add an annotation like this: 28 | # $ task 1 annotate -- ~/taskopen/backup.tmpl 29 | # 30 | # Use taskopen to expand the task (which executes backup.tmpl): 31 | # $ taskopen expand 1 32 | 33 | [General] 34 | # remove if used productively 35 | taskargs = "rc.data.location=/tmp" 36 | 37 | [Actions] 38 | 39 | expand.regex = ".*\\.tmpl" 40 | expand.command = "sh $FILE $UUID" 41 | 42 | [CLI] 43 | alias.expand = "normal --include=expand" 44 | -------------------------------------------------------------------------------- /examples/utility_task: -------------------------------------------------------------------------------- 1 | # EXAMPLE: utility tasks 2 | # 3 | # DESCRIPTION 4 | # Taskopen can be used to execute arbitrary commands that exist as annotations 5 | # to taskwarrior tasks. A utility task is one that carries a "payload" of one 6 | # or more commands, and is intended for a specific function. 7 | # (This is an alternative approach to the 'templates' example.) 8 | # 9 | # SETUP 10 | # Copy all bu the General section to your config file. 11 | # Alternatively, you can test this configuration with a temporary data base, 12 | # by calling 13 | # export TASKOPENRC=/path/to/examples/markdown 14 | # taskopen ... 15 | # In order to operate on the temporary data base with taskwarrior, you may 16 | # define an alias like: 17 | # alias tx="task rc.data.location=/tmp" 18 | # 19 | # USAGE 20 | # Add a new task and annotate with labeled commands: 21 | # task add backup files due:eow 22 | # task 102 annotate -- backup_home_directory: ~/bin/mybackup_app -f home 23 | # task 102 annot -- backup_etc_directory: /usr/bin/unison -opts /etc 24 | # task 102 ann -- backup_var_directory: /usr/bin/rsync -opts /var 25 | # 26 | # Then, when you want to back up your files, run taskopen 27 | # 28 | # $ taskopen backup -x 29 | # 30 | # choose the desired actions(s) from the list and voila! 31 | # (just don't use the made-up commands used as examples) 32 | ~ 33 | 34 | [General] 35 | # remove if used productively 36 | taskargs = "rc.data.location=/tmp" 37 | 38 | [Actions] 39 | 40 | run.regex = ".*" 41 | run.command = "sh $FILE" 42 | 43 | [CLI] 44 | alias.run = "normal --include=run" 45 | -------------------------------------------------------------------------------- /scripts/1.x/do_review.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ $# != 2 ]; then 4 | echo "Usage: $0 " 5 | exit 1 6 | fi 7 | 8 | task $2 info 9 | 10 | echo -n "Continue review? (Y/n): " 11 | read -n 1 input 12 | if [ "$input" == "y" ]; then 13 | $EDITOR $1 14 | else 15 | echo $input 16 | fi 17 | -------------------------------------------------------------------------------- /scripts/1.x/mess2task: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ############################################################################### 4 | # taskopen - file based notes with taskwarrior 5 | # 6 | # Copyright 2010, Johannes Schlatow, Jostein Berntsen. 7 | # All rights reserved. 8 | # 9 | # This program is free software; you can redistribute it and/or modify it under 10 | # the terms of the GNU General Public License as published by the Free Software 11 | # Foundation; either version 2 of the License, or (at your option) any later 12 | # version. 13 | # 14 | # This program is distributed in the hope that it will be useful, but WITHOUT 15 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 16 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 17 | # details. 18 | # 19 | # You should have received a copy of the GNU General Public License along with 20 | # this program; if not, write to the 21 | # 22 | # Free Software Foundation, Inc., 23 | # 51 Franklin Street, Fifth Floor, 24 | # Boston, MA 25 | # 02110-1301 26 | # USA 27 | # 28 | ############################################################################### 29 | 30 | task `task rc._forcecolor=no newest | head -4 | tail -1 | awk '{print $1}'` ann $(egrep -A1 -i '^Message-ID|^Message-id' $* | paste -s |sed 's/[ \t][ \t]*//g' | sed -e 's/^\(M.*>\)[A-Z].*/\1/g') 31 | 32 | -------------------------------------------------------------------------------- /scripts/1.x/mess2task2: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ############################################################################### 4 | # taskopen - file based notes with taskwarrior 5 | # 6 | # Copyright 2010, Johannes Schlatow, Jostein Berntsen. 7 | # All rights reserved. 8 | # 9 | # This program is free software; you can redistribute it and/or modify it under 10 | # the terms of the GNU General Public License as published by the Free Software 11 | # Foundation; either version 2 of the License, or (at your option) any later 12 | # version. 13 | # 14 | # This program is distributed in the hope that it will be useful, but WITHOUT 15 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 16 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 17 | # details. 18 | # 19 | # You should have received a copy of the GNU General Public License along with 20 | # this program; if not, write to the 21 | # 22 | # Free Software Foundation, Inc., 23 | # 51 Franklin Street, Fifth Floor, 24 | # Boston, MA 25 | # 02110-1301 26 | # USA 27 | # 28 | ############################################################################### 29 | 30 | egrep -A1 -i '^Message-ID|^Message-id' $* | paste -s |sed 's/[ \t][ \t]*//g' | sed -e 's/^\(M.*>\)[A-Z].*/\1/g' | clippy 31 | 32 | -------------------------------------------------------------------------------- /scripts/1.x/mutt2task: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ############################################################################### 4 | # taskopen - file based notes with taskwarrior 5 | # 6 | # Copyright 2010, Johannes Schlatow, Jostein Berntsen. 7 | # All rights reserved. 8 | # 9 | # This program is free software; you can redistribute it and/or modify it under 10 | # the terms of the GNU General Public License as published by the Free Software 11 | # Foundation; either version 2 of the License, or (at your option) any later 12 | # version. 13 | # 14 | # This program is distributed in the hope that it will be useful, but WITHOUT 15 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 16 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 17 | # details. 18 | # 19 | # You should have received a copy of the GNU General Public License along with 20 | # this program; if not, write to the 21 | # 22 | # Free Software Foundation, Inc., 23 | # 51 Franklin Street, Fifth Floor, 24 | # Boston, MA 25 | # 02110-1301 26 | # USA 27 | # 28 | ############################################################################### 29 | 30 | /usr/local/bin/task add +email E-mail: $(egrep '^Subject|^From' $* | awk -F: '{print $2}' | sed 's/\(.*>\)\(.*\)/\1 - \2/g') 31 | 32 | -------------------------------------------------------------------------------- /scripts/addnote: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Annotates a task with 'Notes' or 'Notes.*'. 3 | 4 | # REQUIRES: awk 5 | 6 | # INSTALLATION 7 | # Add this to your ~/.taskrc: 8 | # alias.addnote=execute /path/to/addnote 9 | 10 | # Only add an annotation if we are operating on a single ID 11 | if [ $# -eq 1 ]; then 12 | desc=$(task $1 info | awk '$0 ~ /^Description/ {print substr($0, index($0,$2))}') 13 | echo "Attaching to task $1 '$desc'." 14 | echo -n "Type a file extension (or none): " 15 | read ext 16 | ann="" 17 | if [ "$ext" != "" ]; then 18 | ann="Notes.$ext" 19 | else 20 | ann="Notes" 21 | fi 22 | task $1 annotate -- $ann 23 | fi 24 | -------------------------------------------------------------------------------- /scripts/archive: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script is supposed to be used with an archive action such as the following: 4 | # archfiles.regex = "/home/user/((?:Documents|Notes)/.*)" 5 | # archfiles.command = "archive $FILE ~/archive/$LAST_MATCH" 6 | # archfiles.filtercommand = "archive $FILE" 7 | 8 | if [ $# -lt 1 ]; then 9 | echo "Usage: $0 " 10 | exit 1 11 | fi 12 | 13 | # only test if path is ready to be archived 14 | SRCFILE=$1 15 | 16 | # taskwarrior does not support search patterns with '/'s 17 | # as a workaround, we replace the '/'s by a regex '.' 18 | SEARCH=${SRCFILE//\//.} 19 | 20 | if [ $# -eq 1 ]; then 21 | res=$(task _ids ${SEARCH} | wc -l) 22 | if [ $res -eq 1 ]; then 23 | echo "okay" 24 | exit 0 25 | else 26 | echo "Not archivable" 27 | exit 1 28 | fi 29 | else 30 | DSTFILE=$2 31 | DSTPATH=$(dirname $2) 32 | 33 | mkdir -p ${DSTPATH} 34 | mv -i ${SRCFILE} ${DSTFILE} 35 | 36 | ids=$(task uuids ${SEARCH}) 37 | 38 | task $ids denotate -- ${SRCFILE} && task $ids annotate -- ${DSTFILE} 39 | fi 40 | -------------------------------------------------------------------------------- /scripts/attach: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Attaches a file to a task by using ranger as file picker. 3 | 4 | # REQUIRES: ranger, awk 5 | 6 | # INSTALLATION 7 | # Add this to your ~/.taskrc: 8 | # alias.attach=execute /path/to/attach 9 | 10 | # Only add an annotation if we are operating on a single ID 11 | if [ $# -eq 1 ]; then 12 | desc=$(task $1 info | awk '$0 ~ /^Description/ {print substr($0, index($0,$2))}') 13 | echo "Attaching to task $1 '$desc'." 14 | filename=$(mktemp) 15 | echo -n "Type a label: " 16 | read label 17 | ann="" 18 | if [ "$label" != "" ]; then 19 | ann="$label: " 20 | fi 21 | #ranger --choosefile=$filename --selectfile=/path/to/start-point 22 | ranger --choosefile=$filename 23 | ann="$ann$(cat $filename)" 24 | task $1 annotate -- $ann 25 | rm $filename 26 | fi 27 | -------------------------------------------------------------------------------- /scripts/editnote: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Opens a Notes file and creates a header if file does not exist. 3 | 4 | # Usage 5 | if [ $# -eq 3 ]; then 6 | if [ ! -e $1 ]; then 7 | echo "* [ ] $2 #$3" > $1 8 | fi 9 | $EDITOR $1 10 | else 11 | echo "Usage: $0 " 12 | fi 13 | -------------------------------------------------------------------------------- /scripts/rawedit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w -CLASo 2 | 3 | 4 | # Edit the raw annotation text. 5 | # Usage: rawedit "annotation text" 6 | 7 | # Use with the following lines in taskopen's config file: 8 | # [Actions] 9 | # edit.regex = ".*" 10 | # edit.command = "rawedit $UUID \"$ANNOTATION\"" 11 | # 12 | # [CLI] 13 | # alias.edit = "normal --include=edit" 14 | 15 | use File::Temp; 16 | 17 | if ($#ARGV != 1) { 18 | print("Usage: rawedit \"annotation text\""); 19 | exit 1 20 | } 21 | 22 | my $EDITOR = $ENV{"EDITOR"}; 23 | 24 | my $id = $ARGV[0]; 25 | my $text = $ARGV[1]; 26 | 27 | my $new = raw_edit($text); 28 | modify_annotation($id, $text, $new); 29 | 30 | sub raw_edit { 31 | my $old = $_[0]; 32 | 33 | my $filename = File::Temp::tmpnam(); 34 | open(my $fh, '>', $filename) or die "can't open $filename: $!"; 35 | print($fh $old); 36 | close($fh); 37 | 38 | system(qq/$EDITOR "$filename"/); 39 | 40 | open($fh, '<', $filename) or die "can't open $filename: $!'"; 41 | my @lines = <$fh>; 42 | close($fh); 43 | unlink($filename); 44 | 45 | # taskwarrior does not support multi-line annotations 46 | # TODO fix if #1172 has been solved 47 | 48 | my $result = $lines[0]; 49 | chomp($result); 50 | return $result; 51 | } 52 | 53 | sub modify_annotation { 54 | my $id = $_[0]; 55 | my $old = $_[1]; 56 | my $new = $_[2]; 57 | 58 | if ($old ne $new) { 59 | # TODO remove as soon as tw bug TW-1821 has been fixed ('/'s must still be escaped) 60 | if ($old =~ m/\// || $new =~ m/\//) { 61 | print("Cannot replace annotations which contain '/'s (see #1174)."); 62 | exit 1 63 | } 64 | # END REMOVE 65 | `task $id mod /$old/$new/ > /dev/null`; 66 | } 67 | else { 68 | print("No changes detected"); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /scripts/taskban: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This is a wrapper script for taskwarrior and taskopen for kanban-style actions. 4 | 5 | # Taskwarrior prerequisites/UDA config: 6 | # uda.state.type=string 7 | # uda.state.label=State 8 | # uda.state.values=wip,rdy,blg,done 9 | # uda.state.default=rdy 10 | 11 | # Taskopen prerequiites: 12 | # - define a delete action (see examples/default) 13 | # - define an archive action (see scripts/archive) 14 | 15 | DEFAULT_REPORT=() 16 | SUPPRESS_OUT="rc.verbose=nothing" 17 | 18 | if [ $# -lt 1 ]; then 19 | echo "Usage: $0 (add|backlog|delete|archive|proceed|retreat|open|yield|unlink) [|]" 20 | echo " or: $0 (sched|link) (|)" 21 | task ${DEFAULT_REPORT[@]} 22 | exit 1 23 | fi 24 | 25 | case "$1" in 26 | ad|add) 27 | task add ${@:2} 28 | task ${DEFAULT_REPORT[@]} 29 | ;; 30 | ar|arc|arch|archi|archiv|archive) 31 | taskopen archive ${@:2} 32 | task done ${@:2} 33 | task ${DEFAULT_REPORT[@]} 34 | ;; 35 | b|ba|bac|back|backl|backlo|backlog) 36 | task add state:blg ${@:2} 37 | task ${DEFAULT_REPORT[@]} 38 | ;; 39 | d|de|del|dele|delet|delete) 40 | task del ${@:2} 41 | task ${DEFAULT_REPORT[@]} 42 | ;; 43 | p|pr|pro|proc|proce|procee|proceed) 44 | task ${SUPPRESS_OUT} ${@:2} state:wip mod state:done 45 | task ${SUPPRESS_OUT} ${@:2} state:rdy mod state:wip 46 | task ${SUPPRESS_OUT} ${@:2} state:blg mod state:rdy 47 | task ${DEFAULT_REPORT[@]} 48 | ;; 49 | r|re|ret|retr|retre|retrea|retreat) 50 | task ${SUPPRESS_OUT} ${@:2} state:rdy mod state:blg 51 | task ${SUPPRESS_OUT} ${@:2} state:wip mod state:rdy 52 | task ${SUPPRESS_OUT} ${@:2} state:done mod state:wip 53 | task ${DEFAULT_REPORT[@]} 54 | ;; 55 | s|sc|sch|sche|sched) 56 | # schedule to value of last argument 57 | task ${@:2:$(($#-2))} mod sched:${@:$#} 58 | task ${DEFAULT_REPORT[@]} 59 | ;; 60 | y|yi|yie|yiel|yield) 61 | # unschedule 62 | task ${@:2} mod sched: 63 | task ${DEFAULT_REPORT[@]} 64 | ;; 65 | o|op|ope|open) 66 | taskopen ${@:2} 67 | ;; 68 | l|li|lin|link) 69 | # annotate with value of last argument 70 | task ${@:2:$(($#-2))} annotate -- ${@:$#} 71 | ;; 72 | u|un|unl|unli|unlin|unlink) 73 | taskopen delete ${@:2} 74 | ;; 75 | *) 76 | # default: pass everything to taskopen 77 | taskopen $* 78 | ;; 79 | esac 80 | -------------------------------------------------------------------------------- /scripts/users/artur-shaik/attach_vifm: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script attaches file to taskwarrior's task, 4 | # that can be open with taskopen script. 5 | # 6 | # Also, script can create task before attaching file 7 | # to it. 8 | # 9 | # Requires: taskwarrior, taskopen, vifm, getopt 10 | # 11 | # For parameters run: attach_to_task --help 12 | # 13 | # Commands can be added to vifmrc: 14 | # command attachnew attach_to_task -f %d/%f 15 | # command attach attach_to_task -t %a -f %d/%f 16 | 17 | usage() { 18 | echo "Usage: $(basename $0) [-t task_id] [-f filepath]" 19 | } 20 | 21 | SHORT=t:f:h 22 | LONG=task:,file:,help 23 | 24 | PARSED=$(getopt --options $SHORT --longoptions $LONG --name "$0" -- "$@") 25 | if [[ $? != 0 ]]; then 26 | exit 2 27 | fi 28 | 29 | eval set -- "$PARSED" 30 | 31 | while true; do 32 | case "$1" in 33 | -t|--task) 34 | task_id="$2" 35 | number='^[0-9]+' 36 | if ! [[ $task_id =~ $number ]]; then 37 | echo "Wrong task id" 38 | exit 1 39 | fi 40 | shift 2 41 | ;; 42 | -f|--file) 43 | file_name=$(readlink -f "$2") 44 | if ! [[ -f $file_name ]]; then 45 | echo "Not a file" 46 | exit 1 47 | fi 48 | shift 2 49 | ;; 50 | -h|--help) 51 | usage 52 | exit 53 | ;; 54 | --) 55 | shift 56 | break 57 | ;; 58 | esac 59 | done 60 | 61 | if [[ "$task_id" == "" ]]; then 62 | echo -n "Create task (with options): " 63 | read -r task_title 64 | if [[ "$task_title" == "" ]]; then 65 | echo "Cancel" 66 | exit 67 | fi 68 | task_id=$(task add $task_title | grep 'Created' | sed 's/Created task \([0-9]\+\)/\1/g') 69 | if [[ $? -ne 0 || "$task_id" == "" ]]; then 70 | echo "Error creating task" 71 | exit 1 72 | fi 73 | fi 74 | 75 | if [[ "$file_name" == "" ]]; then 76 | if [[ ! -x $(which vifm) ]]; then 77 | echo "Please, install vifm" 78 | exit 1 79 | fi 80 | file_name=$(vifm -c only --choose-files -) 81 | if [[ "$file_name" == "" ]]; then 82 | echo "Cancel" 83 | exit 84 | fi 85 | fi 86 | 87 | desc=$(task $task_id info | awk '$0 ~ /^Description/ {print substr($0, index($0,$2))}') 88 | echo "Attaching to task $task_id '$desc'." 89 | echo -n "Type a label: " 90 | read -r label 91 | annotation="" 92 | if [[ "$label" != "" ]]; then 93 | annotation="$label: " 94 | fi 95 | 96 | task $task_id annotate -- "$annotation$file_name" 97 | -------------------------------------------------------------------------------- /scripts/vims: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Open given file(s) in a remote vim server (or start a new one) 4 | # depending on user input. 5 | 6 | SERVERS=($(vim --serverlist)) 7 | 8 | echo "Exising servers:" 9 | for i in ${!SERVERS[@]}; do 10 | printf " %s) %s\n" "$(($i+1))" "${SERVERS[$i]}" 11 | done 12 | echo -ne "\nPlease type a number or a new name: " 13 | 14 | read choice 15 | 16 | if [ -z "$choice" ]; then 17 | vim $* 18 | elif [ -z "${choice##[0-9]}" ]; then 19 | i=$(($choice-1)) 20 | vim --servername "${SERVERS[$i]}" --remote-tab $* 21 | else 22 | vim --servername "$choice" $* 23 | fi 24 | -------------------------------------------------------------------------------- /scripts/xdg-open-cli: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # use this script to overwrite xdg-open defaults with cli-based applications 3 | 4 | if [ ! $# -eq 1 ]; then 5 | echo "Usage: $0 " 6 | exit 1 7 | fi 8 | 9 | # get MIME type 10 | MIME=$(xdg-mime query filetype $1) 11 | 12 | # use $EDITOR for opening text files 13 | if [[ "$MIME" =~ ^text ]]; then 14 | $EDITOR $1 15 | else 16 | xdg-open $1 17 | fi 18 | -------------------------------------------------------------------------------- /src/config.nim: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2021-2022 Johannes Schlatow 3 | # 4 | # This file is part of taskopen, which is distributed under the terms of the 5 | # GNU General Public License version 2. 6 | # 7 | 8 | import tables 9 | import os 10 | import parsecfg 11 | import streams 12 | import re 13 | import strutils 14 | import ./types 15 | import ./output 16 | 17 | # compile time defaults depending on target system 18 | const EDITOR {.strdefine.}: string = "vim" 19 | const OPEN {.strdefine.}: string = "xdg-open" 20 | const PATH_EXT {.strdefine.}: string = "" 21 | 22 | type 23 | Settings* = object 24 | command*: string 25 | sort*: string 26 | basefilter*: string 27 | filter*: seq[string] 28 | args*: string 29 | all*: bool 30 | validSubcommands*: Table[string, string] 31 | defaultSubcommand*: string 32 | validActions*: OrderedTable[string, Action] 33 | actions*: seq[string] 34 | actionGroups*: Table[string, string] 35 | restrictActions*: bool 36 | inlineCommand*: string 37 | filterCommand*: string 38 | forceCommand*: string 39 | editor*: string 40 | pathExt*: string 41 | taskbin*: string 42 | taskargs*: seq[string] 43 | noAnnot*: string 44 | taskAttributes*: string 45 | debug*: bool 46 | configfile*: string 47 | unparsedOptions*: seq[string] 48 | 49 | proc findConfig*(): string = 50 | ## find config file using $TASKOPENRC, $XDG_CONFIG_HOME/taskopen/taskopenrc, $HOME/.config/taskopen/taskopenrc or ~/.taskopenrc 51 | 52 | result = getHomeDir() / ".taskopenrc" 53 | if getEnv("TASKOPENRC") != "": 54 | result = getEnv("TASKOPENRC") 55 | else: 56 | let path = getConfigDir() / "taskopen" / "taskopenrc" 57 | if os.fileExists(path): 58 | result = path 59 | 60 | proc `[]=` (a: var Action, key, val: string) = 61 | case key 62 | of "target": 63 | a.target = val 64 | of "regex": 65 | a.regex = val 66 | of "labelregex": 67 | a.labelregex = val 68 | of "command": 69 | a.command = val 70 | of "modes": 71 | a.modes = val.split(',') 72 | of "filtercommand": 73 | a.filtercommand = val 74 | of "inlinecommand": 75 | a.inlinecommand = val 76 | 77 | 78 | proc parseFile(filepath: string, settings: var Settings) = 79 | var f = newFileStream(filepath, fmRead) 80 | if f != nil: 81 | var p: CfgParser 82 | var section: string 83 | open(p, f, filepath) 84 | while true: 85 | var e = next(p) 86 | case e.kind 87 | of cfgEof: break 88 | of cfgSectionStart: ## a ``[section]`` has been parsed 89 | section = e.section 90 | of cfgKeyValuePair: 91 | case section 92 | of "General": 93 | case e.key 94 | of "EDITOR": 95 | settings.editor = e.value 96 | of "taskbin": 97 | settings.taskbin = e.value 98 | of "taskargs": 99 | settings.taskargs = e.value.split(' ') 100 | of "path_ext": 101 | settings.pathExt = e.value 102 | of "task_attributes": 103 | settings.taskAttributes = e.value 104 | of "no_annotation_hook": 105 | settings.noAnnot = e.value 106 | else: 107 | warn.log("Invalid config option in [General]: ", e.key, ": ", e.value) 108 | 109 | of "Actions": 110 | if e.key =~ re"(.*)\.(target|regex|labelregex|command|modes|filtercommand|inlinecommand)": 111 | let actname = matches[0] 112 | let actfield = matches[1] 113 | if not settings.validActions.hasKey(actname): 114 | settings.validActions[actname] = Action(name: actname, 115 | target: "annotations", 116 | modes: @["batch", "any", "normal"], 117 | labelregex: ".*", 118 | regex: ".*") 119 | 120 | settings.validActions[actname][actfield] = e.value 121 | else: 122 | warn.log("Invalid config option in [Actions]: ", e.key, ": ", e.value) 123 | 124 | of "CLI": 125 | if e.key == "default": 126 | settings.defaultSubcommand = e.value 127 | elif e.key =~ re"(alias|group)\.([^\.]*)": 128 | let name = matches[1] 129 | case matches[0] 130 | of "alias": 131 | settings.validSubcommands[name] = e.value 132 | of "group": 133 | settings.actionGroups[name] = e.value 134 | else: 135 | warn.log("Invalid config option in [CLI]: ", e.key, ": ", e.value) 136 | of cfgOption: 137 | if section == "General": 138 | if e.key == "debug": ## handle --debug to switch output level early 139 | output.level = debug 140 | settings.debug = true 141 | else: 142 | settings.unparsedOptions.add("--" & e.key & "=" & e.value) 143 | of cfgError: 144 | error.log(e.msg) 145 | close(p) 146 | else: 147 | error.log("cannot open: ", filepath) 148 | 149 | 150 | proc createConfig*(filepath: string, defaults = Settings()) = 151 | var dict=newConfig() 152 | 153 | dict.setSectionKey("General", "taskbin", defaults.taskbin) 154 | dict.setSectionKey("General", "taskargs", defaults.taskargs.join("")) 155 | dict.setSectionKey("General", "no_annotation_hook", defaults.noAnnot) 156 | dict.setSectionKey("General", "task_attributes", defaults.taskAttributes) 157 | dict.setSectionKey("General", "--sort", defaults.sort) 158 | dict.setSectionKey("General", "--active-tasks", defaults.basefilter) 159 | 160 | if defaults.editor != "": 161 | dict.setSectionKey("General", "EDITOR", defaults.editor) 162 | 163 | if defaults.pathExt != "": 164 | dict.setSectionKey("General", "path_ext", defaults.pathExt) 165 | 166 | if defaults.args != "": 167 | dict.setSectionKey("General", "--args", defaults.args) 168 | 169 | if defaults.all: 170 | dict.setSectionKey("General", "--All", "on") 171 | 172 | if defaults.inlineCommand != "": 173 | dict.setSectionKey("General", "--inline-command", defaults.inlineCommand) 174 | 175 | if defaults.filterCommand != "": 176 | dict.setSectionKey("General", "--filter-command", defaults.filterCommand) 177 | 178 | if defaults.forceCommand != "": 179 | dict.setSectionKey("General", "--execute", defaults.forceCommand) 180 | 181 | if defaults.debug: 182 | dict.setSectionKey("General", "--debug", "on") 183 | 184 | for a in defaults.validActions.keys(): 185 | dict.setSectionKey("Actions", a & ".target", defaults.validActions[a].target) 186 | dict.setSectionKey("Actions", a & ".labelregex", defaults.validActions[a].labelregex) 187 | dict.setSectionKey("Actions", a & ".regex", defaults.validActions[a].regex) 188 | dict.setSectionKey("Actions", a & ".command", defaults.validActions[a].command) 189 | dict.setSectionKey("Actions", a & ".modes", defaults.validActions[a].modes.join(",")) 190 | 191 | dict.writeConfig(filepath) 192 | 193 | 194 | proc parseConfig*(filepath: string): Settings = 195 | # set hardcoded defaults 196 | result.sort = "urgency-,annot" 197 | result.validSubcommands["batch"] = "" 198 | result.validSubcommands["any"] = "" 199 | result.validSubcommands["normal"] = "" 200 | result.validSubcommands["version"] = "" 201 | result.validSubcommands["diagnostics"] = "" 202 | result.defaultSubcommand = "normal" 203 | result.basefilter = "+PENDING" 204 | result.taskAttributes = "priority,project,tags,description" 205 | result.noAnnot = "addnote $ID" 206 | result.configfile = filepath 207 | result.validActions["files"] = Action( 208 | name: "files", 209 | target: "annotations", 210 | labelregex: ".*", 211 | regex: "^[\\.\\/~]+.*\\.(.*)", 212 | modes: @["batch", "any", "normal"], 213 | command: OPEN & " $FILE") 214 | result.validActions["notes"] = Action( 215 | name: "notes", 216 | target: "annotations", 217 | labelregex: ".*", 218 | regex: "^Notes(\\..*)?", 219 | modes: @["batch", "any", "normal"], 220 | command: "editnote ~/Notes/tasknotes/$UUID$LAST_MATCH \"$TASK_DESCRIPTION\" $UUID") 221 | result.validActions["url"] = Action( 222 | name: "url", 223 | target: "annotations", 224 | labelregex: ".*", 225 | regex: "((?:www|http).*)", 226 | modes: @["batch", "any", "normal"], 227 | command: OPEN & " $LAST_MATCH") 228 | 229 | result.editor = EDITOR 230 | result.pathExt = PATH_EXT 231 | result.taskbin = "task" 232 | result.taskargs = @[] 233 | 234 | # read config from file 235 | if filepath != "": 236 | if fileExists(filepath): 237 | parseFile(filepath, result) 238 | else: 239 | stdout.write("Config file '", filepath, "' does not exist, create it? [y/N]: ") 240 | let answer = readLine(stdin) 241 | if answer == "y" or answer == "Y": 242 | createConfig(filepath, result) 243 | 244 | for a in result.validActions.keys(): 245 | result.actions.add(a) 246 | 247 | -------------------------------------------------------------------------------- /src/core.nim: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2021-2022 Johannes Schlatow 3 | # 4 | # This file is part of taskopen, which is distributed under the terms of the 5 | # GNU General Public License version 2. 6 | # 7 | 8 | import os 9 | import re 10 | import strutils 11 | import tables 12 | import strtabs 13 | import json 14 | import sugar 15 | import algorithm 16 | 17 | import ./types 18 | import ./config 19 | import ./output 20 | import ./exec 21 | import ./taskwarrior 22 | 23 | proc build_env(s: Settings, 24 | task: JsonNode): StringTableRef = 25 | result = newStringTable() 26 | 27 | # copy env from parent process 28 | for k, v in os.envPairs(): 29 | result[k] = v 30 | 31 | if s.pathExt != "": 32 | result["PATH"] = s.pathExt & ":" & getEnv("PATH") 33 | 34 | if s.editor != "": 35 | result["EDITOR"] = s.editor 36 | 37 | result["ARGS"] = s.args 38 | 39 | result["UUID"] = task["uuid"].getStr() 40 | 41 | if task.hasKey("id"): 42 | result["ID"] = $task["id"].getInt() 43 | else: 44 | result["ID"] = "" 45 | 46 | for attr in s.taskAttributes.split(','): 47 | if task.hasKey(attr): 48 | result["TASK_" & attr.toUpperAscii()] = task[attr].getStr() 49 | 50 | proc copyEnv(baseenv: StringTableRef): StringTableRef = 51 | result = newStringTable() 52 | for k, v in baseenv.pairs(): 53 | result[k] = v 54 | 55 | iterator match_actions_label( 56 | baseenv: StringTableRef, 57 | text: string, 58 | actions: openArray[Action], 59 | single: bool): (Action, StringTableRef) = 60 | 61 | var env = copyEnv(baseenv) 62 | for act in actions: 63 | # split in label and file part 64 | let splitre = re"((\S+):\s+)?(.*)" 65 | if text =~ splitre: 66 | let label = matches[1] 67 | let file = matches[2] 68 | 69 | let labelregex = re(act.labelregex) 70 | let fileregex = re(act.regex) 71 | 72 | # skip action if label does not match 73 | if not label.match(labelregex): 74 | continue 75 | 76 | # skip action if file does not match 77 | env["LAST_MATCH"] = "" 78 | if file =~ fileregex: 79 | for m in matches: 80 | if len(m) == 0: 81 | break 82 | else: 83 | env["LAST_MATCH"] = m 84 | else: 85 | continue 86 | 87 | env["LABEL"] = label 88 | if file.startsWith("~"): 89 | env["FILE"] = file.expandTilde() 90 | else: 91 | env["FILE"] = file 92 | env["ANNOTATION"] = text 93 | 94 | # skip action if filter-command fails 95 | if act.filtercommand != "": 96 | if not exec_filter(act.filtercommand, env): 97 | info.log("Filter command filtered out action ", act.name, " on ", text) 98 | continue 99 | 100 | yield (act, env) 101 | 102 | if single: 103 | break 104 | 105 | env = copyEnv(env) 106 | else: 107 | error.log("Malformed annotation: ", text) 108 | 109 | 110 | iterator match_actions_pure( 111 | baseenv: StringTableRef, 112 | text: string, 113 | actions: openArray[Action], 114 | single: bool): (Action, StringTableRef) = 115 | 116 | var env = copyEnv(baseenv) 117 | for act in actions: 118 | let fileregex = re(act.regex) 119 | if text =~ fileregex: 120 | env["LAST_MATCH"] = "" 121 | for m in matches: 122 | if len(m) == 0: 123 | break 124 | else: 125 | env["LAST_MATCH"] = m 126 | else: 127 | continue 128 | 129 | env["FILE"] = text 130 | env["ANNOTATION"] = text 131 | 132 | # add warning if user specified a labelregex 133 | if act.labelregex != "": 134 | warn.log("labelregex is ignored for actions not targetting annotations") 135 | 136 | # skip action if filter-command fails 137 | if act.filtercommand != "": 138 | if not exec_filter(act.filtercommand, env): 139 | info.log("Filter command filtered out action ", act.name, " on ", text) 140 | continue 141 | 142 | yield (act, env) 143 | if single: 144 | break 145 | env = copyEnv(baseenv) 146 | 147 | 148 | proc find_actionable_items( 149 | s: Settings, 150 | json: JsonNode, 151 | mode = "normal", 152 | single = true): seq[Actionable] = 153 | 154 | # map attributes to potential actions 155 | var action_map: Table[string, seq[Action]] 156 | for actname in s.actions: 157 | var act = s.validActions[actname] 158 | 159 | # apply overrides 160 | if s.filterCommand != "": 161 | act.filtercommand = s.filterCommand 162 | if s.inlineCommand != "": 163 | act.inlinecommand = s.inlineCommand 164 | if s.forceCommand != "": 165 | act.command = s.forceCommand 166 | 167 | if not (mode in act.modes): 168 | continue 169 | if not action_map.hasKey(act.target): 170 | action_map[act.target] = @[] 171 | action_map[act.target].add(act) 172 | 173 | # iterate task attributes 174 | for task in json.items(): 175 | var baseenv = s.build_env(task) 176 | for attr, val in task.pairs(): 177 | if not action_map.hasKey(attr): 178 | continue 179 | 180 | if attr == "annotations": 181 | for ann in val.items(): 182 | let text = ann["description"].getStr() 183 | let entry = ann["entry"].getStr() 184 | for act, env in match_actions_label(baseenv, 185 | text, 186 | action_map[attr], 187 | single=single): 188 | env["ENTRY"] = entry 189 | result.add(Actionable(text: text, 190 | task: task, 191 | entry: entry, 192 | action: act, 193 | env: env)) 194 | else: 195 | let text = val.getStr() 196 | let entry = task["entry"].getStr() 197 | for act, env in match_actions_pure(baseenv, 198 | text, 199 | action_map[attr], 200 | single=single): 201 | env["ENTRY"] = entry 202 | result.add(Actionable(text: text, 203 | task: task, 204 | entry: entry, 205 | action: act, 206 | env: env)) 207 | 208 | 209 | proc sortkeys(sortstr: string): seq[tuple[key: string, desc: bool]] = 210 | for field in sortstr.split(','): 211 | if field =~ re"(.*?)(\+|-)?$": 212 | let key = matches[0] 213 | let desc = matches[1] == "-" 214 | result.add((key: key, desc: desc)) 215 | 216 | 217 | proc run*(settings: Settings, single = true, interactive = true) = 218 | # construct taskwarrior filter 219 | let context = current_context(settings.taskbin, settings.taskargs) 220 | var filters = @[context] & settings.filter 221 | if not settings.all: 222 | filters &= @[settings.basefilter] 223 | 224 | # get json result from taskwarrior 225 | let json = settings.taskbin.json(settings.taskargs, filters) 226 | 227 | # find matching actions 228 | var actionables = settings.find_actionable_items(json, single=single) 229 | 230 | # sort actionables 231 | let sortkeys = sortkeys(settings.sort) 232 | actionables.sort do (x, y: Actionable) -> int: 233 | for field in sortkeys: 234 | let xhas = x.task.hasKey(field.key) 235 | let yhas = y.task.hasKey(field.key) 236 | 237 | var res: int 238 | if field.key == "annot": 239 | res = cmp(x.text, y.text) 240 | elif field.key == "entry": 241 | res = cmp(x.entry, y.entry) 242 | elif not xhas and not yhas: 243 | continue 244 | elif not xhas or not yhas: 245 | if not xhas: 246 | res = 1 247 | else: 248 | res = -1 249 | elif field.key in ["id", "urgency"]: 250 | let xval = x.task[field.key].getInt() 251 | let yval = y.task[field.key].getInt() 252 | res = cmp(xval, yval) 253 | else: 254 | let xval = x.task[field.key].getStr() 255 | let yval = y.task[field.key].getStr() 256 | res = cmp(xval, yval) 257 | 258 | if res == 0: 259 | continue 260 | elif field.desc: 261 | return -1*res 262 | else: 263 | return res 264 | 265 | return 0 266 | 267 | if len(actionables) == 0: 268 | warn.log("No actions applicable.") 269 | 270 | var selected: seq[tuple[cmd: string, env: StringTableRef]] 271 | if interactive and len(actionables) != 1: 272 | # run no annotation hook if len(actionables) == 0 273 | if len(actionables) == 0 and settings.noAnnot != "" and len(json) == 1: 274 | info.log("Executing no_annotation_hook.") 275 | if exec_cmd(settings.noAnnot, env=build_env(settings, json[0])) != 0: 276 | error.log("Failed executing \"", settings.noAnnot, "\".") 277 | return 278 | elif len(actionables) == 0: 279 | return 280 | 281 | # generate menu 282 | selected = collect(newSeq): 283 | for i in menu(actionables): 284 | (cmd: actionables[i].action.command, env: actionables[i].env) 285 | else: 286 | selected = collect(newSeq): 287 | for a in actionables: 288 | (cmd: a.action.command, env: a.env) 289 | 290 | # perform selected action(s) 291 | let res = exec_all(selected) 292 | if res.exitCode != 0: 293 | error.log("Command \"", selected[res.num], "\" failed with exit code: ", res.exitCode) 294 | else: 295 | info.log(res.num, " commands executed.") 296 | -------------------------------------------------------------------------------- /src/exec.nim: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2021-2022 Johannes Schlatow 3 | # 4 | # This file is part of taskopen, which is distributed under the terms of the 5 | # GNU General Public License version 2. 6 | # 7 | 8 | import osproc 9 | import strtabs 10 | 11 | proc exec_filter*(cmd: string, env: StringTableRef = nil): bool {.inline.} = 12 | return execCmdEx(cmd, env=env).exitCode == 0 13 | 14 | iterator exec_inline*(cmd: string, env: StringTableRef = nil): string = 15 | var p = startProcess(cmd, env=env, options={poEvalCommand}) 16 | for line in p.lines: 17 | yield line 18 | p.close() 19 | 20 | proc exec_cmd*(cmd: string, env: StringTableRef = nil): int {.inline.} = 21 | let p = startProcess(cmd, env=env, options={poEvalCommand, poParentStreams}) 22 | result= p.waitForExit() 23 | p.close() 24 | 25 | proc exec_all*(cmds: openArray[tuple[cmd: string, env: StringTableRef]]): tuple[num: int, exitCode: int] = 26 | ## execute all commands and return the number of successfully executed commands 27 | 28 | result.num = 0 29 | for cmd in cmds: 30 | let p = startProcess(cmd.cmd, env=cmd.env, options={poEvalCommand, poParentStreams}) 31 | let code = p.waitForExit() 32 | p.close() 33 | if code != 0: 34 | result.exitCode = code 35 | return result 36 | inc(result.num) 37 | 38 | when isMainModule: 39 | assert(exec_filter("ls -l")) 40 | assert(not exec_filter("lasdfasdf -l -a -z")) 41 | for i in exec_inline("ls -l"): 42 | echo i 43 | -------------------------------------------------------------------------------- /src/output.nim: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2021-2022 Johannes Schlatow 3 | # 4 | # This file is part of taskopen, which is distributed under the terms of the 5 | # GNU General Public License version 2. 6 | # 7 | 8 | import terminal 9 | import tables 10 | import json 11 | import re 12 | from strutils import split, parseInt, spaces, center, strip 13 | from unicode import runeLen, runeSubStr, alignLeft, align 14 | import ./types 15 | import ./exec 16 | 17 | type 18 | LogLevel* = enum 19 | debug 20 | info 21 | warn 22 | error 23 | 24 | ColorSetting = tuple 25 | fg: ForegroundColor 26 | brightfg: bool 27 | bg: BackgroundColor 28 | brightbg: bool 29 | style: set[terminal.Style] 30 | 31 | LevelColors* = array[debug..error, ColorSetting] 32 | 33 | Align* = enum 34 | Left 35 | Center 36 | Right 37 | 38 | Column* = object 39 | align*: Align 40 | width*: Natural 41 | 42 | 43 | # TODO add true color support 44 | 45 | # default color config 46 | var levelcolors*: LevelColors 47 | levelcolors[debug] = (fgGreen, false, bgDefault, false, {}) 48 | levelcolors[info] = (fgDefault, false, bgDefault, false, {}) 49 | levelcolors[warn] = (fgBlue, true, bgDefault, false, {}) 50 | levelcolors[error] = (fgRed, true, bgDefault, false, {}) 51 | 52 | # TODO make colors configurable 53 | var colorstyles = {"action": (fg: fgYellow, 54 | brightfg: false, 55 | bg: bgDefault, 56 | brightbg: false, 57 | style: {styleUnderscore}), 58 | "number": (fg: fgGreen, 59 | brightfg: false, 60 | bg: bgDefault, 61 | brightbg: false, 62 | style: {}), 63 | "annot": (fg: fgWhite, 64 | brightfg: false, 65 | bg: bgDefault, 66 | brightbg: false, 67 | style: {styleDim}), 68 | "desc": (fg: fgDefault, 69 | brightfg: false, 70 | bg: bgDefault, 71 | brightbg: false, 72 | style: {styleItalic}), 73 | "separator": (fg: fgGreen, 74 | brightfg: false, 75 | bg: bgDefault, 76 | brightbg: false, 77 | style: {}), 78 | "id": (fg: fgDefault, 79 | brightfg: false, 80 | bg: bgDefault, 81 | brightbg: false, 82 | style: {}) 83 | }.newTable() 84 | 85 | var level* = warn 86 | 87 | let coloredout = isatty(stdout) 88 | let colorederr = isatty(stderr) 89 | 90 | template log*(lvl: LogLevel = info, v: varargs[string, `$`]) = 91 | var f = stdout 92 | var colored = coloredout 93 | if lvl >= error: 94 | f = stderr 95 | colored = colorederr 96 | 97 | if lvl >= level: 98 | if colored: 99 | f.setForegroundColor(levelcolors[lvl].fg, levelcolors[lvl].brightfg) 100 | f.setBackgroundColor(levelcolors[lvl].bg, levelcolors[lvl].brightbg) 101 | for s in v: 102 | f.write(s) 103 | terminal.styledWriteLine(f, resetStyle) 104 | else: 105 | for s in v: 106 | f.write(s) 107 | f.write("\n") 108 | 109 | template colorout(s: string, v: varargs[string, `$`]) = 110 | if coloredout and s in colorstyles: 111 | stdout.setStyle(colorstyles[s].style) 112 | stdout.setForegroundColor(colorstyles[s].fg, colorstyles[s].brightfg) 113 | stdout.setBackgroundColor(colorstyles[s].bg, colorstyles[s].brightbg) 114 | for str in v: 115 | stdout.write(str) 116 | terminal.styledWrite(stdout, resetstyle) 117 | else: 118 | for str in v: 119 | stdout.write(str) 120 | 121 | 122 | proc menu*(items: openArray[Actionable]): seq[int] = 123 | colorout("default", "Please select one or multiple actions:\n") 124 | for i, item in items.pairs(): 125 | let indent = " " 126 | if i+1 >= 10: 127 | stdout.write(indent[0..^1]) 128 | else: 129 | stdout.write(indent) 130 | 131 | colorout("number", i+1, ")") 132 | stdout.write(" ") 133 | colorout("action", item.action.name, ":") 134 | stdout.write(" ") 135 | colorout("annot", item.text) 136 | stdout.write("\n", indent, " ") 137 | colorout("desc", "(\"", item.task["description"].getStr(), "\")") 138 | if item.task.hasKey("id"): 139 | stdout.write(" ") 140 | colorout("separator", "--") 141 | stdout.write(" ") 142 | colorout("id", item.task["id"].getInt()) 143 | stdout.write("\n") 144 | 145 | # output command to be executed 146 | info.log(indent, " ", "command: ", item.action.command) 147 | 148 | if item.action.inlinecommand != "": 149 | info.log(indent, " ", "inline: ", item.action.inlinecommand) 150 | for line in exec_inline(item.action.inlinecommand, item.env): 151 | stdout.writeline(indent, " ", line) 152 | 153 | stdout.write("Type number(s): ") 154 | let answer = readLine(stdin) 155 | let maxid = len(items) 156 | for ids in answer.split(): 157 | if ids =~ re"(\d+)(?:\.\.|-)(\d+)": 158 | let fromid = parseInt(matches[0]) 159 | let toid = parseInt(matches[1]) 160 | if fromid < 1 or fromid > maxid: 161 | error.log(fromid, " is an invalid number") 162 | quit(1) 163 | if toid < 1 or toid > maxid: 164 | error.log(toid, " is an invalid number") 165 | quit(1) 166 | if fromid >= toid: 167 | error.log(ids, " is an invalid range") 168 | quit(1) 169 | for id in fromid..toid: 170 | result.add(id-1) 171 | elif ids =~ re"\d+": 172 | let id = parseInt(ids) 173 | if id < 1 or id > maxid: 174 | error.log(id, " is an invalid number") 175 | quit(1) 176 | result.add(id-1) 177 | else: 178 | error.log(ids, " is not a number or a range") 179 | 180 | proc splitColumn(s: string, w: Positive): seq[string] = 181 | let length = runeLen(s) 182 | 183 | var i = 0 184 | while i < length: 185 | # TODO preferably split at whitespaces 186 | # e.g. find whitespace in reverse from i to i-w/2 187 | # if none found, cut at i 188 | var substr = s.runeSubStr(i, w) 189 | if i > 0: 190 | substr = substr.strip() 191 | result.add(substr) 192 | i += w 193 | 194 | 195 | proc columnise*(coldef: openArray[Column], indent: Natural, values: varargs[string, `$`]) = 196 | 197 | # first, split input into multiple rows 198 | var multicols: seq[seq[string]] 199 | var rowNum = 0 200 | 201 | var col = 0 202 | for s in values: 203 | var width = 0 204 | if len(coldef) > col: 205 | width = coldef[col].width 206 | 207 | if width == 0: 208 | width = runeLen(s) 209 | 210 | multicols.add(splitColumn(s, width)) 211 | rowNum = max(len(multicols[^1]), rowNum) 212 | col += 1 213 | 214 | # flip dimensions of multicols 215 | var rows: seq[seq[string]] 216 | for i in 0..rowNum-1: 217 | var row: seq[string] 218 | for c in multicols: 219 | if len(c) > i: 220 | row.add(c[i]) 221 | else: 222 | row.add("") 223 | 224 | rows.add(row) 225 | 226 | for row in rows: 227 | stdout.write(spaces(indent)) 228 | 229 | col = 0 230 | for field in row: 231 | var style = Column(align: Left, width: 0) 232 | if len(coldef) > col: 233 | style = coldef[col] 234 | 235 | if style.width == 0: 236 | style.width = runeLen(field) 237 | 238 | case style.align 239 | of Left: 240 | if col < len(multicols)-1: 241 | stdout.write(alignLeft(field, style.width)) 242 | else: 243 | stdout.write(field) 244 | of Center: 245 | # FIXME center() is missing in unicode module 246 | stdout.write(center(field, style.width)) 247 | of Right: 248 | stdout.write(align(field, style.width)) 249 | 250 | col += 1 251 | stdout.writeline("") 252 | 253 | 254 | when isMainModule: 255 | warn.log("Warning: ", "This is ", "shown.") 256 | debug.log("Debug: ", "This ", "is ", "not ", "shown.") 257 | error.log("Error: ", "This is ", "an error.") 258 | error.log("Int ", 1) 259 | var s = @[1,2,3] 260 | error.log("Seq ", s) 261 | let columns = @[Column(align: Left, width: 10), Column(align: Left, width: 3), Column(align: Right, width: 12)] 262 | columns.columnise(2, "Test", " = " ,"Asdf") 263 | columns.columnise(2, "Test1234 asdf asdf asdf", " = ", "Asdf") 264 | columns.columnise(2, "Test1234", " = ", "Asdf foobar foobar foobar foobar") 265 | -------------------------------------------------------------------------------- /src/taskopen.nim: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2021-2022 Johannes Schlatow 3 | # 4 | # This file is part of taskopen, which is distributed under the terms of the 5 | # GNU General Public License version 2. 6 | # 7 | 8 | import parseopt 9 | import os 10 | import system 11 | from strutils import indent, split, splitWhitespace, join 12 | import distros 13 | import tables 14 | 15 | # taskopen modules 16 | import ./output 17 | import ./config 18 | import ./taskwarrior as tw 19 | import ./types 20 | import ./core 21 | 22 | const VERSION {.strdefine.}: string = "unknown" 23 | 24 | proc version():string = 25 | result = "unknown" 26 | when defined(version): 27 | result = VERSION 28 | elif defined(versionNimble): 29 | let regex = re(".*version\\s*=\\s*\"([^\n]+)\"", {reDotAll}) 30 | const nf = staticRead("../taskopen.nimble") 31 | if nf =~ regex: 32 | result = matches[0] 33 | 34 | when not defined(release): 35 | result &= " (Debug)" 36 | 37 | 38 | proc writeDiag(settings: Settings) = 39 | echo "Environment" 40 | 41 | var indent = 2 42 | 43 | let env = [Column(align: Left, width: 16), 44 | Column(align: Left, width: 0)] 45 | 46 | when defined(posix): 47 | if detectOs(MacOSX): 48 | env.columnise(indent, "Platform: ", "Mac OSX") 49 | elif detectOs(Linux): 50 | env.columnise(indent, "Platform: ", "Linux") 51 | when defined(windows): 52 | env.columnise(indent, "Platform: ", "Windows") 53 | 54 | env.columnise(indent, "Taskopen: ", version()) 55 | env.columnise(indent, "Taskwarrior: ", tw.version(settings.taskbin)) 56 | env.columnise(indent, "Configuration: ", settings.configfile) 57 | 58 | echo "Current configuration" 59 | echo(indent("Binaries and paths:", indent)) 60 | indent += 2 61 | 62 | var cfg = [Column(align: Left, width: 18), 63 | Column(align: Left, width: 3), 64 | Column(align: Left, width: 54)] 65 | 66 | cfg.columnise(indent, "taskbin", " = ", settings.taskbin) 67 | cfg.columnise(indent, "taskargs", " = ", settings.taskargs.join(" ")) 68 | cfg.columnise(indent, "editor", " = ", settings.editor) 69 | cfg.columnise(indent, "path_ext", " = ", settings.pathExt) 70 | 71 | echo(indent("General:", indent-2)) 72 | cfg.columnise(indent, "debug", " = ", settings.debug) 73 | cfg.columnise(indent, "no_annotation_hook", " = ", settings.noAnnot) 74 | cfg.columnise(indent, "task_attributes", " = ", settings.taskAttributes) 75 | 76 | echo(indent("Action groups:", indent-2)) 77 | for group, actions in settings.actionGroups.pairs(): 78 | cfg.columnise(indent, group, " = ", actions) 79 | 80 | echo(indent("Subcommands:", indent-2)) 81 | cfg.columnise(indent, "default", " = ", settings.defaultSubcommand) 82 | for sub, alias in settings.validSubcommands.pairs(): 83 | if alias != "": 84 | cfg.columnise(indent, sub, " = ", alias) 85 | 86 | cfg[0].width -= 2 87 | echo(indent("Actions:", indent-2)) 88 | for action in settings.validActions.values(): 89 | echo(indent(action.name, indent)) 90 | cfg.columnise(indent+2, ".target", " = ", action.target) 91 | cfg.columnise(indent+2, ".regex", " = ", action.regex) 92 | cfg.columnise(indent+2, ".labelregex", " = ", action.labelregex) 93 | cfg.columnise(indent+2, ".command", " = ", action.command) 94 | cfg.columnise(indent+2, ".modes", " = ", action.modes.join(",")) 95 | 96 | if action.inlinecommand != "": 97 | cfg.columnise(indent+2, ".inlinecommand", " = ", action.inlinecommand) 98 | if action.filtercommand != "": 99 | cfg.columnise(indent+2, ".filtercommand", " = ", action.filtercommand) 100 | 101 | 102 | proc writeHelp() = 103 | echo("Usage: ", getAppFilename(), " [subcommand] [options] [filter1 filter2 .. filterN]") 104 | 105 | let indent = 2 106 | var columns = [Column(align: Left, width: 28), 107 | Column(align: Left, width: 49)] 108 | 109 | echo("") 110 | echo("Available options:") 111 | columns.columnise(indent, "-h, --help", "Show this text.") 112 | columns.columnise(indent, "-v, --verbose", "Print additional info messages.") 113 | columns.columnise(indent, "--debug", "Print debug messages (includes -v).") 114 | 115 | columns.columnise(indent, "-s=, --sort='key1+,key2-'", "Defines the sort order of actionable items.") 116 | columns.columnise(indent, "-c=, --config=\"filepath\"", "Use a different config file.") 117 | columns.columnise(indent, "-a=, --active-tasks='filter'", "Changes the filter to determine active tasks.") 118 | columns.columnise(indent, "-A, --All", "Query all tasks (ignores taskwarrior context and active task filter).") 119 | 120 | columns.columnise(indent, "-x=, --execute='cmd'", "Overrides the command executed for every action.") 121 | columns.columnise(indent, "-f=, --filter-command='cmd'", "Overrides filter command for every action.") 122 | columns.columnise(indent, "-i=, --inline-command='cmd'", "Overrides inline command for every action.") 123 | columns.columnise(indent, "--args='arguments'", "Defines arguments that will be available as $ARGS in any command.") 124 | 125 | columns.columnise(indent, "--include=action1,action2", "Only consider the listed actions. Also determines their priority.") 126 | columns.columnise(indent, "--exclude=action1,action2", "Consider all but the listed actions.") 127 | 128 | 129 | columns[0].width -= 12 130 | columns[1].width += 12 131 | 132 | echo("") 133 | echo("Available subcommands:") 134 | columns.columnise(indent, "normal", "Shows a menu of the first applicable action for each annotation. Default subcommand.") 135 | columns.columnise(indent, "any", "Shows a menu for every applicable action for each annotation.") 136 | columns.columnise(indent, "batch", "Executes the first applicable action for each annotation without showing an interactive menu before.") 137 | columns.columnise(indent, "diagnostics", "Print diagnostic output.") 138 | columns.columnise(indent, "version", "Print version information.") 139 | 140 | proc includeActions(valid: OrderedTable[string, Action], groups: Table[string, string], includes: string): seq[string] = 141 | for a in includes.split(','): 142 | if valid.hasKey(a): 143 | result.add(a) 144 | elif groups.hasKey(a): 145 | # expand action group 146 | for a2 in groups[a].split(','): 147 | if valid.hasKey(a2): 148 | result.add(a2) 149 | 150 | 151 | proc excludeActions(valid: OrderedTable[string, Action], groups: Table[string, string], excludes: string): seq[string] = 152 | var excluded = excludes.split(',') 153 | 154 | # expand action groups 155 | var expanded = excluded 156 | for a in excluded: 157 | if not valid.hasKey(a) and groups.hasKey(a): 158 | for a2 in groups[a].split(','): 159 | if valid.hasKey(a2): 160 | expanded.add(a2) 161 | 162 | for a in valid.keys(): 163 | if not (a in expanded): 164 | result.add(a) 165 | 166 | 167 | proc parseOpts(opts: seq[string], settings: var Settings, ignoreconfig: bool): string = 168 | const shortNoVal = { 'v', 'h', 'A'} 169 | const longNoVal = @["verbose", "help", "All", "debug"] 170 | var p = initOptParser(opts, 171 | shortNoVal=shortNoVal, 172 | longNoVal=longNoVal) 173 | 174 | while true: 175 | p.next() 176 | case p.kind 177 | of cmdEnd: break 178 | of cmdShortOption, cmdLongOption: 179 | if p.key == "": 180 | settings.filter.add(p.remainingArgs) 181 | break 182 | 183 | case p.key 184 | of "h", "help": 185 | writeHelp() 186 | quit() 187 | 188 | of "v", "verbose": 189 | output.level = LogLevel(min(int(info), int(output.level))) 190 | 191 | of "debug": 192 | output.level = debug 193 | settings.debug = true 194 | 195 | of "s", "sort": 196 | settings.sort = p.val 197 | 198 | of "c", "config": 199 | if not ignoreconfig: 200 | return p.val 201 | 202 | of "a", "active-tasks": 203 | settings.basefilter = p.val 204 | 205 | of "x", "execute": 206 | settings.forceCommand = p.val 207 | 208 | of "f", "filter-command": 209 | settings.filterCommand = p.val 210 | 211 | of "i", "inline-command": 212 | settings.inlineCommand = p.val 213 | 214 | of "args": 215 | settings.args = p.val 216 | 217 | of "include": 218 | if settings.restrictActions: 219 | warn.log("Ignoring --include option because --exclude was already specified.") 220 | else: 221 | settings.actions = includeActions(settings.validActions, 222 | settings.actionGroups, 223 | p.val) 224 | settings.restrictActions = true 225 | 226 | of "exclude": 227 | if settings.restrictActions: 228 | warn.log("Ignoring --exclude option because --include was already specified.") 229 | else: 230 | settings.actions = excludeActions(settings.validActions, 231 | settings.actionGroups, 232 | p.val) 233 | settings.restrictActions = true 234 | 235 | of "A", "All": 236 | settings.all = true 237 | else: 238 | warn.log("Invalid command line option: ", p.key, "=", p.val) 239 | 240 | of cmdArgument: 241 | if settings.command == "" and settings.validSubcommands.hasKey(p.key): 242 | let alias = settings.validSubcommands[p.key] 243 | 244 | if alias != "": 245 | let cfg = parseOpts(alias.splitWhitespace(), settings, ignoreconfig) 246 | 247 | if cfg != "": 248 | return cfg 249 | else: 250 | settings.command = p.key 251 | 252 | else: 253 | settings.filter.add(p.key) 254 | 255 | result = "" 256 | 257 | 258 | proc setup(): Settings = 259 | output.level = warn 260 | 261 | let configfile = findConfig() 262 | 263 | # first, read the config to get aliases and default options 264 | result = parseConfig(configfile) 265 | 266 | # second, parse command line options 267 | let configfile_override = parseOpts(result.unparsedOptions & commandLineParams(), 268 | result, 269 | false) 270 | 271 | if configfile_override != "" and configfile_override != configfile: 272 | # if --config options was found, redo everything 273 | result = parseConfig(configfile_override) 274 | discard parseOpts(result.unparsedOptions & commandLineParams(), 275 | result, 276 | true) 277 | 278 | # apply default command 279 | if result.command == "": 280 | if not result.validSubcommands.hasKey(result.defaultSubcommand): 281 | warn.log("Ignoring non-existing default subcommand '", result.defaultSubcommand, "'.") 282 | result.command = "normal" 283 | else: 284 | let alias = result.validSubcommands[result.defaultSubcommand] 285 | if alias != "": 286 | # note, if an alias is used as default subcommand --config is ignored 287 | discard parseOpts(alias.splitWhitespace(), result, true) 288 | else: 289 | result.command = result.defaultSubcommand 290 | 291 | 292 | when isMainModule: 293 | 294 | var settings = setup() 295 | 296 | try: 297 | case settings.command 298 | of "normal": 299 | run(settings, single=true, interactive=true) 300 | of "batch": 301 | run(settings, single=true, interactive=false) 302 | of "any": 303 | run(settings, single=false, interactive=true) 304 | of "diagnostics": 305 | writeDiag(settings) 306 | of "version": 307 | echo version() 308 | except OSError as e: 309 | error.log(e.msg) 310 | quit(1) 311 | -------------------------------------------------------------------------------- /src/taskwarrior.nim: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2021-2022 Johannes Schlatow 3 | # 4 | # This file is part of taskopen, which is distributed under the terms of the 5 | # GNU General Public License version 2. 6 | # 7 | 8 | import osproc 9 | import strutils 10 | import re 11 | import json 12 | import ./output 13 | 14 | const default_args = ["rc.verbose=blank,label,edit", "rc.json.array=on", "rc.gc=off"] 15 | 16 | proc concat[I1, I2: static[int]; T](a: array[I1, T], b: array[I2, T]): array[I1 + I2, T] = 17 | result[0..a.high] = a 18 | result[a.len..result.high] = b 19 | 20 | template exec(taskbin: string, args: openArray[string], res: untyped) = 21 | debug.log("Executing: ", taskbin, " ", args.join(" ")) 22 | res = execProcess(taskbin, args=args, options = {poUsePath}) 23 | res.stripLineEnd() 24 | 25 | proc version*(taskbin: string): string = 26 | let args = concat(default_args, ["_version"]) 27 | exec(taskbin, args, result) 28 | 29 | proc current_context*(taskbin: string, taskargs: openArray[string]): string = 30 | let args = @default_args & @taskargs & @["context", "show"] 31 | exec(taskbin, args, result) 32 | for line in splitLines(result): 33 | if line =~ re".*with filter '(.*)' is currently applied.$": 34 | return "\\(" & matches[0] & "\\)" 35 | elif line =~ re".*read filter: '(.*)'$": 36 | return "\\(" & matches[0] & "\\)" 37 | return "" 38 | 39 | proc json*(taskbin: string, taskargs: openArray[string], filter: openArray[string]): JsonNode = 40 | let args = @default_args & @taskargs & @filter & @["export"] 41 | 42 | debug.log("Executing: ", taskbin, " ", args.join(" ")) 43 | var p = startProcess(taskbin, args=args, options = {poUsePath}) 44 | result = parseJson(outputStream(p)) 45 | p.close() 46 | 47 | -------------------------------------------------------------------------------- /src/types.nim: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2021-2022 Johannes Schlatow 3 | # 4 | # This file is part of taskopen, which is distributed under the terms of the 5 | # GNU General Public License version 2. 6 | # 7 | 8 | import strtabs 9 | import json 10 | 11 | type 12 | Action* = object 13 | name*: string 14 | target*: string 15 | regex*: string 16 | labelregex*: string 17 | command*: string 18 | modes*: seq[string] 19 | filtercommand*: string 20 | inlinecommand*: string 21 | 22 | Actionable* = object 23 | text*: string 24 | task*: JsonNode 25 | entry*: string 26 | action*: Action 27 | env*: StringTableRef 28 | 29 | -------------------------------------------------------------------------------- /taskopen.nimble: -------------------------------------------------------------------------------- 1 | # TODO This file is untested. 2 | 3 | # Package 4 | 5 | version = "2.0.0alpha" 6 | author = "Johannes Schlatow" 7 | description = " A companion app for taskwarrior that augments annotation handling." 8 | license = "GPLv2" 9 | bin = @["taskopen"] 10 | 11 | # TODO specify installDirs or installFiles 12 | 13 | # Deps 14 | 15 | requires "nim >= 1.4.0" 16 | 17 | # TODO add external dependency to taskwarrior 18 | 19 | --------------------------------------------------------------------------------