#!/usr/bin/env perl
#
# TOFU - plain (stupid) text-based todo manager.
#

# Copyright (c) 2008, 2009 Sbastien Boillod <sbb at tuxfamily dot org>.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

use strict;
use warnings;
use File::Basename;
use File::Copy;
use constant TFU_TANK => "$ENV{HOME}/.tofu";
use constant TFU_VERSION => "2.5";
use constant TFU_DATE => "20090213";

# === "Core" functions ========================================================

sub answer {
    # Return the value attached to the answer specified by the user.
    # answer ANSWERS, VALUES
    my ($alw, %val) = @_;
    while (<STDIN>) {
        chomp; print "Please give an allowed answer [$alw]: " and next unless (exists $val{$_});
        return $val{$_};
    }
}

sub argon {
    # Analyze the arguments from the command-line an return them
    # in the given datas structures.
    # argon SELECTORS ACTIONS
    my ($err, $sel, $act) = ("", @_);
    # Our list of authorities (A = "action", AD = "action in definition style", S = "selector").
    my %auth = qw(Smax 1 Spr 1 Swi 1 Swil 1 Swo 1 Swol 1
                 Aclean 0 Acrown 0 Acset 0 Ahset 0 Awset 0 Adelete 0 Aedit 0 Aprint 0
                 Aread 0 Aheat 0 Afresh 0 ADerase 0 ADfeed 0 ADmove 0 ADprintf 0 ADstamp 0);
    # We get the stack or print help if there is not.
    my $stack = shift @ARGV or &help();
    # We stop here if a stat pattern is given.
    &stat($stack) if (substr($stack, 0, 1) eq "/");
    $stack = &checkstack(&TFU_TANK . "/$stack", &confetti("auto-repair", 1));
    for (reverse @ARGV) {
        # We parse the options.
        my ($name, $val) = split /=/, $_, 2;
        if ($val) {
            # We have a definition.
            if ($auth{"S$name"}) {
                $$sel{$name} = $val; $$sel{all}++ unless $name eq "max"; # "max" is neutral (always applied).
            } elsif (exists $auth{"AD$name"}) {
                # A same action is executed only once.
                push @{$act}, "$name,$val" unless $auth{"AD$name"}++;
                $$sel{atom} = ++$$sel{"next"} if ($name eq "feed" and &confetti("next-on-feed", 0));
           } else {
                $err = "[E] The '$_' option is unallowed or doesn't expect any value.";
           } 
        } elsif (exists $auth{"A$name"}) {
           push @{$act}, "$name" unless $auth{"A$name"}++;
        } elsif ($name =~ m/^(-?[1-9][0-9]*|last|top)$/) {
            $$sel{atom} = ++$$sel{$name};
            $$sel{all} = ++$$sel{ban} if (substr($name, 0, 1) eq "-");
        } elsif ($name eq "next") {
            $$sel{atom} = ++$$sel{$name};
            push @{$act}, "edit" unless (not &confetti("edit-on-next", 0) or $auth{"Aedit"}++);
        } else {
            $err = "[E] The '$_' option is unallowed or expects a value.";
        }
    }
    die "$err\n" if $err;
    # "print" is the default action if there is no other one.
    @{$act} = (@{$act}) ? reverse (@{$act}) : ("print"); return $stack;
}

sub checknew {
    # Ensure that the added tasks have a content and return the "next" value.
    # checknew STACK, NEXT, TASKS
    my ($stk, $nx, $tsk) = @_;
    my ($chk) = reverse &title($stk, "", ($nx - 1));
    while ($chk eq "") {
        warn "[W] The new '" . ($nx - 1) . "' task has no content, set one now (will be removed otherwise) [yn]?\n";
        my $do = &answer("yn", "n", 0, "y", 1);
        unless ($do) {
            &merge($stk, 1, --$nx); delete $$tsk{$nx}; last;
        }
        &edit($stk, ($nx - 1)); ($chk) = reverse &title($stk, "", ($nx - 1));
    }
    return $nx;
}

sub checkstack {
    # Ensure the stack exists, check for trailing *.txt and return its complete path.
    # checkstack STACK, QUIET REPAIR MODE.
    my ($stk, $qie) = @_;
    die "[E] The given '". &basename($stk) . "' stack does not exist.\n" unless (-d $stk);
    if (glob "$stk/*.txt") {
        unless ($qie) {
            warn "[W] There are trailing tasks content in this stack, integrate them now ?[yn]?\n";
            return $stk unless &answer("yn", "n", 0, "y", 1);
        }
        &merge($stk, 0);
    }
    return $stk;
}

sub format {
    # Translate the given format in a printf-suited one.
    # format FORMAT
    my ($fmt) =  @_;
    my ($c, %order) = (0);
    my %nest = ("(", ")", "[", "]", "{", "}", "<", ">");
    $fmt =~ s/%/%%/g;
    while ($fmt =~ m/%%(-?[0-9]+)?([^ ])?([itmpk])/) {
        warn "[W] Each formatting sequence (-> %$3) can be used only once.\n" and $fmt = "" if exists $order{"%".$3};
        $order{"%".$3} = $c++;
        $order{"b%".$3} = defined($2) ? $2 : "";
        $order{"a%".$3} = defined($2) ? $nest{$2} || $2 : "";
        my $rpl = $1 ? $1 : "";
        $fmt =~ s/%%($rpl)\Q$order{"b%".$3}\E$3/%$1s/;
    }
    while ($fmt =~ m/%%([0-9]+) /) {
        my $tmp = sprintf("%$1s", " ");
        $fmt =~ s/%%$1 /$tmp/;
    }
    $fmt =~ s/\\n/\n/g;
    $fmt =~ s/\\t/\t/g;
    return $fmt, %order;
}

sub confetti {
     # Get value of user configuration or return a default one.
     # confetti VALUE, DEFAULT
     my ($val, $get, $def) = ("", @_);
     open CONF, "<$ENV{HOME}/.tofurc" or return $def;
     while (<CONF>) {
         chomp;
         if (m/^\s*$get\s*=\s*(.+?)$/) {
             $val = $1;
             last;
         }
     }
     close CONF;
     $val = $def if $val =~ m/^\s*$/;
     return $val;
}

sub order {
    # Order the tasks according to their priority.
    # order TASKS
    my ($tsk) = @_;
    my %put = ("WARM", [], "HOT", [], "COOL", []);
    map {push @{$put{$$tsk{$_}}}, $_ if exists $put{$$tsk{$_}}} sort { $a<=>$b } keys %$tsk;
    return @{$put{HOT}}, @{$put{WARM}}, @{$put{COOL}};
}

sub mangle {
    # Split and return the selected string as a hash.
    # mangle SEPARATOR, STRING
    my ($fs, $str) = @_;
    return map {$_, 1} (split /$fs/, $str);
}

sub merge {
    # Manage the content of the stack.
    # merge STACK, DELETE MODE, SELECTION
    my ($stk, $del, @rm) = @_;
    my %sel = $del ? map { $_, 1 } @rm : map { &basename($_, ".txt"), 1 } glob "$stk/*.txt";
    open NEW, ">$stk/content.new";
    unless ($del) {
        # We first add the potential modified files (can optimize &explode).
        for my $n (keys %sel) {
            next unless (-s "$stk/$n.txt"); # Don't care about empty files.
            open TXT, "<$stk/$n.txt"; while (<TXT>) { print NEW "$n>$_"; }; close TXT;
        }
    }
    if (open OLD,  "<$stk/content") {
        # We add the taks which weren't touched.
        while (<OLD>) { print NEW $_ if (m/^([0-9]+)>/ and not $sel{$1}); }
        close OLD;
    }
    close NEW;
    move "$stk/content.new", "$stk/content" and unlink ((glob "$stk/*.txt"), (glob "$stk/*~"), (glob "$stk/*.bak"));
}

sub readstack {
    # Parse the stack, fill the hashes given in reference with its content, then
    # return the number of the next task.
    # readstack STACK, PRIORITY, MARKERS.
    my ($n, $t, $stack, $pr, $tags) = (1, 0, @_);
    if (open S, "<$stack/stack") { 
        while (<S>) {
            chomp;
            if (m/^([0-9]+) (WARM|HOT|COOL) ?(.*)/) {
                ($$pr{$1}, $$tags{$1}) = ($2, $3) unless exists $$pr{$1}; # deleted tasks are set to 0.
            } elsif (m/^>([0-9]+.*)/) {
                ($n, $t) = split /,/, $1;
            }
        }
        close S;
    }
    return ($n, $t);
}

sub resolve {
    # Expanse the real name of tasks relatively called, then return if the stack must be written.
    # resolve NEXT, CRITERIA, TASKS, TAGS.
    my ($w, $n, $t, $cr, $tsk, $tg) = (0, @_);
    if ($$cr{"last"}) {
        my ($get) = sort {$b <=> $a} keys %$tsk; $$cr{$get}++ if $get;
    }
    if ($$cr{"top"}) {
        my ($get) = ($t and $$tsk{$t}) ? $t : &order($tsk); $$cr{$get}++ if $get;
    }
    if ($$cr{"next"}) { # should always be the last name resolved.
        $$cr{$$n}++; $$tsk{$$n} = "COOL"; $$tg{$$n} = ""; $w = 1; $$cr{"next"} = $$n++; # recorded for &feed().
    }
    return $w;
}

sub title {
    # Return in a hash the found titles for the selected tasks.
    # title STACK, DEFAULT VALUE, SELECTION
    my ($stk, $def) = (shift @_, shift @_);
    my %sel = map { $_, $def } @_;
    if (open TXT,  "<$stk/content") {
        while (<TXT>) {
            if (s/^([0-9]+)>\s*// and exists $sel{$1} and $sel{$1} eq $def) {
                chomp; $sel{$1} = $_ if $_;
            }
        }
        close TXT;
    }
    return %sel;
}

sub writestack { 
    # Write/update the stack.
    # writestack STACK, TASKS, MARKERS, NEXT, TOP, CHECK
    my ($stk, $tsk, $mrk, $nx, $top, $chk) =  @_;
    $nx = &checknew($stk, $nx, $tsk) if ($chk and &confetti("check-new-tasks", 1));
    &readstack($stk, $tsk, $mrk);
    open S, ">$stk/stack" or die "[E] Can't write in '$stk/stack'.\n";
    for (keys %$tsk) {
        print S "$_ $$tsk{$_} $$mrk{$_}\n" if $$tsk{$_};
    }
    print S ">$nx,$top\n"; close S; return 1;
}

# === "Selectors" functions ===================================================

sub atomtrap {
    # Remove or pick up the requested individual tasks.
    # atomtrap TASKS, CRITERIA, BANISH MODE
    my ($tsk, $sel, $ban) = @_;
    if ($ban) {
        map { delete $$tsk{$1} if /^-([0-9]+)$/ } keys %$sel ;
    } else {
        map { delete $$tsk{$_} if ($$sel{"-$_"} or not $$sel{$_}) } keys %$tsk ;
    }
}

sub deathline {
    # Limit the selection.
    # deathline TASKS, CRITERIA, MAX
    my ($tsk, $crt, $max) = (@_);
    my (%st, %ok) = ();
    $max = (keys %$tsk) unless $max =~ /^[1-9][0-9]*$/;
    # We keep the individualy selected tasks first.
    map { $st{$_} = $$tsk{$_} if /^[0-9]+$/ } keys %$crt;
    for (&order(\%st), &order($tsk)) {
        $ok{$_} = $$tsk{$_};
        last if (keys %ok) >= $max;
    }
    %$tsk = %ok;
}

sub marksman {
    # (De)Select the tasks according to their markers.
    # marksman TASKS, MARKERS, CRITERIA, DROP MODE, MATCH ALL, SELECTION
    my ($tsk, $mrk, $crit, $drop, $all, $tgt) = @_;
    # We put target markers in an hash.
    my %val = &mangle(",", $tgt);
    # We define the limit (different if only one or all markers must be matched). 
    my $lim = $all ? keys %val : 1; # one value in a hash = 2 items.
    for (keys %$mrk) {
        # For each key, we put the markers in a hash, and set the match counter to 0.
        my ($key, %ok)= ($_);
        map { $ok{$_}++ if $val{$_} } (split / /, $$mrk{$key});
        if (($drop && keys %ok >= $lim) or ( keys %ok < $lim && ! $drop)) {
            # If they aren't ecplicitly called, we remove the tasks as requested.
            delete $$tsk{$key} unless $$crit{$key};
        }
    }
}

sub thermostat {
    # Select the tasks according to their priority.
    # thermostat TASKS MARKERS CRITERIA PRIORITIES
    my ($tsk, $mrk, $crit, $pr) = @_;
    # We get the requested priorities.
    my %keep = ("HOT", 0, "WARM", 0, "COOL", 0);
    if ($pr =~ m/^[1-7]$/) {
        # We are in octal mode.
        ++$keep{HOT} and $pr -= 4 if ($pr > 3); 
        ++$keep{WARM} and $pr -= 2 if ($pr > 1); 
        ++$keep{COOL} if ($pr > 0);
    } else {
        # We are in explicit mode.
        my @cut = split /,/, $pr;
        for (@cut) {
            $pr = uc($_);
            if (exists $keep{$pr}) {
                $keep{$pr}++;
            } else {
                warn "[W] The given '$_' value is not a valid priority.\n";
            }
        }
    }
    # Ok, we parse.
    for (keys %$tsk) {
        delete $$tsk{$_} unless ($keep{$$tsk{$_}} or $$crit{$_});
    }
}

# === "Actions" functions =====================================================

sub delete {
    # Remove the given tasks from the stack.
    # delete STACK, DEFAULT ACTION, TASKS
    my ($stk, $do, $del) = @_;
    my (%titles, @rm) = (&title($stk, "Untitled", keys %$del));
    foreach my $d (&order($del)) {
        unless ($do == 2) {
            print "Delete task '$d': \"$titles{$d}\" [yYnNr] ? ";
            $do = &answer(qw(YyNnr n 0 N -1 y 1 Y 2 r 3));
        }
        if ($do > 2) {
            &explode($stk, 0, $d); redo;
        } elsif ($do > 0) {
            $$del{$d} = 0; push @rm, $d;
        } elsif ($do < 0) {
            return 1;
        }
    }
    &merge($stk, 1, @rm); return (grep {$_} values %$del) ? 1 : 0; # will break the main loop if no tasks are left.
}

sub display {
    # Print a formatted and ordered list of the selected tasks.
    # display STACK, FORMAT, TASKS, MARKERS
    my ($stk, $fmt, $tsk, $mrk) = @_;
    my (%ttl, %rpl, @args) = (&title($stk, "Untitled", keys %$tsk));
    # We define the color, unless the "use-color" option is not set to "yes".
    my %col = ((!($fmt =~ s/^\\0//)) and &confetti("use-color", 1)) ? 
                         ("WARM", "\e[0;33m", "HOT", "\e[0;31m", "COOL", "\e[0;36m", "NONE", "\e[0;0m")
                        :("WARM", "", "HOT", "", "COOL", "", "NONE", ""); 
    ($fmt, %rpl) = &format($fmt) or return 1;
    $args[$rpl{"%k"}] =  $rpl{"b%k"} . &basename($stk) . $rpl{"a%k"} if exists $rpl{"%k"};
    for my $id (&order($tsk)) {
        $args[$rpl{"%t"}] =  $rpl{"b%t"} . $ttl{$id} . $rpl{"a%t"} if exists $rpl{"%t"};
        $args[$rpl{"%i"}] = $rpl{"b%i"} . $id . $rpl{"a%i"} if exists $rpl{"%i"};
        $args[$rpl{"%m"}] = $$mrk{$id} ? $rpl{"b%m"} . join(", ", split(" ", $$mrk{$id})) . $rpl{"a%m"}
                                       : $rpl{"b%m"} . "-" . $rpl{"a%m"} if exists $rpl{"%m"};
        $args[$rpl{"%p"}] = $rpl{"b%p"} . $$tsk{$id} . $rpl{"a%p"} if exists $rpl{"%p"};
        printf($col{$$tsk{$id}} . $fmt, @args);
    }
    print $col{NONE};
    return 1;
}

sub edit {
    # Edit the content of the given tasks
    # edit STACK TASKS
    my ($ed, $do, $stk, @tsk) = (&confetti ("editor", "vi"), 0, @_);
    my %ttl = &title($stk, "Untitled", @tsk); &explode($stk, 1, @tsk); # We get the titles and generate the files.
    if (&confetti ("global-edition", "0")) {
        # The entire tasks list is given as argument.
        system $ed . " " . map { "$stk/$_.txt" } @tsk;
    } else {
        # Tasks are edited each one after the other.
        system $ed . " " . "$stk/" . shift(@tsk) . ".txt";
        foreach my $t (@tsk) {
            unless ($do > 1) {
                print "Edit next task '$t': \"$ttl{$t}\" [yYnN] ? ";
                $do = &answer(qw(YyNn n 0 N -1 y 1 Y 2));
            }
            if ($do > 0) {
                system $ed . " " . "$stk/$t.txt";
            } elsif ($do < 0) {
                last;
            }
        }
    }
    &merge($stk, 0); return 1;
}

sub erase {
    # Remove the specified markers from the selected tasks.
    # erase MARKERS, MARKERS TO REMOVE
    my ($mrk, $rm) = @_;
    if ($rm ne "") {
        for (keys %$mrk) {
            my %set =  &mangle(" ", $$mrk{$_});
            map { delete $set{$_} } (split /,/, $rm);
            $$mrk{$_} = join " ", sort (keys %set);
        }
    } else {
        map { $$mrk{$_} = "" } keys %$mrk;
    }
    return 1;
}

sub explode {
    # Display the content of the selected tasks into files or on stdout.
    # explode STACK, FILE MODE, TASKS
    my ($sep, $prv, $stk, $fm, @tsk) = (0, 0, @_);
    open TEXT, "<$stk/content" or return 1;
    for my $cur (@tsk) {
        if ($fm) {
            open OUT, ">$stk/$cur.txt"; select OUT;
        }
        while (<TEXT>) {
            if (s/^$cur>//) {
                print  "<====[$prv]" . "=" x (66 - length("$prv$cur")) . "[$cur]====>\n" unless ($fm or not $sep++); 
                print $_;
                $prv = $cur; last;
            } 
        }
        while (<TEXT>) { last unless s/^$cur>//; print $_ };
        seek(TEXT, 0, 0); close OUT if $fm;
    }
    close TEXT; select STDOUT; # Ensure default output is however stdout.
    return 1;
}

sub feed {
    # Specify a "one-line" task content.
    # feed STACK, CONTENT, NEXT, TASKS
    my ($sm, $stk, $cnt, $nx, $tsk) = (&confetti("safe-feed", 1), @_);
    for my $txt ($sm ? $nx : map {$_ if $$tsk{$_}} keys %$tsk) { # We won't feed deleted tasks.
        last unless $txt; # The selection can be empty.
        open TXT, ">$stk/$txt.txt"; print TXT "$cnt\n"; close TXT;
    }
    &merge($stk, 0); return 1;
}

sub heat {
    # Increase/decrease the priority of the tasks by one level.
    # heat INCREASE, TASKS
    my ($inc, $tsk) = @_;
    my %set = $inc eq "heat" ? qw(WARM HOT COOL WARM) : qw(HOT WARM WARM COOL);
    map {$_ = $set{$_} if $set{$_}} %$tsk;
    return 1;
}

sub help {
    # Display the help which is writen at the end of this file.
    # help
    my $pr = 0;
    map { print " " x ((80 - length($_))/2) . "$_\n" } ("Tofu-" . &TFU_VERSION . " (" .&TFU_DATE . ")",
                                                       "A plain (stupid) text-based todo(s) manager." );
    open HELP, "<$0";
    while (<HELP>) {
        $pr++ if s/^#\s+Eo[FS]\s+$//;
        print " " . substr($_, 1) if ($pr and $_);
    }
    close HELP;
    exit 0;
}

sub jump {
    # Set the priority of the selected tasks to the given one.
    # jump PRIORITY TASKS
    my ($pr, $tsk) = @_;
    my %val = ("c", "COOL", "h", "HOT", "w", "WARM");
    for (keys %$tsk) {
        $$tsk{$_} = $val{$pr} if $$tsk{$_}; # We do not reattribute priority to deleted tasks.
    }
    return 1;
}

sub stamp {
    # Add the specified markers on the selected tasks.
    # stamp MARKERS, NEW MARKERS
    my ($mrk, $add) = @_;
    # We put the new markers in a hash.
    my %new = &mangle(",", $add);
    for (keys %$mrk) {
        # We add the markers to the existing ones.
        my %list = (%new, &mangle(" ", $$mrk{$_}));
        $$mrk{$_} = join " ", sort (keys %list);
    }
    return 1;
}

sub stat {
    # Print a digest of the stacks matching the given pattern. 
    # stat PATTERN
    my ($pat) = @_;
    # We define the color, unless the "color" option is not set to "yes".
    my %col = &confetti("use-color", 1) ? ("WARM", "\e[0;33m", "HOT", "\e[0;31m", "COOL", "\e[0;36m", "NONE", "\e[0;0m")
                                    :  ("WARM", "", "HOT", "", "COOL", "", "NONE", "");
    # We determine if we have a litteral pattern.
    $pat .= "*" unless (length($pat) > 1 and substr($pat,-1,1) eq "/");
    print "Stack name               |  Cur./Tot.       |  Composition (HWC)\n";
    print "-" x 25 . "+" . "-" x 18 . "+" . "-" x 26 ."\n";
    # Ok, let's fill the array...
    for (grep { -r "$_/stack" } glob &TFU_TANK . "/$pat") {
        my ($stk, @mph, %tsk, %nul) = (&basename($_));
        my ($tot) = &readstack(&TFU_TANK . "/$stk", \%tsk, \%nul);
        my ($sp1, $sp2) = (length($stk) - 25 < 1 ? 25 - length($stk) : 1, 15 - length(--$tot . keys %tsk));
        foreach my $pr (qw(HOT WARM COOL)) {
            push @mph, $col{$pr} . grep (/^$pr$/, values %tsk) . $col{NONE};
        }
        print $stk . " " x $sp1 . "|  " .  (keys %tsk) . "/$tot" . " " x $sp2 . "|  " . join(", ", @mph) . "\n";
    } 
    exit 0;
}

sub transfer {
    # Move a task from the current stack to another one.
    # transfer SOURCE STACK, DESTINATION STACK, TASKS, MARKERS
    my ($sstk, $dstk, $stsk, $stgs, %dtsk, %dtgs) = @_;
    warn "[W] Can't open the requested '$dstk' stack.\n" and return 0 unless (-d $dstk);
    &erase($stgs, "") if &confetti("clean-on-move", 0);
    &stamp($stgs, &basename($sstk)) if &confetti("source-auto-stamp", 0);
    my ($nxt, $top) = &readstack($dstk, \%dtsk, \%dtgs);
    &explode($sstk, 1, %$stsk);
    for my $t (sort { $a<=>$b } keys %$stsk) {
        next unless $$stsk{$t}; # some task could have been deleted.
        $dtsk{$nxt} = $$stsk{$t}; $dtgs{$nxt} = $$stgs{$t};
        move "$sstk/$t.txt", "$dstk/" . $nxt++ . ".txt";
    }
    &writestack($dstk, \%dtsk, \%dtgs, $nxt, $top, ""); &merge($dstk, 0);
    &delete($sstk, 2, $stsk); # We let delete return the final signal (maybe there is no tasks left).
}

# === Let's jam! ==============================================================

my ($sw, @actions, %criteria, %tasks, %tags) = ();
my $stack = &argon(\%criteria, \@actions);
# We open the stack, ...
my ($nx, $top) = &readstack($stack, \%tasks, \%tags);

if (%criteria) {
    # We apply our criteria, if there is.
    $sw = &resolve(\$nx, $top, \%criteria, \%tasks, \%tags); # we first resolve the relative names ("next", "last", ...)
    if ($criteria{all}) {
        &thermostat(\%tasks, \%tags, \%criteria, $criteria{pr}) if $criteria{pr};
        &marksman(\%tasks, \%tags, \%criteria, 0, 1, $criteria{wil}) if $criteria{wil};
        &marksman(\%tasks, \%tags, \%criteria, 0, 0, $criteria{wi}) if $criteria{wi};
        &marksman(\%tasks, \%tags, \%criteria, 1, 0, $criteria{wo}) if $criteria{wo};
        &marksman(\%tasks, \%tags, \%criteria, 1, 1, $criteria{wol}) if $criteria{wol};
        &atomtrap(\%tasks, \%criteria, $criteria{all}) if $criteria{ban};
    } elsif ($criteria{atom}) {
        # We have just individual selectors
        &atomtrap(\%tasks, \%criteria, 0);
    }
    &deathline(\%tasks, \%criteria, $criteria{max}) if $criteria{max};
}

# Here we need at least one task.
warn "No task in the selection.\n" and exit 0 unless (%tasks);

for (@actions) {
    # The order of execution is the one given by the user.
    next unless defined $_; # action is undefined when ordered several times on the command line.
    my ($ok, $do, $args) = (1, split(/,/, $_, 2));
    if ($do eq "clean") {
        $sw++; $ok = &erase(\%tags, "");
    } elsif ($do eq "crown") {
        $sw++; ($top) = &order(\%tasks); 
    } elsif ($do eq "delete") {
        $sw++; $ok = &delete($stack, 0, \%tasks);
    } elsif ($do eq "edit") {
        $sw++; $ok = &edit($stack, &order(\%tasks));
    } elsif ($do eq "erase") {
        $sw++; $ok = &erase(\%tags, $args);
    } elsif ($do eq "feed") {
        $sw++; $ok = &feed($stack, $args, $criteria{"next"}, \%tasks);
    } elsif ($do =~ m/^(fresh|heat)$/) {
        $sw++; $ok = &heat($do, \%tasks);
    } elsif ($do eq "move") {
        $sw++; $ok = &transfer($stack, &TFU_TANK . "/$args", \%tasks, \%tags);
    } elsif ($do eq "print") {
        $ok = &display($stack, &confetti("output-format", '%-9i%-11(p%[m\n%9 %t\n\n'), \%tasks, \%tags);
    } elsif ($do eq "printf") {
        $ok = &display($stack, $args, \%tasks, \%tags);
    } elsif ($do eq "read") {
        $ok = &explode($stack, 0, &order(\%tasks));
    } elsif ($do eq "stamp") {
        $sw++; $ok = &stamp(\%tags, $args);
    } else {
        $sw++; $ok = &jump(substr($do, 0, 1), \%tasks);
    }
    last unless $ok;
}

# We write the stack if needed. 
&writestack($stack, \%tasks, \%tags, $nx, $top, $criteria{"next"}) if $sw;

# EoS
#
#   This is a digest of the options acknowledged by tofu. For more details,
#   please, refer to "man 1 tofu".
#
#   Available selectors:
#       
#       last            ---     select the last task added in the stack.
#       max=<n>         ---     limit the selection to <n> tasks maximum.
#       n              ---     select explicitely the task <n>.
#       -n             ---     select all of the tasks but not <n>.
#       next            ---     call and select the next task in the stack.
#       pr=<priorities> ---     select the tasks with the given priorities.
#       top             ---     call in the selection the current top task.
#       wi=<markers>    ---     select the tasks with at least one of the given
#                               markers.
#       wil=<markers>   ---     select the tasks with at least all of the given
#                               markers.
#       wo=<markers>    ---     don't select the tasks with at least one of the
#                               given markers.
#       wol=<markers>   ---     don't select the tasks with at least all of the
#                               given markers.
#   Available actions:
#
#       [chw]set        ---     set the selected task to the [c]ool, [w]arm or
#                               [h]ot priority.
#       clean           ---     remove all of the markers from the selected
#                               tasks.
#       crown           ---     define the first task of the selection as "top".
#       delete          ---     remove the selected tasks from the stack.
#       edit            ---     edit the content of the selected tasks.
#       erase=<markers> ---     remove the given markers from the selected
#                               tasks.
#       feed=<string>   ---     take <string> as the content of the next task.
#       fresh           ---     decrease the priority of the selected tasks by
#                               one level.
#       heat            ---     increase the priority of the selected tasks by
#                               one level.
#       move=<stack>    ---     move the selected tasks into <stack>.
#       print           ---     print a report about the selected tasks, this
#                               is the default action.
#       printf=<format> ---     print a report about the selected tasks with the
#                               specified <format>.
#       read            ---     print the content of the selected tasks.
#       stamp=<markers> ---     put the given markers on the selected tasks.
#       
#   Bug reports, suggestions, feedbacks, questions, and so on, should be sent
#   to Seb (author/maintainer, the license -- MIT/X11 -- is at the head of the
#   script) here : <sbb at tuxfamily dot org>.
#
# EoF
