Permalink
Fetching contributors…
Cannot retrieve contributors at this time
executable file 1225 lines (1103 sloc) 34.5 KB
#!/usr/bin/perl -w -CLASo
###############################################################################
# taskopen - file based notes with taskwarrior
#
# Copyright 2010-2016, Johannes Schlatow.
# All rights reserved.
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the
#
# Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor,
# Boston, MA
# 02110-1301
# USA
#
###############################################################################
use JSON qw( decode_json ); # From CPAN
use File::Temp;
use strict;
use warnings;
# DON'T TOUCH THE FOLLOWING LINES
my $VERSION="v1.1.3";
my $REVNUM="263";
my $REVHASH="610c25e";
# END-DON'T-TOUCH
my $HOME = $ENV{"HOME"};
my $XDG = "xdg-open";
if ($^O =~ m/.*darwin.*/) { #OSX
$XDG = "open";
}
if ($^O =~ m/.*cygwin.*/) { #CYGWIN
$XDG = "cygstart";
}
my $cfgfile = "$HOME/.taskopenrc";
# find alternative config file specification in argument list
for (my $i = 0; $i <= $#ARGV; ++$i) {
my $arg = $ARGV[$i];
if ($arg eq "-c") {
$cfgfile = $ARGV[++$i];
$cfgfile =~ s/^~/$HOME/;
print "Using alternate config file $cfgfile\n";
last;
}
}
# create cfgfile if it doesn't exist (ask first)
unless (-e $cfgfile) {
my $choice = "inval";
while ($choice ne "y" &&
$choice ne "Y" &&
$choice ne "n" &&
$choice ne "N" &&
$choice ne "")
{
print STDOUT qq{Config file "$cfgfile" does not exist. Do you want to create it? (Y/n) };
$choice = <STDIN>;
chomp($choice);
}
if ($choice eq "n" || $choice eq "N") {
print STDERR qq/Can not continue without a config file.\n/;
exit 0;
}
else {
open(my $fh, '>', $cfgfile) or die "can't open $cfgfile: $!";
# write default config
print $fh q<
#BROWSER='xdg-open $FILE &\>/dev/null'
#EDITOR='vim'
#FILE_CMD='xdg-open'
TASKBIN='task'
# If you sync tasks NOTES_FOLDER should be a location that syncs and is available to
# other computers, i.e. /users/dropbox/tasknotes
# NOTES_FOLDER to store notes in, must already exist!
NOTES_FOLDER="$HOME/tasknotes/"
# Preferred extension for tasknotes
NOTES_EXT=".txt"
# Path to notes file. UUID will be replaced with the actual uuid of
# the task. If NOTES_CMD
# Default is: ${NOTES_FOLDER}UUID${NOTES_EXT}
#NOTES_FILE="$HOME/tasknotes/UUID.txt"
# Command that opens notes. UUID will be replaced with the actual uuid of
# the task.
# Default is: $EDITOR $NOTES_FILE
#NOTES_CMD="vim "$HOME/tasknotes/$UUID.txt""
# Specify the default sorting.
# Default is taskwarrior's default sorting, i.e. sorting by task IDs.
#DEFAULT_SORT="urgency-,label,annot"
# Apply a default taskwarrior filter in order to exclude certain tasks.
# Default is: status.is:pending
#DEFAULT_FILTER=
# Default command for '-i'
# Default is: ls -la
#DEFAULT-i="ls -la"
# Add some paths to the taskopen's PATH variable
#PATH_EXT=/path/to/taskopen/scripts
# Regular expression that referes to the NOTES_FILE.
# Default is: Notes
#NOTES_REGEX="Notes"
# Regular expression that identifies annotations openable by BROWSER.
# Default is: www|http
#BROWSER_REGEX="www|http"
# Regular expression that identifies file paths in annotations. Will be opened by xdg-open.
# Default is: \.|\/|~
#FILE_REGEX="\.|\/|~"
# Regular expression that identifies a text annotation. Automatically triggers raw edit mode like '-r'.
#TEXT_REGEX=".*"
# Custom regular expression that specifies annotations passed to CUSTOM1_CMD, e.g:
#CUSTOM1_REGEX="Message-[Ii][Dd]:|message:"
#CUSTOM1_CMD="muttjumpwrapper"
# Custom regular expression that specifies annotations passed to CUSTOM2_CMD.
#CUSTOM2_REGEX=""
#CUSTOM2_CMD=""
# Execute an arbitrary command if there is no annotation available. The corresponding taskwarrior IDs will
# be passed as arguments, e.g. "addnote 21 42"
#NO_ANNOTATION_HOOK=addnote
# Make additional taskwarrior attributes available as sort keys and environment variables.
# E.g. TASK_ATTRIBUTES="project,tags" allows to sort by "task_project" or "task_tags" and to use
# "$TASK_PROJECT" or "$TASK_TAGS" within your (custom) commands.
#TASK_ATTRIBUTES=""
>;
close $fh;
}
}
my %config;
open(my $fh, '<', $cfgfile) or die "can't open $cfgfile: $!";
while (<$fh>) {
chomp;
s/#.*//; # Remove comments
s/^\s+//; # Remove opening whitespace
s/\s+$//; # Remove closing whitespace
next unless length;
my ($key, $value) = split(/\s*=\s*/, $_, 2);
$value =~ s/"(.*)"/$1/;
$value =~ s/'(.*)'/$1/;
$config{$key} = $value;
}
close $fh;
if (exists $config{"PATH_EXT"}) {
$ENV{"PATH"} = qq/$config{"PATH_EXT"}:$ENV{"PATH"}/;
}
my $TASKBIN;
if (exists $config{"TASKBIN"}) {
$TASKBIN = qq/$config{"TASKBIN"} rc.verbose=off rc.json.array=on/;
}
else {
$TASKBIN = 'task rc.verbose=off rc.json.array=on';
}
my $FOLDER;
if (exists $config{"NOTES_FOLDER"}) {
$FOLDER = $config{"NOTES_FOLDER"};
}
else {
$FOLDER = "~/tasknotes/";
}
my $EXT;
if (exists $config{"NOTES_EXT"}) {
$EXT = $config{"NOTES_EXT"};
}
else {
$EXT = ".txt";
}
my $BROWSER;
if (exists $config{"BROWSER"}) {
$BROWSER = $config{"BROWSER"};
}
else {
$BROWSER = $XDG;
}
my $EDITOR;
if (exists $config{"EDITOR"}) {
$EDITOR = $config{"EDITOR"};
}
elsif (exists $ENV{"VISUAL"}) {
$EDITOR = $ENV{"VISUAL"};
}
elsif (exists $ENV{"EDITOR"}) {
$EDITOR = $ENV{"EDITOR"};
}
elsif (!system("which vim &> /dev/null")) {
$EDITOR = "vim";
}
elsif (!system("which nano &> /dev/null")) {
$EDITOR = "nano";
}
else {
die "Cannot find any editor. Please specify EDITOR in your ~/.taskopenrc\n";
}
my $NOTES_FILE;
if (exists $config{"NOTES_FILE"}) {
$NOTES_FILE = $config{"NOTES_FILE"};
}
else {
$NOTES_FILE = "${FOLDER}UUID$EXT";
}
my $NOTES_CMD;
if (exists $config{"NOTES_CMD"}) {
$NOTES_CMD = $config{"NOTES_CMD"};
}
else {
$NOTES_CMD = qq/$EDITOR \"$NOTES_FILE\"/;
}
my $EXCLUDE;
if (exists $config{"DEFAULT_FILTER"}) {
$EXCLUDE = $config{"DEFAULT_FILTER"};
}
else {
$EXCLUDE = "status.is:pending";
}
my $DEBUG;
if (exists $config{"DEBUG"} && $config{"DEBUG"} =~ m/\d+/) {
$DEBUG = $config{"DEBUG"};
}
else {
$DEBUG = 0;
}
my $SORT;
if (exists $config{"DEFAULT_SORT"}) {
$SORT = $config{"DEFAULT_SORT"};
}
else {
$SORT = "";
}
my $DEFAULT_SORT = $SORT;
my $SHOW_CMD;
if (exists $config{"DEFAULT-i"}) {
$SHOW_CMD = $config{"DEFAULT-i"};
}
else {
$SHOW_CMD = "ls -la";
}
my $DEFAULT_i = $SHOW_CMD;
my $DEFAULT_x;
if (exists $config{"DEFAULT-x"}) {
$DEFAULT_x = $config{"DEFAULT-x"};
}
else {
# $DEFAULT_x = qq/$ENV{"SHELL"} -c/;
$DEFAULT_x = "\$FILE";
}
my $NOTES_REGEX;
if (exists $config{"NOTES_REGEX"}) {
$NOTES_REGEX = $config{"NOTES_REGEX"};
}
else {
$NOTES_REGEX = "Notes";
}
my $BROWSER_REGEX;
if (exists $config{"BROWSER_REGEX"}) {
$BROWSER_REGEX = $config{"BROWSER_REGEX"};
}
else {
$BROWSER_REGEX = "www|http";
}
my $FILE_REGEX;
if (exists $config{"FILE_REGEX"}) {
$FILE_REGEX = $config{"FILE_REGEX"};
}
else {
$FILE_REGEX = "\\\.|\\\/|~";
}
my $FILE_CMD;
if (exists $config{"FILE_CMD"}) {
$FILE_CMD = $config{"FILE_CMD"};
}
else {
$FILE_CMD = $XDG;
}
my %custom_regex = ();
for my $key (keys %config) {
if ($key =~ m/CUSTOM(\d+)_REGEX/) {
my $regexkey = "CUSTOM" . $1 . "_REGEX";
my $cmdkey = "CUSTOM" . $1 . "_CMD";
if (exists $config{$cmdkey}) {
$custom_regex{$config{$regexkey}} = $config{$cmdkey}
}
else {
$custom_regex{$config{$regexkey}} = ""
}
}
}
my @TASK_ATTRIBUTES;
if (exists $config{"TASK_ATTRIBUTES"}) {
@TASK_ATTRIBUTES = split(",", $config{"TASK_ATTRIBUTES"});
}
my $NO_ANNOTATION_HOOK = "";
if (exists $config{"NO_ANNOTATION_HOOK"}) {
$NO_ANNOTATION_HOOK = $config{"NO_ANNOTATION_HOOK"};
}
my $REGEX_CODE = "fbn";
if (keys(%custom_regex)) {
$REGEX_CODE = "${REGEX_CODE}c";
}
my $TEXT_REGEX = "";
if (exists $config{"TEXT_REGEX"}) {
$TEXT_REGEX = $config{"TEXT_REGEX"};
$REGEX_CODE = "${REGEX_CODE}t";
}
else {
$REGEX_CODE = "${REGEX_CODE}T";
}
sub print_version {
print "$VERSION $REVNUM $REVHASH\n";
}
sub print_version_long {
print "\n";
print "Taskopen, release $VERSION, revision $REVNUM ($REVHASH)\n";
print "Copyright 2010-2016, Johannes Schlatow.\n";
print "\n";
}
sub diag {
print_version_long;
my $task_version = qx{$TASKBIN _version};
chomp($task_version);
# FIXME no xdg --version on OSX
my $xdg_version;
if ($^O =~ m/.*darwin.*/) { #OSX
$xdg_version = "N/A on OSX";
} elsif ($^O =~ m/.*cygwin.*/) { #CYGWIN
my @output = qx{$XDG --version};
if ($output[0] =~ m/.*\s+(.*)$/) {
$xdg_version = $1;
} else {
$xdg_version = "Couldn't parse '$XDG --version'";
}
} else {
$xdg_version = qx{$XDG --version};
chomp($xdg_version);
}
print "Environment\n";
print " Platform: $^O\n";
print " Perl: $^V\n";
print " Taskwarrior: $task_version\n";
print " xdg-open: $xdg_version\n";
print " Configuration: $cfgfile\n";
print "\n";
print "Current configuration\n";
print " Binaries and paths:\n";
print " BROWSER = $BROWSER\n";
print " TASKBIN = $TASKBIN\n";
print " EDITOR = $EDITOR\n";
print " FILE_CMD = $FILE_CMD\n";
print " PATH = $ENV{'PATH'}\n";
print "\n";
print " Others:\n";
print " NOTES_FOLDER = $FOLDER\n";
print " NOTES_EXT = $EXT\n";
print " NOTES_FILE = $NOTES_FILE\n";
print " NOTES_CMD = $NOTES_CMD\n";
print " DEFAULT_FILTER = $EXCLUDE\n";
print " DEFAULT_SORT = $DEFAULT_SORT\n";
print " current: $SORT\n";
print " DEFAULT-i = $DEFAULT_i\n";
print " current: $SHOW_CMD\n";
print " DEFAULT-x = $DEFAULT_x\n";
print " DEBUG = $DEBUG\n";
print " NOTES_REGEX = $NOTES_REGEX\n";
print " BROWSER_REGEX = $BROWSER_REGEX\n";
print " FILE_REGEX = $FILE_REGEX\n";
print " TEXT_REGEX = $TEXT_REGEX\n";
print " NO_ANNOTATION_HOOK = $NO_ANNOTATION_HOOK\n";
print " TASK_ATTRIBUTES = @TASK_ATTRIBUTES\n";
print " Custom regex commands:\n";
for my $key (keys %custom_regex) {
my $cmd = $custom_regex{$key};
print (" $key\n");
print (" $cmd\n");
}
print "\n";
}
sub set_action
{
my $force = $_[0];
my $arg = $_[1];
my $action= $_[2];
if ($force->{"action"}) {
print STDERR qq/Cannot use $arg in conjunction with $force->{"arg"}\n/;
exit 1;
}
else {
$force->{"action"} = $action;
$force->{"arg"} = $arg;
}
}
sub set_mode
{
my $mode = $_[0];
my $val = $_[1];
if ($$mode && $$mode ne $val) {
print STDERR qq/Cannot use combine arguments -b with -l or -L\n/;
exit 1;
}
else {
$$mode = $val;
}
}
# argument parsing
my $FILTER = "";
my $ID_CMD = "ids";
my $LABEL;
my $HELP;
my $LIST_ANN;
my $LIST_EXEC;
my %FORCE;
my $MATCH;
my $TYPE;
my $BROKEN = 0;
my $MODE;
my $SHOW;
for (my $i = 0; $i <= $#ARGV; ++$i) {
my $arg = $ARGV[$i];
if ($arg eq "-h") {
$HELP = 1;
}
elsif ($arg eq "-v") {
print_version;
exit 0;
}
elsif ($arg eq "-V") {
diag;
exit 0;
}
elsif ($arg eq "-l") {
set_mode(\$MODE, "list");
$LIST_ANN = 1;
}
elsif ($arg eq "-L") {
set_mode(\$MODE, "list");
$LIST_EXEC = 1;
}
elsif ($arg eq "-n") {
if ($REGEX_CODE =~ m/N/) {
print STDERR "Cannot combine argument -n with -N\n";
exit 1;
}
if (length($REGEX_CODE) == 1) {
print STDERR "Cannot combine argument -n with -f/-t\n";
exit 1;
}
$REGEX_CODE = "n";
}
elsif ($arg eq "-N") {
if ($REGEX_CODE eq "n") {
print STDERR "Cannot combine argument -N with -n\n";
exit 1;
}
$REGEX_CODE =~ s/n/N/;
}
elsif ($arg eq "-f") {
if ($REGEX_CODE =~ m/F/) {
print STDERR "Cannot combine argument -f with -F\n";
exit 1;
}
if (length($REGEX_CODE) == 1) {
print STDERR "Cannot combine argument -f with -n/-t\n";
exit 1;
}
$REGEX_CODE = "f";
}
elsif ($arg eq "-F") {
if ($REGEX_CODE eq "f") {
print STDERR "Cannot combine argument -F with -f\n";
exit 1;
}
$REGEX_CODE =~ s/f/F/;
}
elsif ($arg eq "-t") {
if ($REGEX_CODE =~ m/T/) {
print STDERR "Cannot combine argument -t with -T\n";
exit 1;
}
if (length($REGEX_CODE) == 1) {
print STDERR "Cannot combine argument -t with -n/-f\n";
exit 1;
}
$REGEX_CODE = "t";
}
elsif ($arg eq "-T") {
if ($REGEX_CODE eq "t") {
print STDERR "Cannot combine argument -T with -t\n";
exit 1;
}
$REGEX_CODE =~ s/t/T/;
}
elsif ($arg eq "-a") {
$EXCLUDE = "";
}
elsif ($arg eq "-A") {
$EXCLUDE = "";
$ID_CMD = "uuids";
}
elsif ($arg eq "-b") {
set_mode(\$MODE, "batch");
}
elsif ($arg eq "-B") {
$BROKEN = 1;
}
elsif ($arg eq "-D") {
set_action(\%FORCE, "-D", "\\del");
}
elsif ($arg eq "-r") {
set_action(\%FORCE, "-r", "\\raw");
}
elsif ($arg eq "-s") {
$SORT = $ARGV[++$i];
if (!$SORT || $SORT =~ m/^-/) {
print STDERR "Missing argument after $arg\n";
exit 1;
}
}
elsif ($arg eq "-m") {
$MATCH = $ARGV[++$i];
if (!$MATCH || $MATCH =~ m/^-/) {
print STDERR "Missing expression after -m\n";
exit 1;
}
}
elsif ($arg eq "--type") {
$TYPE = $ARGV[++$i];
if (!$TYPE || $TYPE =~ m/^-/) {
printf STDERR "Missing expression after -t\n";
exit 1;
}
}
elsif ($arg eq "-e") {
set_action(\%FORCE, "-e", $EDITOR);
}
elsif ($arg eq "-x") {
my $action;
if ($i >= $#ARGV || $ARGV[$i+1] =~ m/^-/) {
$action = $DEFAULT_x;
}
else {
$action = $ARGV[++$i];
}
set_action(\%FORCE, "-x", $action);
}
elsif ($arg eq "-i") {
if ($i < $#ARGV && $ARGV[$i+1] !~ m/^-/) {
$SHOW_CMD = $ARGV[++$i];
}
$SHOW = 1;
}
elsif ($arg =~ m/\\+(.+)/) {
$LABEL = $1;
}
elsif ($arg eq "-c") {
# just skip (handled above)
$i++;
}
elsif ($arg eq "-p") {
if ($i < $#ARGV && $ARGV[$i+1] !~ m/^-/) {
my $cmd = $ARGV[++$i];
open(PIPE, "|-", $cmd) or die "Cannot execute '$cmd'!\n";
select(PIPE);
$|++;
}
else {
print STDERR "Missing argument after $arg\n";
exit 1;
}
}
else {
$FILTER = "$FILTER $arg";
}
}
sub execute {
_execute(0, $_[0])
}
sub execute_fork {
_execute(1, $_[0])
}
sub _execute {
my $fork = $_[0];
my $cmd = $_[1];
if ($DEBUG > 0) {
if ($fork > 0) {
print "forking: ";
}
else {
print "executing: ";
}
print "$cmd\n";
}
if ($fork > 0) {
system($cmd);
}
else {
exec($cmd);
}
}
sub parse_number {
my $input = $_[0];
my $max = $_[1];
my @items = split(m/[^\d\.-]+/, $input);
if ($#items >= 0) {
my @result;
foreach my $item (@items) {
if ($item =~ m/(\d+)(?:\.\.|-)(\d+)/) {
my $start = $1;
my $end = $2;
if ($start < 1 || $end > $max) {
print STDERR qq/"$item" is an invalid range\n/;
exit 1;
}
while ($start <= $end) {
push(@result, $start);
$start++;
}
}
elsif ($item =~ m/\d+/) {
if ($item < 1 || $item > $max) {
print STDERR qq/"$item" is an invalid number\n/;
exit 1;
}
push(@result, $item);
}
else {
print STDERR qq/"$item" is not a number\n/;
exit 1;
}
}
return @result;
}
else {
print STDERR qq/Invalid input "$input"\n/;
exit 1;
}
}
sub get_filepath {
my $ann = $_[0];
my $file = $ann->{"annot"};
if ($file =~ m/^$NOTES_REGEX/ || $ann->{"raw"} =~ m/^$NOTES_REGEX/) {
$file = $NOTES_FILE;
$file =~ s/([^\$])UUID/$1$ann->{"uuid"}/g;
}
$file =~ s/^~/$HOME/;
return $file;
}
sub raw_edit {
my $old = $_[0];
my $filename = File::Temp::tmpnam();
open(my $fh, '>', $filename) or die "can't open $filename: $!";
print($fh $old);
close($fh);
system(qq/$EDITOR "$filename"/);
open($fh, '<', $filename) or die "can't open $filename: $!'";
my @lines = <$fh>;
close($fh);
unlink($filename);
# taskwarrior does not support multi-line annotations
# TODO fix if #1172 has been solved
my $result = $lines[0];
chomp($result);
return $result;
}
sub reset_env {
delete $ENV{"UUID"};
delete $ENV{"FILE"};
delete $ENV{"ID"};
delete $ENV{"LABEL"};
delete $ENV{"ANNOTATION"};
delete $ENV{"LAST_MATCH"};
foreach my $att (@TASK_ATTRIBUTES) {
my $uc_att = uc($att);
delete $ENV{"TASK_$uc_att"};
}
}
sub prepare_cmd {
my $cmd = $_[0];
my $ann = $_[1];
my $file = $_[2];
my $match = $_[3];
if ($cmd !~ m/\$/) {
$cmd = qq/$cmd '$file'/;
}
else {
$ENV{"FILE"} = $file;
$ENV{"UUID"} = $ann->{"uuid"};
$ENV{"ID"} = $ann->{"id"};
$ENV{"LABEL"} = $ann->{"label"};
$ENV{"ANNOTATION"} = $ann->{"raw"};
$ENV{"LAST_MATCH"} = $match;
foreach my $att (@TASK_ATTRIBUTES) {
if (exists $ann->{"task_$att"}) {
my $uc_att = uc($att);
$ENV{"TASK_$uc_att"} = $ann->{"task_$att"};
}
}
}
return $cmd;
}
sub modify_annotation {
my $ann = $_[0];
my $raw = $_[1];
if ($raw ne $ann->{"raw"}) {
# TODO remove as soon as tw bug TW-1821 has been fixed ('/'s must still be escaped)
if ($ann->{'raw'} =~ m/\// || $raw =~ m/\//) {
return qq{echo "Cannot replace annotations which contain '/'s (see #1174)."}
}
# END REMOVE
return qq%$TASKBIN $ann->{"uuid"} mod /$ann->{"raw"}/$raw/%;
}
else {
return qq/echo "No changes detected"/;
}
}
sub create_cmd {
my $ann = $_[0];
my $FORCE = $_[1];
my $file = $ann->{"annot"};
reset_env();
my $cmd;
my $match = "";
if ($FORCE->{"action"}) {
if ($FORCE->{"action"} eq "\\del") {
return qq/$TASKBIN $ann->{'uuid'} denotate -- "$ann->{'raw'}"/;
}
elsif ($FORCE->{"action"} eq "\\raw") {
my $raw = raw_edit($ann->{"raw"});
return modify_annotation($ann, $raw);
}
else {
$file = get_filepath($ann);
$cmd = qq/$FORCE->{"action"}/;
}
}
else {
if ($REGEX_CODE !~ m/N/ and ($file =~ m/^($NOTES_REGEX)/ || $ann->{"raw"} =~ m/^($NOTES_REGEX)/)) {
# $file = get_filepath($ann);
$match = $+;
$cmd = $NOTES_CMD;
$cmd =~ s/([^\$])UUID/$1\$UUID/g;
}
elsif ($file =~ m/^($BROWSER_REGEX)/ ) {
$match = $+;
if ($file =~ m/^www/) {
# prepend http://
$file = "http://$file";
}
$cmd = qq{$BROWSER};
}
elsif ($REGEX_CODE !~ m/F/ and $file =~ m/^($FILE_REGEX)/) {
$match = $+;
# use XDG for all file types
$cmd = qq{$FILE_CMD};
}
else {
my $found = 0;
for my $regex (keys %custom_regex) {
if ($file =~ m/^($regex)/) {
$match = $+;
$cmd = qq{$custom_regex{$regex}};
$found = 1;
last;
}
}
if (not $found) {
if ($REGEX_CODE !~ m/T/ and length $TEXT_REGEX and $file =~ m/^($TEXT_REGEX)/ ) {
my $raw = raw_edit($ann->{"raw"});
return modify_annotation($ann, $raw);
}
else {
die("no command to execute");
}
}
}
}
return prepare_cmd($cmd, $ann, $file, $match);
}
sub sort_hasharr
{
my $arr = $_[0];
my $sortkeys = $_[1];
return sort {
foreach my $sortkey (@{$sortkeys}) {
$sortkey =~ m/(.*?)(\+|-)?$/;
if (!$a->{$1} && !$b->{$1}) {
next;
}
elsif (!$a->{$1}) {
return 1;
}
elsif (!$b->{$1}) {
return -1;
}
if ($a->{$1} eq $b->{$1}) {
next;
}
elsif ($2 && $2 eq "-") {
return $b->{$1} cmp $a->{$1};
}
else {
return $a->{$1} cmp $b->{$1};
}
}
return 0;
} @{$arr};
}
my $tmpregex="";
my $tmplabelregex="";
foreach my $code (split("", $REGEX_CODE)) {
if ($code eq "n") {
$tmpregex = "${tmpregex}${NOTES_REGEX}|";
$tmplabelregex = "${tmplabelregex}${NOTES_REGEX}|";
}
elsif ($code eq "f") {
$tmpregex = "${tmpregex}${FILE_REGEX}|";
}
elsif ($code eq "b") {
$tmpregex = "${tmpregex}${BROWSER_REGEX}|";
}
elsif ($code eq "c") {
for my $regex (keys %custom_regex) {
$tmpregex = "${tmpregex}${regex}|";
}
}
elsif ($code eq "t") {
$tmpregex = "${tmpregex}${TEXT_REGEX}|"
}
}
chop($tmpregex);
my $FILEREGEX = qr{^((?:$tmpregex).*)};
my $LABELREGEX = qr{^($tmplabelregex)$};
if ($HELP) {
print "Usage: $0 [options] [id|filter1 filter2 ... filterN] [\\\\label]\n\n";
print "Available options:\n";
print " -h Show this text\n";
print " -v Print version information\n";
print " -V Print diagnostic information\n";
print " -l List-only mode, does not open any file; shows annotations\n";
print " -L List-only mode, does not open any file; shows command line\n";
print " -b Batch mode, processes every file in the list\n";
print " -n Only show/open notes file, i.e. annotations matching '$NOTES_REGEX'\n";
print " -N Show all but notes files; inverse of -n\n";
print " -f Only show/open real files, i.e. annotations matching '$FILE_REGEX'\n";
print " -F Show all but real files; inverse of -f\n";
print " -B Show only files that don't exist (combine with -f)\n";
print " -t Only show/open text annotations, i.e. annotations matching '$TEXT_REGEX'\n";
print " -T Show all but text annotations; inverse of -t\n";
print " -a Query all active tasks; clears the DEFAULT_FILTER\n";
print " -A Query all tasks, i.e. completed and deleted tasks as well (very slow)\n";
print " -D Delete the annotation rather than opening it\n";
print " -r Raw mode, opens the annotation text with your EDITOR\n";
print " -m 'regex' Only include annotations that match 'regex'\n";
print " --type 'regex' Only open files whose type (as returned by 'file') matches 'regex'\n";
print " -s 'key1+,key2-' Sort annotations by the given key which can be a taskwarrior field or\n";
print " 'annot', 'label', 'entry', 'size', 'type', 'time', 'mtime' or 'atime'\n";
print " -e Force to open file with EDITOR\n";
print " -x ['cmd'] Execute file, optionally prepend cmd to the command line\n";
print " -i ['cmd'] Execute 'cmd <filename>' for each file and shows its output interleaved with the other output.\n";
print " -c filepath Use alternate taskopenrc file as specified by 'filepath'\n";
print " -p 'cmd' Pipe output into 'cmd' (as in 'taskopen | cmd')\n";
print "\n";
exit 1;
}
my @SORT_KEYS;
if ($SORT) {
@SORT_KEYS = split(',', $SORT);
}
if ($DEBUG > 0) {
print "[DEBUG] Applying filter: $EXCLUDE$FILTER\n";
}
my $ID = qx{$TASKBIN $ID_CMD $EXCLUDE$FILTER};
chomp($ID);
if ($ID eq "") {
print STDERR "There is no task that matches: $EXCLUDE$FILTER\n";
exit 1;
}
sub parse_annotations {
my $ID = $_[0];
# query IDs and parse json
my $json = qx{$TASKBIN $ID export};
my @decoded_json = @{decode_json("$json")};
# Reorganize data
my @annotations;
foreach my $task (@decoded_json) {
if (exists $task->{"annotations"}) {
foreach my $ann (@{$task->{"annotations"}}) {
$ann->{"description"} =~ m/(?:(\S+):\s+)?(.*)/;
my $file = $2;
my $label = $1;
if ( ($file && $file =~ m/$FILEREGEX/ ) ||
($label && $label =~ m/$LABELREGEX/) )
{
if (!$MATCH || ($file =~ m/$MATCH/)) {
if (!$label) {
$label = "";
}
if (!$LABEL || ($LABEL eq $label) ) {
my %entry = ( "annot" => $file,
"uuid" => $task->{"uuid"},
"id" => $task->{"id"},
"raw" => $ann->{"description"},
"label" => $label,
"description" => $task->{"description"});
if ($BROKEN) {
my $filepath = get_filepath(\%entry);
if (-e $filepath) {
if ($DEBUG > 0) {
print qq/[DEBUG] Skipping existing file "$filepath".\n/;
}
next;
}
}
foreach my $att (@TASK_ATTRIBUTES) {
if (exists $task->{$att}) {
if (ref($task->{$att}) eq "ARRAY") {
$entry{"task_$att"} = join(",", @{$task->{$att}});
}
else {
$entry{"task_$att"} = $task->{$att};
}
}
else {
$entry{"task_$att"} = "";
}
}
# Copy sort keys
foreach my $key (@SORT_KEYS) {
$key =~ m/(.*?)(\+|-)?$/;
if (!exists $entry{$1}) {
if (exists $ann->{$1})
{
$entry{$1} = $ann->{$1};
}
elsif (exists $ann->{"task_$1"})
{
$entry{$1} = $ann->{"task_$1"};
}
elsif (exists $task->{$1})
{
# FIXME this is only present due to backwards compatibility
printf STDERR qq/You are using the taskwarrior attribute "$1" for sorting. Please use "task_$1" instead and add "$1" to TASK_ATTRIBUTES in your taskopenrc.\n/;
exit 1;
}
elsif ($1 eq "size") {
my $filepath = get_filepath(\%entry);
$entry{$1} = qx{stat -c "%s" "$filepath"};
}
elsif ($1 eq "mtime") {
my $filepath = get_filepath(\%entry);
$entry{$1} = qx{stat -c "%Y" "$filepath"};
}
elsif ($1 eq "time") {
my $filepath = get_filepath(\%entry);
$entry{$1} = qx{stat -c "%W" "$filepath"};
}
elsif ($1 eq "atime") {
my $filepath = get_filepath(\%entry);
$entry{$1} = qx{stat -c "%X" "$filepath"};
}
elsif ($1 eq "type") {
my $filepath = get_filepath(\%entry);
$entry{$1} = qx{file "$filepath"};
}
else {
print STDERR qq/Unknown sort key "$1"\n/;
exit 1;
}
}
}
if ($TYPE) {
my $filetype;
if (!$entry{"type"}) {
my $filepath = get_filepath(\%entry);
$filetype = qx{file "$filepath"};
}
else {
$filetype = $entry{"type"};
}
if ($filetype =~ m/$TYPE/) {
push(@annotations, \%entry);
}
elsif ($DEBUG > 0) {
print qq/[DEBUG] Skipping file "$entry{'annot'}" whose type doesn't match '$TYPE'\n/;
}
}
else {
push(@annotations, \%entry);
}
}
elsif ($DEBUG > 0) {
if (!$label) {
print qq/[DEBUG] Skipping unlabeled annotation "$ann->{"description"}"\n/;
}
else {
print qq/[DEBUG] Skipping label "$label" (!= "$LABEL")\n/;
}
}
}
elsif ($MATCH && ($DEBUG > 0)) {
print qq/[DEBUG] Skipping annotation which doesn't match '$MATCH': $ann->{"description"}\n/;
}
}
elsif ($DEBUG > 0) {
print qq/[DEBUG] Skipping annotation "$ann->{"description"}"\n/;
}
}
}
}
return @annotations;
}
my @annotations = parse_annotations($ID);
if ($#annotations < 0) {
print STDERR "No compatible annotation found.";
if ($NO_ANNOTATION_HOOK) {
print STDERR " Executing NO_ANNOTATION_HOOK:\n";
execute_fork("$NO_ANNOTATION_HOOK $ID");
@annotations = parse_annotations($ID);
if ($#annotations < 0) {
print STDERR "Still no annotations found.\n";
exit 1;
}
}
else {
print STDERR "\n";
exit 1;
}
}
if ($#SORT_KEYS >= 0) {
@annotations = sort_hasharr(\@annotations, \@SORT_KEYS);
}
# choose an annotation/file to open
my @choices = (0);
if ($#annotations > 0 || ($MODE && $MODE eq "list")) {
print "\n";
if (!$MODE) {
print "Please select an annotation:\n";
}
my $i = 1;
foreach my $ann (@annotations) {
print " $i)";
if (!$MODE || $MODE ne "list" || $LIST_ANN) {
my $id = $ann->{'id'};
if ($id == 0) {
$id = $ann->{'uuid'};
}
my $text = qq/$ann->{'raw'} ("$ann->{'description'}") -- $id/;
print " $text\n";
}
if ($LIST_EXEC) {
if ($LIST_ANN) {
print " executes:";
}
my $cmd = create_cmd($ann, \%FORCE);
print " $cmd\n";
}
if ($SHOW) {
reset_env();
my $filepath = get_filepath($ann);
my $cmd = prepare_cmd($SHOW_CMD, $ann, $filepath);
my $output = qx/$cmd/;
chomp($output);
print " $output\n";
}
$i++;
}
if ($MODE && $MODE eq "list") {
exit 0;
}
elsif ($MODE && $MODE eq "batch") {
@choices = (1..$#annotations+1);
}
else {
# read input
select(STDOUT);
close(PIPE);
print STDOUT "Type number(s): ";
my $choice = <STDIN>;
chomp ($choice);
@choices = parse_number($choice, $#annotations+1);
}
}
##############################################
#open annotations[$choice] with an appropriate program
if ($#choices > 0) {
my $tmp = join(",", @choices);
print "\n";
print STDOUT qq/Do you really want to process files $tmp? (y\/N)\n/;
my $choice = <STDIN>;
chomp ($choice);
if ($choice !~ m/^y/i) {
exit 0;
}
foreach my $choice (@choices) {
my $ann = $annotations[$choice-1];
execute_fork(create_cmd($ann, \%FORCE));
}
}
else {
my $ann = $annotations[$choices[0]-1];
execute(create_cmd($ann, \%FORCE));
}