/w2do.pl

http://w2do.googlecode.com/ · Perl · 872 lines · 518 code · 162 blank · 192 comment · 126 complexity · 1a46e37c5395a9593316e24e4e9b024e MD5 · raw file

  1. #!/usr/bin/env perl
  2. # w2do, a simple text-based todo manager
  3. # Copyright (C) 2008, 2009 Jaromir Hradilek
  4. # This program is free software: you can redistribute it and/or modify it
  5. # under the terms of the GNU General Public License as published by the
  6. # Free Software Foundation, version 3 of the License.
  7. #
  8. # This program is distributed in the hope that it will be useful, but
  9. # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTA-
  10. # BILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
  11. # License for more details.
  12. #
  13. # You should have received a copy of the GNU General Public License along
  14. # with this program. If not, see <http://www.gnu.org/licenses/>.
  15. use strict;
  16. use warnings;
  17. use locale;
  18. use Text::Wrap;
  19. use File::Copy;
  20. use File::Basename;
  21. use File::Spec::Functions;
  22. use Term::ANSIColor;
  23. use Getopt::Long;
  24. # General script information:
  25. use constant NAME => basename($0, '.pl'); # Script name.
  26. use constant VERSION => '2.1.1'; # Script version.
  27. # Global script settings:
  28. our $HOMEDIR = $ENV{HOME} || $ENV{USERPROFILE} || '.';
  29. our $savefile = $ENV{W2DO_SAVEFILE} || catfile($HOMEDIR, '.w2do');
  30. our $backext = '.bak'; # Backup file extension.
  31. our $coloured = 0; # Colour output setup.
  32. our $verbose = 1; # Verbosity level.
  33. our $headcol = 'bold white on_green'; # Table header colour.
  34. our $donecol = 'green'; # Done task colour.
  35. our $todaycol = 'bold'; # Today's task colour.
  36. $Text::Wrap::columns = $ENV{W2DO_WIDTH} || 75; # Default table width.
  37. # Command line options:
  38. my $identifier = undef; # Task identifier.
  39. my $action = 0; # Default action.
  40. my %args = (); # Specifying options.
  41. # Signal handlers:
  42. $SIG{__WARN__} = sub {
  43. exit_with_error((shift) . "Try `--help' for more information.", 22);
  44. };
  45. # Display script usage:
  46. sub display_help {
  47. my $NAME = NAME;
  48. # Print the message:
  49. print << "END_HELP";
  50. Usage: $NAME [-l] [-t task] [-g group] [-d date] [-p priority] [-f|-u]
  51. $NAME -a task [-g group] [-d date] [-p priority] [-f|-u]
  52. $NAME -c id [-t task] [-g group] [-d date] [-p priority] [-f|-u]
  53. $NAME -r id
  54. $NAME [options]
  55. General options:
  56. -l, --list display items in the task list
  57. -a, --add task add new item to the task list
  58. -c, --change id change selected item in the task list
  59. -r, --remove id remove selected item from the task list
  60. --change-group group change all items in the selected group
  61. --remove-group group remove all items from the selected group
  62. --purge-group group remove all finished items in the selected group
  63. --change-date date change all items with selected due date
  64. --remove-date date remove all items with selected due date
  65. --purge-date date remove all finished items with selected due date
  66. --change-old change all items with passed due date
  67. --remove-old remove all items with passed due date
  68. --purge-old remove all finished items with passed due date
  69. --change-all change all items in the task list
  70. --remove-all remove all items from the task list
  71. --purge-all remove all finished items from the task list
  72. -U, --undo revert last action
  73. -G, --groups display all groups in the task list
  74. -S, --stats display detailed task list statistics
  75. -h, --help display this help and exit
  76. -v, --version display version information and exit
  77. Specifying options:
  78. -t, --task task specify the task name
  79. -g, --group group specify the group name
  80. -d, --date date specify the due date; available options are
  81. anytime, today, yesterday, tomorrow, month,
  82. year, or an exact date in the YYYY-MM-DD format
  83. -p, --priority priority specify the priority; available options are 1-5
  84. where 1 represents the highest priority
  85. -f, --finished specify the finished task
  86. -u, --unfinished specify the unfinished task
  87. Additional options:
  88. -s, --savefile file use selected file instead of the default ~/.w2do
  89. -w, --width width use selected line width; the minimal value is 75
  90. -q, --quiet avoid displaying messages that are not necessary
  91. -C, --colour use coloured output instead of the default plain
  92. text version
  93. END_HELP
  94. }
  95. # Display script version:
  96. sub display_version {
  97. my ($NAME, $VERSION) = (NAME, VERSION);
  98. # Print the message:
  99. print << "END_VERSION";
  100. $NAME $VERSION
  101. Copyright (C) 2008, 2009 Jaromir Hradilek
  102. This program is free software; see the source for copying conditions. It is
  103. distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
  104. without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PAR-
  105. TICULAR PURPOSE.
  106. END_VERSION
  107. }
  108. # Display all groups in the task list:
  109. sub display_groups {
  110. my $stats = {};
  111. # Get task list statistics:
  112. get_stats($stats);
  113. # Check whether the list is not empty:
  114. if (%$stats) {
  115. # Display the list of groups:
  116. print join(', ', map { "$_ (" . $stats->{$_}->{done} . '/'
  117. . $stats->{$_}->{tasks} . ')' }
  118. sort keys(%$stats)),
  119. "\n";
  120. }
  121. else {
  122. # Report empty list:
  123. print "The task list is empty.\n" if $verbose;
  124. }
  125. }
  126. # Display detailed task list statistics:
  127. sub display_statistics {
  128. my $stats = {};
  129. my $per;
  130. # Get task list statistics:
  131. my ($groups, $tasks, $undone) = get_stats($stats);
  132. # Display overall statistics:
  133. printf "%d group%s, %d task%s, %d unfinished\n\n",
  134. $groups, (($groups != 1) ? 's' : ''),
  135. $tasks, (($tasks != 1) ? 's' : ''),
  136. $undone;
  137. # Process each group:
  138. foreach my $group (sort (keys %$stats)) {
  139. # Count group percentage:
  140. $per = int($stats->{$group}->{done} * 100 / $stats->{$group}->{tasks});
  141. # Display group progress:
  142. printf "%-11s %s %d%%\n", "$group:", draw_progressbar($per), $per;
  143. }
  144. # Count overall percentage:
  145. $per = $tasks ? int(($tasks - $undone) * 100 / $tasks) : 0;
  146. # Display overall progress:
  147. printf "---\n%-11s %s %d%%\n", "total:", draw_progressbar($per), $per;
  148. }
  149. # Display items in the task list:
  150. sub display_tasks {
  151. my $args = shift;
  152. my @data;
  153. # Load matching tasks:
  154. load_selection(\@data, undef, $args);
  155. # Check whether the list is not empty:
  156. if (@data) {
  157. my $current = '';
  158. my ($id, $group, $date, $priority, $state, $task);
  159. # Prepare the table layout:
  160. my $format = " %-4s %-10s %-10s %s %s %s\n";
  161. my $caption = " id group date pri sta task" .
  162. ' 'x ($Text::Wrap::columns - 45) . "\n";
  163. my $divider = '-'x $Text::Wrap::columns . "\n";
  164. my $border = '='x $Text::Wrap::columns . "\n";
  165. my $indent = ' 'x 41;
  166. # Set up the line wrapper:
  167. $Text::Wrap::columns++;
  168. # Display table header:
  169. $coloured ? print colored ($caption, $headcol)
  170. : print $border, $caption, $border;
  171. # Process each task:
  172. foreach my $line (sort @data) {
  173. # Parse the task record:
  174. $line =~ /^([^:]*):([^:]*):([1-5]):([ft]):(.*):(\d+)$/;
  175. # Check whether the group has changed:
  176. if (lc($1) ne $current) {
  177. # Display the divider unless the first group is being listed:
  178. if ($group) {
  179. $coloured ? print colored ($caption, $headcol) : print $divider;
  180. }
  181. # Remember the current group:
  182. $current = lc($1);
  183. }
  184. # If possible, use relative date reference:
  185. if ($2 eq date_to_string(time)) { $date = 'today'; }
  186. elsif ($2 eq date_to_string(time - 86400)) { $date = 'yesterday'; }
  187. elsif ($2 eq date_to_string(time + 86400)) { $date = 'tomorrow'; }
  188. else { $date = $2; }
  189. # Prepare the rest of the task entry:
  190. $id = $6;
  191. $group = $1;
  192. $priority = $3;
  193. $state = ($4 eq 'f') ? '-' : 'f';
  194. $task = wrap($indent, $indent, $5); $task =~ s/\s+//;
  195. # Set up colours:
  196. print color $donecol if $coloured && $state eq 'f';
  197. print color $todaycol if $coloured && $state ne 'f'
  198. && $date eq 'today';
  199. # Display the task entry:
  200. printf($format, $id, $group, $date, $priority, $state, $task);
  201. # Reset colours:
  202. print color 'reset' if $coloured;
  203. }
  204. }
  205. else {
  206. # Report empty list:
  207. print "No matching task found.\n" if $verbose;
  208. }
  209. }
  210. # Add new item to the task list:
  211. sub add_task {
  212. my $args = shift;
  213. # Use default value when none is provided:
  214. my $group = $args->{group} || 'general';
  215. my $date = $args->{date} || 'anytime';
  216. my $priority = $args->{priority} || '3';
  217. my $state = $args->{state} || 'f';
  218. my $task = $args->{task} || '';
  219. my $id = choose_id();
  220. # Create the task record:
  221. my @data = ("$group:$date:$priority:$state:$task:$id\n");
  222. # Add data to the end of the save file:
  223. add_data(\@data);
  224. # Report success:
  225. print "Task has been successfully added with id $id.\n" if $verbose;
  226. }
  227. # Change selected item in the task list:
  228. sub change_task {
  229. my (@selected, @data);
  230. # Load selected task:
  231. load_selection(\@selected, \@data, { id => shift });
  232. # Change selected item:
  233. change_selection(\@selected, \@data, shift);
  234. }
  235. # Remove selected item from the task list:
  236. sub remove_task {
  237. my (@selected, @data);
  238. # Load selected task:
  239. load_selection(\@selected, \@data, { id => shift });
  240. # Remove selected item:
  241. remove_selection(\@selected, \@data);
  242. }
  243. # Change all items in the selected group:
  244. sub change_group {
  245. my (@selected, @data);
  246. # Load selected tasks:
  247. load_selection(\@selected, \@data, { group => shift });
  248. # Change selected items:
  249. change_selection(\@selected, \@data, shift);
  250. }
  251. # Remove all items in the selected group:
  252. sub remove_group {
  253. my (@selected, @data);
  254. # Load selected tasks:
  255. load_selection(\@selected, \@data, { group => shift });
  256. # Remove selected items:
  257. remove_selection(\@selected, \@data);
  258. }
  259. # Remove all finished items in the selected group:
  260. sub purge_group {
  261. my (@selected, @data);
  262. # Load selected tasks:
  263. load_selection(\@selected, \@data, { group => shift });
  264. # Purge selected items:
  265. purge_selection(\@selected, \@data);
  266. }
  267. # Change all items with selected due date:
  268. sub change_date {
  269. my (@selected, @data);
  270. # Load selected tasks:
  271. load_selection(\@selected, \@data, { date => shift });
  272. # Change selected items:
  273. change_selection(\@selected, \@data, shift);
  274. }
  275. # Remove all items with the selected due date:
  276. sub remove_date {
  277. my (@selected, @data);
  278. # Load selected tasks:
  279. load_selection(\@selected, \@data, { date => shift });
  280. # Remove selected items:
  281. remove_selection(\@selected, \@data);
  282. }
  283. # Remove all finished items with selected due date:
  284. sub purge_date {
  285. my (@selected, @data);
  286. # Load selected tasks:
  287. load_selection(\@selected, \@data, { date => shift });
  288. # Purge selected items:
  289. purge_selection(\@selected, \@data);
  290. }
  291. # Change all items with passed due date:
  292. sub change_old {
  293. my (@selected, @data);
  294. # Load selected tasks:
  295. load_old(\@selected, \@data);
  296. # Change selected items:
  297. change_selection(\@selected, \@data, shift);
  298. }
  299. # Remove all items with passed due date:
  300. sub remove_old {
  301. my (@selected, @data);
  302. # Load selected tasks:
  303. load_old(\@selected, \@data);
  304. # Change selected items:
  305. remove_selection(\@selected, \@data);
  306. }
  307. # Purge all items with passed due date:
  308. sub purge_old {
  309. my (@selected, @data);
  310. # Load selected tasks:
  311. load_old(\@selected, \@data);
  312. # Purge selected tasks:
  313. purge_selection(\@selected, \@data);
  314. }
  315. # Change all items in the task list:
  316. sub change_all {
  317. my (@selected, @data);
  318. # Load all tasks:
  319. load_selection(\@selected, \@data);
  320. # Change all items:
  321. change_selection(\@selected, \@data, shift);
  322. }
  323. # Remove all items from the task list:
  324. sub remove_all {
  325. my (@selected, @data);
  326. # Load all tasks:
  327. load_selection(\@selected, \@data);
  328. # Remove all items:
  329. remove_selection(\@selected, \@data);
  330. }
  331. # Remove all finished items from the task list:
  332. sub purge_all {
  333. my (@selected, @data);
  334. # Load all tasks:
  335. load_selection(\@selected, \@data);
  336. # Purge all tasks:
  337. purge_selection(\@selected, \@data);
  338. }
  339. # Revert last action:
  340. sub revert_last_action {
  341. # Try to restore data from the backup file:
  342. if (move("$savefile$backext", $savefile)) {
  343. # Report success:
  344. print "Last action has been successfully reverted.\n" if $verbose;
  345. }
  346. else {
  347. # If not present, we are probably at oldest change:
  348. print "Already at oldest change.\n" if $verbose;
  349. }
  350. }
  351. # Change selected items in the task list:
  352. sub change_selection {
  353. my ($selected, $data, $args) = @_;
  354. # Check whether the selection is not empty:
  355. if (@$selected) {
  356. # Check whether the changed item is suplied:
  357. if (%$args) {
  358. # Process each item:
  359. foreach my $item (@$selected) {
  360. # Parse the task record:
  361. if ($item =~ /^([^:]*):([^:]*):([1-5]):([ft]):(.*):(\d+)$/) {
  362. # Use existing value when none is supplied:
  363. my $group = $args->{group} || $1;
  364. my $date = $args->{date} || $2;
  365. my $priority = $args->{priority} || $3;
  366. my $state = $args->{state} || $4;
  367. my $task = $args->{task} || $5;
  368. my $id = $6;
  369. # Update the task record:
  370. push(@$data, "$group:$date:$priority:$state:$task:$id\n");
  371. }
  372. }
  373. # Store data to the save file:
  374. save_data($data);
  375. # Report success:
  376. print "Selected tasks have been successfully changed.\n" if $verbose;
  377. }
  378. else {
  379. # Report missing option:
  380. print "You have to specify what to change.\n" if $verbose;
  381. }
  382. }
  383. else {
  384. # Report empty selection:
  385. print "No matching task found.\n" if $verbose;
  386. }
  387. }
  388. # Remove selected items from the task list:
  389. sub remove_selection {
  390. my ($selected, $data) = @_;
  391. # Check whether the selection is not empty:
  392. if (@$selected) {
  393. # Store data to the save file:
  394. save_data($data);
  395. # Report success:
  396. print "Selected tasks have been successfully removed.\n" if $verbose;
  397. }
  398. else {
  399. # Report empty selection:
  400. print "No matching task found.\n" if $verbose;
  401. }
  402. }
  403. # Remove all finished items in the selection:
  404. sub purge_selection {
  405. my ($selected, $data) = @_;
  406. # Check whether the selection is not empty:
  407. if (@$selected) {
  408. # Process each item:
  409. foreach my $item (@$selected) {
  410. # Add unfinished tasks back to the list:
  411. if ($item =~ /^[^:]*:[^:]*:[1-5]:f:.*:\d+$/) {
  412. push(@$data, $item);
  413. }
  414. }
  415. # Store data to the save file:
  416. save_data($data);
  417. # Report success:
  418. print "Selected tasks have been successfully purged.\n" if $verbose;
  419. }
  420. else {
  421. # Report empty selection:
  422. print "No matching task found.\n" if $verbose;
  423. }
  424. }
  425. # Load selected data from the save file:
  426. sub load_selection {
  427. my ($selected, $rest, $args) = @_;
  428. my $reserved = '[\\\\\^\.\$\|\(\)\[\]\*\+\?\{\}]';
  429. # Escape reserved characters:
  430. $args->{group} =~ s/($reserved)/\\$1/g if $args->{group};
  431. $args->{task} =~ s/($reserved)/\\$1/g if $args->{task};
  432. # Use default pattern when none is provided:
  433. my $group = $args->{group} || '[^:]*';
  434. my $date = $args->{date} || '[^:]*';
  435. my $priority = $args->{priority} || '[1-5]';
  436. my $state = $args->{state} || '[ft]';
  437. my $task = $args->{task} || '';
  438. my $id = $args->{id} || '\d+';
  439. # Create the mask:
  440. my $mask = "^$group:$date:$priority:$state:.*$task.*:$id\$";
  441. # Open the save file for reading:
  442. if (open(SAVEFILE, "$savefile")) {
  443. # Process each line:
  444. while (my $line = <SAVEFILE>) {
  445. # Check whether the line matches given pattern:
  446. if ($line =~ /$mask/i) {
  447. # Add line to the selected items list:
  448. push(@$selected, $line);
  449. }
  450. else {
  451. # Add line to the other items list:
  452. push(@$rest, $line);
  453. }
  454. }
  455. # Close the save file:
  456. close(SAVEFILE);
  457. }
  458. }
  459. # Load data with passed due date from the save file:
  460. sub load_old {
  461. my ($selected, $rest) = @_;
  462. # Open the save file for reading:
  463. if (open(SAVEFILE, "$savefile")) {
  464. # Process each line:
  465. while (my $line = <SAVEFILE>) {
  466. # Parse the task record:
  467. $line =~ /^[^:]*:([^:]*):[1-5]:[ft]:.*:\d+$/;
  468. # Check whether the line matches given pattern:
  469. if ("$1" lt date_to_string(time) && "$1" ne 'anytime') {
  470. # Add line to the selected items list:
  471. push(@$selected, $line);
  472. }
  473. else {
  474. # Add line to the other items list:
  475. push(@$rest, $line);
  476. }
  477. }
  478. # Close the save file:
  479. close(SAVEFILE);
  480. }
  481. }
  482. # Save data to the save file:
  483. sub save_data {
  484. my $data = shift;
  485. # Backup the save file:
  486. copy($savefile, "$savefile$backext") if (-r $savefile);
  487. # Open the save file for writing:
  488. if (open(SAVEFILE, ">$savefile")) {
  489. # Process each item:
  490. foreach my $item (@$data) {
  491. # Write data to the save file:
  492. print SAVEFILE $item;
  493. }
  494. # Close the save file:
  495. close(SAVEFILE);
  496. }
  497. else {
  498. # Report failure and exit:
  499. exit_with_error("Unable to write to `$savefile'.", 13);
  500. }
  501. }
  502. # Add data to the end of the save file:
  503. sub add_data {
  504. my $data = shift;
  505. # Backup the save file:
  506. copy($savefile, "$savefile$backext") if (-r $savefile);
  507. # Open the save file for appending:
  508. if (open(SAVEFILE, ">>$savefile")) {
  509. # Process each item:
  510. foreach my $item (@$data) {
  511. # Write data to the save file:
  512. print SAVEFILE $item;
  513. }
  514. # Close the save file:
  515. close(SAVEFILE);
  516. }
  517. else {
  518. # Report failure and exit:
  519. exit_with_error("Unable to write to `$savefile'.", 13);
  520. }
  521. }
  522. # Get task list statistics:
  523. sub get_stats {
  524. my $stats = shift;
  525. my $groups = 0;
  526. my $tasks = 0;
  527. my $undone = 0;
  528. # Open the save file for reading:
  529. if (open(SAVEFILE, "$savefile")) {
  530. # Process each line:
  531. while (my $line = <SAVEFILE>) {
  532. # Parse the task record:
  533. if ($line =~ /^([^:]*):[^:]*:[1-5]:([ft]):.*:\d+$/) {
  534. my $group = lc($1);
  535. # Count group statistics:
  536. if ($stats->{$group}) {
  537. # Increment counters:
  538. $stats->{$group}->{tasks} += 1;
  539. $stats->{$group}->{done} += ($2 eq 't') ? 1 : 0;
  540. }
  541. else {
  542. # Initialize counters:
  543. $stats->{$group}->{tasks} = 1;
  544. $stats->{$group}->{done} = ($2 eq 't') ? 1 : 0;
  545. # Increment number of counted groups:
  546. $groups++;
  547. }
  548. # Count overall statistics:
  549. $tasks++;
  550. $undone++ unless ($2 eq 't');
  551. }
  552. }
  553. }
  554. # Return overall statistics:
  555. return $groups, $tasks, $undone;
  556. }
  557. # Choose first available ID:
  558. sub choose_id {
  559. my @used = ();
  560. my $chosen = 1;
  561. # Open the save file for reading:
  562. if (open(SAVEFILE, "$savefile")) {
  563. # Build the list of used IDs:
  564. while (my $line = <SAVEFILE>) {
  565. push(@used, int($1)) if ($line =~ /:(\d+)$/);
  566. }
  567. # Close the save file:
  568. close(SAVEFILE);
  569. # Find first unused ID:
  570. foreach my $id (sort {$a <=> $b} @used) {
  571. $chosen++ if ($chosen == $id);
  572. }
  573. }
  574. # Return the result:
  575. return $chosen;
  576. }
  577. # Translate due date alias to mask:
  578. sub translate_mask {
  579. my $date = shift;
  580. if ($date eq 'month') {
  581. return substr(date_to_string(time), 0, 8) . '..';
  582. }
  583. elsif ($date eq 'year') {
  584. return substr(date_to_string(time), 0, 5) . '..-..';
  585. }
  586. else {
  587. return translate_date($date);
  588. }
  589. }
  590. # Translate due date alias to YYYY-MM-DD string:
  591. sub translate_date {
  592. my $date = shift;
  593. if ($date =~ /^\d{4}-[01]\d-[0-3]\d$/) { return $date }
  594. elsif ($date eq 'anytime') { return $date }
  595. elsif ($date eq 'today') { return date_to_string(time) }
  596. elsif ($date eq 'yesterday') { return date_to_string(time - 86400) }
  597. elsif ($date eq 'tomorrow') { return date_to_string(time + 86400) }
  598. elsif ($date eq 'month') { return date_to_string(time + 2678400) }
  599. elsif ($date eq 'year') { return date_to_string(time + 31536000) }
  600. else { exit_with_error("Invalid due date `$date'.", 22) }
  601. }
  602. # Translate given date to YYYY-MM-DD string:
  603. sub date_to_string {
  604. my @date = localtime(shift);
  605. return sprintf("%d-%02d-%02d", ($date[5] + 1900), ++$date[4], $date[3]);
  606. }
  607. # Draw progress bar:
  608. sub draw_progressbar {
  609. my $percent = shift;
  610. my $pointer = ($percent > 0 && $percent < 100) ? '>' : '';
  611. return '[' . '=' x int($percent/10) . $pointer .
  612. ' ' x ($percent ? (9 - int($percent/10)) : 10) . ']';
  613. }
  614. # Display given message and immediately terminate the script:
  615. sub exit_with_error {
  616. my $message = shift || 'An unspecified error has occurred.';
  617. my $retval = shift || 1;
  618. print STDERR NAME . ": $message\n";
  619. exit $retval;
  620. }
  621. # Set up the option parser:
  622. Getopt::Long::Configure('no_auto_abbrev', 'no_ignore_case', 'bundling');
  623. # Parse command line options:
  624. GetOptions(
  625. # Specifying options:
  626. 'task|t=s' => sub { $args{task} = $_[1] },
  627. 'group|g=s' => sub { $args{group} = $_[1] },
  628. 'date|d=s' => sub { $args{date} = $_[1] },
  629. 'priority|p=i' => sub { $args{priority} = $_[1] },
  630. 'finished|f' => sub { $args{state} = 't' },
  631. 'unfinished|u' => sub { $args{state} = 'f' },
  632. # Additional options:
  633. 'quiet|q' => sub { $verbose = 0 },
  634. 'verbose|V' => sub { $verbose = 1 },
  635. 'plain|P' => sub { $coloured = 0 },
  636. 'colour|color|C' => sub { $coloured = 1 },
  637. 'savefile|s=s' => sub { $savefile = $_[1] },
  638. 'width|w=i' => sub { $Text::Wrap::columns = $_[1] },
  639. # General options:
  640. 'list|l' => sub { $action = 0 },
  641. 'add|a=s' => sub { $action = 1; $args{task} = $_[1] },
  642. 'change|c=i' => sub { $action = 2; $identifier = $_[1] },
  643. 'remove|r=i' => sub { $action = 3; $identifier = $_[1] },
  644. 'change-group=s' => sub { $action = 12; $identifier = $_[1] },
  645. 'remove-group=s' => sub { $action = 13; $identifier = $_[1] },
  646. 'purge-group=s' => sub { $action = 14; $identifier = $_[1] },
  647. 'change-date=s' => sub { $action = 22; $identifier = $_[1] },
  648. 'remove-date=s' => sub { $action = 23; $identifier = $_[1] },
  649. 'purge-date=s' => sub { $action = 24; $identifier = $_[1] },
  650. 'change-old' => sub { $action = 32 },
  651. 'remove-old' => sub { $action = 33 },
  652. 'purge-old' => sub { $action = 34 },
  653. 'change-all' => sub { $action = 42 },
  654. 'remove-all' => sub { $action = 43 },
  655. 'purge-all' => sub { $action = 44 },
  656. 'undo|U' => sub { $action = 95 },
  657. 'groups|G' => sub { $action = 96 },
  658. 'stats|S' => sub { $action = 97 },
  659. 'help|h' => sub { display_help(); exit 0 },
  660. 'version|v' => sub { display_version(); exit 0 },
  661. );
  662. # Detect superfluous options:
  663. if (scalar(@ARGV) != 0) {
  664. exit_with_error("Invalid option `$ARGV[0]'.", 22);
  665. }
  666. # Trim group option:
  667. if (my $value = $args{group}) {
  668. $args{group} = substr($value, 0, 10);
  669. }
  670. # Translate due date option:
  671. if (my $value = $args{date}) {
  672. if ($action == 0) { $args{date} = translate_mask($value) }
  673. else { $args{date} = translate_date($value) }
  674. }
  675. # Translate due date identifier:
  676. if ($action >= 22 && $action <= 24) {
  677. $identifier = translate_mask($identifier);
  678. }
  679. # Check priority option:
  680. if (my $value = $args{priority}) {
  681. unless ($value =~ /^[1-5]$/) {
  682. exit_with_error("Invalid priority `$value'.", 22);
  683. }
  684. }
  685. # Check line width option:
  686. if ($Text::Wrap::columns < 75) {
  687. exit_with_error("Invalid line width `$Text::Wrap::columns'.", 22);
  688. }
  689. # Perform appropriate action:
  690. if ($action == 0) { display_tasks(\%args) }
  691. elsif ($action == 1) { add_task(\%args) }
  692. elsif ($action == 2) { change_task($identifier, \%args) }
  693. elsif ($action == 3) { remove_task($identifier) }
  694. elsif ($action == 12) { change_group($identifier, \%args) }
  695. elsif ($action == 13) { remove_group($identifier) }
  696. elsif ($action == 14) { purge_group($identifier) }
  697. elsif ($action == 22) { change_date($identifier, \%args) }
  698. elsif ($action == 23) { remove_date($identifier) }
  699. elsif ($action == 24) { purge_date($identifier) }
  700. elsif ($action == 32) { change_old(\%args) }
  701. elsif ($action == 33) { remove_old() }
  702. elsif ($action == 34) { purge_old() }
  703. elsif ($action == 42) { change_all(\%args) }
  704. elsif ($action == 43) { remove_all() }
  705. elsif ($action == 44) { purge_all() }
  706. elsif ($action == 95) { revert_last_action() }
  707. elsif ($action == 96) { display_groups() }
  708. elsif ($action == 97) { display_statistics() }
  709. # Return success:
  710. exit 0;