#!/usr/bin/perl -w # # A little gtd management system. # # Copyright (C) 2007 Alessandro Dotti Contra # # Revision: 0.9.1 #=============================================================================== # 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., 675 Mass Ave, Cambridge, MA 02139, USA. # # For any questions related to this software, please write to: # # Alessandro Dotti Contra # v. Verne, 6 40128 Bologna (BO) # ITALY # # or email to: alessandro@hyboria.org #============================================================================== # DOCUMENTATION #============================================================================== =head1 gtd.pl =head2 Description gtd.pl is nothing more than a gtd aware cards browser. Each card represents a project, and lists the actions of the projects in a free form. =head2 Cards Cards are plain text files ending with the .otl suffix. Since each card represents a project, the first line of the card follows this special formatting rule: @ID:CATEGORY:URGENCY:START_DATE:DUE_DATE:NAME where: =over =item ID: unique identifier of the project =item CATEGORY: the category under which the project is filed =item URGENCY: urgency flag - can be either 0 or 1 =item START_DATE: the date upon which the project will start (optional) =item DUE_DATE: the date upon which the project dues (optional) =item NAME: name of the project =back =head2 Default key bindings =over =item CTRL+x: select the menu =item CTRL+b: select the browser =item CTRL+f: select the category filter =item CTRL+r: refresh the browser =item CTRL+a: show all projects =item CTRL+u: show urgent projects =item CTRL+v: show the currently selected project =item CTRL+t: show due projects =item CTRL+q: quits the application =back All standard ncurses key bindings work as expected. =head2 Notes The otl suffix was chosen because the author likes to use gtd.pl with vim and vimoutliner. Cards are edited using an external editor. Gvim is the default editor, but can be changed by modifying the $EDITOR value in the CONFIGURATION section of the script. Comments, bug reports, suggestions and any kind of contribution can be sent to: alessandro@hyboria.org =cut #============================================================================== # CONFIGURATION #============================================================================== my $DATADIR = "/home/adotti/gtd"; my $EDITOR = "gvim"; #============================================================================== # MODULES #============================================================================== use Curses::UI; use Date::Calc qw( Today Delta_Days ); #============================================================================== # GLOBAL DATA #============================================================================== my %cards = (); my %filed_cards = (); my %urgent = (); my %due = (); my %browser = (); my %viewer = (); my @categories = (); my $browser = undef; my $filter = undef; #============================================================================== # FUNCTIONS (prototypes) #============================================================================== sub read_cards($); sub load_filter(); sub load_browser($$); sub view_card(); sub show_category(); sub show_all(); sub show_urgent(); sub show_due(); sub refresh(); sub is_due($); #============================================================================== # # Read cards # read_cards($DATADIR); #============================================================================== # INTERFACE #============================================================================== # # Create the root object. # my $cui = new Curses::UI ( -clear_on_exit => 1, -color_support => 1, ); # # Create the main menu # # File submenu # my $submenu_file = [ { -label => 'Exit ^Q', -value => \&exit_app }, ]; my $submenu_show = [ { -label => 'All projects ^A', -value => \&show_all }, { -label => 'Urgent projects ^U', -value => \&show_urgent }, { -label => 'Due projects ^D', -value => \&show_due }, ]; my @menu = ( { -label => 'File', -submenu => $submenu_file }, { -label => 'Show', -submenu => $submenu_show }, ); my $menu = $cui->add( 'menu','Menubar', -menu => \@menu, -fg => "green", ); # # Create the main window # my $main = $cui->add( 'main', 'Window', -y => 1, ); # Create the filter popup # $filter = load_filter(); # Create the main browser # show_all(); # # Set bindings # $cui->set_binding(sub {$menu->focus()}, "\cX"); $cui->set_binding(sub {$browser->focus()}, "\cB"); $cui->set_binding(sub {$filter->focus()}, "\cF"); $cui->set_binding(sub {refresh()}, "\cR"); $cui->set_binding(sub {show_all()}, "\cA"); $cui->set_binding(sub {show_urgent()}, "\cU"); $cui->set_binding(sub {view_card()}, "\cV"); $cui->set_binding(sub {exit 0}, "\cQ"); $cui->set_binding(sub {show_due()}, "\cT"); $browser->focus(); $cui->mainloop(); exit 0; #============================================================================== # FUNCTIONS #============================================================================== sub read_cards($) { # Read cards from the data directory and file them # my $dir = shift; opendir(DIR,"$dir") || die "Can't read directory $dir: $!\n"; my @items = readdir(DIR); chdir($dir); foreach my $item(sort @items) { next if $item =~ /^\./; if(-f $item && $item =~ /\.otl$/) { next unless open(CARD,"$item"); while() { chomp; # Read the first line to gather card information # if(/^(:|@)/) { my ($id,$category,$urgency,$sdate,$ddate,$name) = split(/:/); push @categories, $category unless grep(/$category/,@categories); $cards{$name} = $item; $urgent{$name} = $item if $urgency; $due{$name} = $item if is_due($ddate); $filed_cards{$category}{$name} = $item; } } close(CARD); } } closedir(DIR); } sub load_filter() { # Load the filter drop-down # my $values = []; my $labels = {}; $values->[0] = 0; $labels->{0} = "All projects"; my $i = 1; foreach my $category (sort @categories) { $values->[$i] = $i; $labels->{$i} = $category; $i++; } my $filter = $main->add( undef, 'Popupmenu', -y => 1, -values => $values, -labels => $labels, -width => 50, -onchange => \&show_category, ); return $filter; } sub load_browser($$) { my $title = shift; my $hashref = shift; my $values = []; my $labels = {}; my $i = 0; foreach my $name(sort keys %{ $hashref }) { $values->[$i] = $i; $labels->{$i} = $name; $i++; } my $listbox = $main->add( undef, 'Listbox', -y => 2, -values => $values, -labels => $labels, -title => $title, -pad => 1, -border => 1, -bfg => 'green', -vscrollbar => 1, -onchange => \&view_card, ); return $listbox; } sub view_card() { my $listbox; if(@_) { $listbox = shift; } else { $listbox = $browser; } my $selected = $listbox->get(); my $name = $listbox->{-labels}->{$selected}; system "$EDITOR $DATADIR/$cards{$name}"; } sub show_all() { $browser = load_browser("All projects", \%cards); $browser{'All projects'} = $browser; $browser->focus(); } sub show_category() { my $pm = shift; my $val = $pm->get; my $selected = $pm->{-labels}->{$val}; if($browser{$selected}) { $browser = $browser{$selected}; } else { $browser = load_browser($selected, \%{ $filed_cards{$selected} }); $browser{$selected} = $browser; } $browser->focus(); } sub show_urgent() { #Show cards with the urgency flag set # $browser = load_browser("Urgent projects", \%urgent); $browser->focus(); } sub show_due() { #Show cards with due tasks # $browser = load_browser("Due projects", \%due); $browser->focus(); } sub refresh() { #Refresh data # %cards = (); %filed_cards = (); %urgent = (); %due = (); %browser = (); %viewer = (); @categories = (); $browser = undef; $filter = undef; read_cards($DATADIR); # Create the filter popup # $filter = load_filter(); # Create the main browser # $browser = load_browser("All projects", \%cards); $browser{'All projects'} = $browser; $browser->focus(); } sub is_due($) { # checks if a date is due or not # my $date = shift; if($date =~ /^(\d{1,4})-(\d{1,2})-(\d{1,2})$/) { return Delta_Days(Today(),$1,$2,$3) > 0 ? 0 : 1; } else { return 0; } }