=pod =head1 App::TimeTracker, Metaprogramming & Method Modifiers =head2 META =head3 Author Thomas Klausner =head3 Email domm AT plix.at =head3 URL http://domm.plix.at =head3 URL_slides http://domm.plix.at/talks/2013_berlin_app_timetracker =head3 Date 2013-03-14 =head3 Location Betahaus, Berlin =head3 Event German Perl Workshop 2013 =head2 App:TimeTracker time tracking for impatient and lazy command line lovers =head2 Time Tracking We need Time Tracking Software =over =item * to figure out what we can bill our customers =item * to evaluate our estimates =item * to stop us from working too long =back There are lots of different solutions for time tracking available And in spirit of this xkcd =for newslide =for img standards.png =for newslide ... we build our own =head2 Example usage ~/perl/App-TimeTracker$ tracker start Started working on App-TimeTracker at 11:51:19 =for newslide ~/perl/App-TimeTracker$ tracker current Working 00:12:42 on App-TimeTracker =for newslide ~/perl/App-TimeTracker$ tracker stop Worked 00:13:12 on App-TimeTracker =for newslide ~/perl/App-TimeTracker$ tracker current Currently not working on anything. =for newslide ~/perl/App-TimeTracker$ tracker start --tag testing Started working on App-TimeTracker (testing) at 12:11:07 =for newslide ~/perl/App-TimeTracker$ tracker start --tag documentation Worked 00:12:41 on App-TimeTracker (testing) Started working on App-TimeTracker (documentation) at 12:23:48 =for newslide ~$ tracker report --this week App-TimeTracker 03:57:21 Xaxos 15:55:39 oe1.orf.at 00:09:32 total 20:02:32 =head2 Storage Information on each task is stored in a JSON file { "stop" : "2011-08-01T17:11:00", "project" : "App-TimeTracker", "duration" : "02:09:50", "tags" : [ "config" ], "seconds" : 7790, "__CLASS__" : "App::TimeTracker::Data::Task", "user" : "domm", "start" : "2011-08-01T15:01:10" } =for newslide Those JSON files are created directly from our Moose-powered C class. And most of the work is done by C and C =for newslide package **App::TimeTracker::Data::Task**; use Moose; has 'start' => ( isa=>'DateTime', is=>'ro', required=>1, default=>sub { DateTime->now(time_zone=>'local') } ); has 'stop' => ( isa=>'DateTime', is=>'rw', trigger=>\&_calc_duration, ); has 'project' => ( isa=>'Str', is=>'ro', required=>1, ); =for newslide package App::TimeTracker::Data::Task; use **Moose**; %%has%% '@@start@@' => ( isa=>'DateTime', is=>'ro', required=>1, default=>sub { DateTime->now(time_zone=>'local') } ); %%has%% '@@stop@@' => ( isa=>'DateTime', is=>'rw', trigger=>\&_calc_duration, ); %%has%% '@@project@@' => ( isa=>'Str', is=>'ro', required=>1, ); =for newslide use **MooseX::Storage**; with Storage( format => [ JSONpm => { json_opts => { pretty => 1 } } ], io => "File", ); =for newslide use MooseX::Storage; @@with@@ **Storage**( format => [ %%JSONpm%% => { json_opts => { pretty => 1 } } ], io => "File", ); =for newslide my **$task** = App::TimeTracker::Data::Task->**new**( ... ); $task->store( $path_to_file ); =for newslide my $task = App::TimeTracker::Data::Task->new( ... ); $task->**store**( @@$path_to_file@@ ); .. and the current object will be stringified as a JSON file. but there's one small problem: =for newslide MooseX::Storage can only stringify native Moose types. If you use other types, you'll have to add a C to expand and collapse the respective objects. MooseX::Storage::Engine->add_custom_type_handler( 'DateTime' => ( expand => sub { DateTime::Format::ISO8601->parse_datetime(shift) }, collapse => sub { (shift)->iso8601 } ) ); =for newslide One more handy Moose feature: has 'stop' => ( isa=>'DateTime', is=>'rw', **trigger**=>%%\&_calc_duration%%, ); The trigger is called when the attribute is set and can be used to set other attributes: =for newslide sub _calc_duration { my ( $self, $stop ) = @_; $stop ||= $self->stop; my $delta = $stop->subtract_datetime($self->start); $self->**seconds**($dtf_sec->format_duration($delta)); $self->**duration**($dtf_dur->format_duration($delta)); } =for newslide Add some more helper methods and we have a nice and simple class to store, retrieve, handle and alter all data we need to keep track of our time. =for newslide We're not using a database (not even SQLite) because it's terrible complicated to synchronize a database between different machines and / or allow offline access to a central database So we went the way of C and decided to store all info locally. We'll see later how synchronization is implemented. =head2 But... This is now all nice and shiny, but not very special. What we actually wanted was a system that in addition to simply keeping track of time would make it easy to stick to a good workflow. =head2 Our Workflow Find a ticket you need / want to work on. (We're using RequestTracker) Create a git topic branch with a meaningful name. Tell your co-workers that you're now working on this ticket. ... HackHackHack ... =for newslide Merge your branch back into master. Announce that you're done with this ticket. Record the time you spend on this ticket. =for newslide While each of those steps doesn't take that long it is still very annoying to actually do all of them. And they can be automated. Now it would have been easy to hardcode all of these steps into our timetracker. But: =for newslide Besides my regular job, I work on other projects commercial and open source Not all of them use RequestTracker Not all of them use git Not all of them are of interest to my co-workers (so I do not want to spam them with announcements) =for newslide We need a flexible, pluggable application. The behavior of the application should be different depending on the context. We need to basically build a custom application from a set of available building blocks. This sounded like a good excuse to use Meta Programming, one of the more esoteric / cool features of Moose. =for newslide =head2 Dynamically generated Applications =head3 C, the frontend #!/usr/bin/perl use strict; use warnings; use App::TimeTracker::Proto; App::TimeTracker::Proto->new->run; The modern way to write a script: Use a Class, initiate it, and the run it. =for newslide App::TimeTracker::**Proto**->new->run; Everything else is handled in the class, which is much easier to test. =head3 App::TimeTracker::Proto A class that creates new classes based on the current config. The C method basically look like this: =for newslide sub run { my $self = shift; my $config = $self->load_config; my $class = Moose::Meta::Class->create_anon_class( superclasses => ['App::TimeTracker'], roles => [ map { 'App::TimeTracker::Command::' . $_ } 'Core', @{ $config->{plugins} } ], ); $class->name->new_with_options( { home => $self->home, config => $config, _currentproject => $self->project, } )->run; } =for newslide sub run { my $self = shift; my **$config** = $self->**load_config**; my $class = Moose::Meta::Class->create_anon_class( superclasses => ['App::TimeTracker'], roles => [ map { 'App::TimeTracker::Command::' . $_ } 'Core', @{ $config->{plugins} } ], ); $class->name->new_with_options( { home => $self->home, config => $config, _currentproject => $self->project, } )->run; } We'll take a close look at C later. For now it's enough to know that it returns a hash containing a list of plugins =for newslide sub run { my $self = shift; my $config = $self->load_config; my %%$class%% = @@Moose::Meta::Class@@->**create_anon_class**( superclasses => ['App::TimeTracker'], roles => [ map { 'App::TimeTracker::Command::' . $_ } 'Core', @{ $config->{plugins} } ], ); $class->name->new_with_options( { home => $self->home, config => $config, _currentproject => $self->project, } )->run; } =for newslide sub run { my $self = shift; my $config = $self->load_config; my $class = Moose::Meta::Class->create_anon_class( @@superclasses@@ => ['App::TimeTracker'], roles => [ map { 'App::TimeTracker::Command::' . $_ } 'Core', @{ $config->{plugins} } ], ); $class->name->new_with_options( { home => $self->home, config => $config, _currentproject => $self->project, } )->run; } =for newslide sub run { my $self = shift; my $config = $self->load_config; my $class = Moose::Meta::Class->create_anon_class( @@superclasses@@ => ['**App::TimeTracker**'], roles => [ map { 'App::TimeTracker::Command::' . $_ } 'Core', @{ $config->{plugins} } ], ); $class->name->new_with_options( { home => $self->home, config => $config, _currentproject => $self->project, } )->run; } =for newslide sub run { my $self = shift; my $config = $self->load_config; my $class = Moose::Meta::Class->create_anon_class( superclasses => ['App::TimeTracker'], @@roles@@ => [ map { 'App::TimeTracker::Command::' . $_ } 'Core', @{ $config->{plugins} } ], ); $class->name->new_with_options( { home => $self->home, config => $config, _currentproject => $self->project, } )->run; } =for newslide sub run { my $self = shift; my $config = $self->load_config; my $class = Moose::Meta::Class->create_anon_class( superclasses => ['App::TimeTracker'], @@roles@@ => [ **map** { '%%App::TimeTracker::Command::%%' . $_ } '**Core**', @{ **$config->{plugins}** } ], ); $class->name->new_with_options( { home => $self->home, config => $config, _currentproject => $self->project, } )->run; } =for newslide sub run { my $self = shift; my $config = $self->load_config; my $class = Moose::Meta::Class->create_anon_class( superclasses => ['App::TimeTracker'], roles => [ map { 'App::TimeTracker::Command::' . $_ } 'Core', @{ $config->{plugins} } ], ); @@$class->name@@->**new_with_options**( { home => $self->home, config => $config, _currentproject => $self->project, } )->%%run%%; } =for newslide What we've done here is to dynamically generate a new anonymous class based on the config that's valid for exactly this project. Now let's take a look on how the configuration is set up: =head3 Config File Gathering I'll spare you the code here, because there's nothing fancy happening. We start at the current working directory, and look for a file called C<.tracker.json>. We walk up the whole directory tree up to the root dir, looking for more files called C<.tracker.json> in each directory. All found config files are merged (using the wonderful C) At last, we merge a default config file located in C<~/.TimeTracker/tracker.json> =for newslide So for example, I always want to use the C plugin, in all my projects: **~/.TimeTracker**/tracker.json { "plugins":["SyncViaGit"] } =for newslide I keep all my job-related project in one directory, and there I define the plugins I always use for this job: ~/**validad**/.tracker.json { "plugins":["Git","RT"] } =for newslide I only to tell my colleagues that I work on a task when I'm working on one specific project: ~/validad/**Xaxos**/.tracker.json { "plugins":["Post2IRC"] } =for newslide Depending on where in the filesystem I call C, different config files are loaded. And thus different dynamic classes are created. Now we know how to figure out which dynamic class to load. But we also want to run it... =head2 Command Implementation $class->name->new_with_options( { home => $self->home, config => $config, _currentproject => $self->project, } )->**run**; We defined C<$class> to have the superclass C i.e. C<$class> is a subclass of C C is implemented there: =for newslide sub run { my $self = shift; my $command = 'cmd_'.($self->extra_argv->[0] || 'missing'); $self->cmd_commands unless $self->can($command); $self->_current_command($command); $self->$command; } =for newslide sub run { my $self = shift; my **$command** = 'cmd_'.($self->@@extra_argv->[0]@@ || 'missing'); $self->cmd_commands unless $self->can($command); $self->_current_command($command); $self->$command; } Get's the command name ('start') from ARGV. =for newslide sub run { my $self = shift; my $command = 'cmd_'.($self->extra_argv->[0] || 'missing'); $self->cmd_commands unless $self->can($command); $self->_current_command($command); @@$self@@->**$command**; } And calls a method named "cmd_COMMAND" ("cmd_start"). =for newslide Each command is implemented as a method provided by a role. The core commands (i.e. the ones that are always available) live in C. They are not very spectacular: =for newslide sub **cmd_start** { my $self = shift; $self->cmd_stop( 'no_exit' ); my $task = App::TimeTracker::Data::Task->new({ start => $self->at || $self->now, project => $self->project, tags => $self->tags, description => $self->description, }); $self->_current_task( $task ); $task->do_start( $self->home ); } =for newslide sub cmd_start { my $self = shift; $self->**cmd_stop**( 'no_exit' ); my $task = App::TimeTracker::Data::Task->new({ start => $self->at || $self->now, project => $self->project, tags => $self->tags, description => $self->description, }); $self->_current_task( $task ); $task->do_start( $self->home ); } =for newslide sub cmd_start { my $self = shift; $self->cmd_stop( 'no_exit' ); my **$task** = %%App::TimeTracker::Data::Task->new%%({ start => $self->at || $self->now, project => $self->project, tags => $self->tags, description => $self->description, }); $self->_current_task( $task ); $task->do_start( $self->home ); } =for newslide sub cmd_start { my $self = shift; $self->cmd_stop( 'no_exit' ); my $task = App::TimeTracker::Data::Task->new({ **start** => $self->@@at@@ || %%$self->now%%, project => $self->project, tags => $self->tags, description => $self->description, }); $self->_current_task( $task ); $task->do_start( $self->home ); } =for newslide sub cmd_start { my $self = shift; $self->cmd_stop( 'no_exit' ); my $task = App::TimeTracker::Data::Task->new({ start => $self->at || $self->now, project => $self->project, tags => $self->tags, description => $self->description, }); $self->_current_task( $task ); **$task**->@@do_start@@( $self->home ); } =for newslide sub cmd_start { my $self = shift; $self->cmd_stop( 'no_exit' ); my $task = App::TimeTracker::Data::Task->new({ start => $self->**at** || $self->now, project => $self->**project**, tags => $self->**tags**, description => $self->**description**, }); $self->_current_task( $task ); $task->do_start( $self->home ); } But as you can see here, commands also take options like 'at', 'project', 'tags', ... Let's take a look at how the options are defined and handled =head2 Option Handling package App::TimeTracker; with qw( MooseX::Getopt ); C makes it very easy to set the attributes you defined for your class via the command line. In fact, you don't have to do anything but applying the C role to your class. And init your object with C. =for newslide $class->name->**new_with_options**( { home => $self->home, config => $config, _currentproject => $self->project, } )->run; But for our purpose, not all options apply to all commands. Eg C and C need an option C<--at> ~$ tracker start --at 10:00 But other commands don't need this option. Maybe one command requires a specific option, but another command does not. The solution: more metaprogramming =for newslide Just like we can dynamically generate a class, we can also add methods and attributes to classes: =for newslide # App::TimeTracker::Proto sub **run** { my $self = shift; @@# stuff you've already seen@@ my %commands; foreach my $method ($class->get_all_method_names) { next unless $method =~ /^cmd_/; $method =~ s/^cmd_//; $commands{$method}=1; } my $load_attribs_for_command; foreach (@ARGV) { if ($commands{$_}) { $load_attribs_for_command='_load_attribs_'.$_; last; } } if ($load_attribs_for_command && $class->has_method($load_attribs_for_command)) { $class->name->$load_attribs_for_command($class); } } =for newslide Now this part is a little bit hacky... The problem is that in C we haven't parsed the command line yet. So we look through @ARGV to find something that looks like a registered command. =for newslide my %commands; foreach my $method (%%$class%%->**get_all_method_names**) { next unless $method =~ /^cmd_/; $method =~ s/^cmd_//; $commands{$method}=1; } =for newslide my %commands; foreach my $method ($class->get_all_method_names) { **next** unless $method =~ /^%%cmd_%%/; $method =~ s/^cmd_//; $commands{$method}=1; } =for newslide my %%%commands%%; foreach my $method ($class->get_all_method_names) { next unless $method =~ /^cmd_/; $method =~ s/^cmd_//; %%$commands%%{**$method**}=1; } =for newslide my $load_attribs_for_command; foreach (**@ARGV**) { if ($commands{$_}) { $load_attribs_for_command='_load_attribs_'.$_; last; } } =for newslide my $load_attribs_for_command; foreach (@ARGV) { if (**$commands{$_}**) { $load_attribs_for_command='_load_attribs_'.$_; last; } } If we find a command in C =for newslide my **$load_attribs_for_command**; foreach (@ARGV) { if ($commands{$_}) { **$load_attribs_for_command**='%%_load_attribs_%%'.@@$_@@; last; } } We calc a method name based on the command, which will be used to load the attributes for this command =for newslide if ( $load_attribs_for_command && **$class**->@@has_method@@(%%$load_attribs_for_command%%) ) { $class->name->$load_attribs_for_command($class); } =for newslide if ( $load_attribs_for_command && $class->has_method($load_attribs_for_command) ) { $class->name->%%$load_attribs_for_command($class)%%; } =for newslide But how do we now define the attributes? =for newslide sub **_load_attribs**_%%start%% { my ($class, $meta) = @_; $meta->add_attribute('at'=>{ isa=>'TT::DateTime', is=>'ro', coerce=>1, documentation=>'Start at', }); $meta->add_attribute('project'=>{ isa=>'Str', is=>'ro', documentation=>'Project name', lazy_build=>1, }); } =for newslide sub _load_attribs_start { my (**$class**, %%$meta%%) = @_; $meta->add_attribute('at'=>{ isa=>'TT::DateTime', is=>'ro', coerce=>1, documentation=>'Start at', }); =for newslide sub _load_attribs_start { my ($class, $meta) = @_; %%$meta%%->**add_attribute**('at'=>{ isa=>'TT::DateTime', is=>'ro', coerce=>1, documentation=>'Start at', }); =for newslide sub _load_attribs_start { my ($class, $meta) = @_; $meta->add_attribute('**at**'=>{ %%isa=>'TT::DateTime',%% %%is=>'ro',%% %%coerce=>1,%% %%documentation=>'Start at',%% }); same as @@has@@ '**at**' => ( %%isa=>'TT::DateTime',%% %%is=>'ro',%% %%coerce=>1,%% %%documentation=>'Start at',%% ); =for newslide So we now have a dynamically generated class based on some config values, with matching attributes that can be set via the command line. Nice. But one thing's missing: The plugins! =head2 Plugins As we're using Moose, it is just natural to use C as a mean to implement Plugin-Like behavior. my $class = Moose::Meta::Class->create_anon_class( superclasses => ['App::TimeTracker'], **roles** => [ map { 'App::TimeTracker::Command::' . $_ } 'Core', @{ $config->{plugins} } ], ); =for newslide One easy plugin is C. It makes it very easy to manage all your tracking files via git. You need to turn your C<~/.TimeTracker> directory into a git repo make it shareable using one of the many ways provided by git. And then you can say ~$ tracker sync To sync the repos =for newslide package **App::TimeTracker::Command::SyncViaGit**; use Moose::Role; use Git::Repository; sub cmd_sync { my $self = shift; my $r = Git::Repository->new( work_tree => $self->home ); my @new = $r->run('ls-files' =>'-om'); foreach my $changed (@new) { $r->run(add=>$changed); } $r->run(commit => '-m','synced on '.$self->now); foreach my $cmd (qw(pull push)) { my $c = $r->command( $cmd ); print $c->stderr->getlines; $c->close; } } =for newslide package App::TimeTracker::Command::SyncViaGit; use **Moose::Role**; use Git::Repository; =for newslide package App::TimeTracker::Command::SyncViaGit; use Moose::Role; use **Git::Repository**; =for newslide sub **cmd_sync** { my $self = shift; my $r = Git::Repository->new( work_tree => $self->home ); my @new = $r->run('ls-files' =>'-om'); foreach my $changed (@new) { $r->run(add=>$changed); } $r->run(commit => '-m','synced on '.$self->now); foreach my $cmd (qw(pull push)) { my $c = $r->command( $cmd ); print $c->stderr->getlines; $c->close; } } do git stuff, not interesting.. =for newslide C is a very simple plugin that B a new command But more often than that we want a plugin to B an existing command Eg, during C also make a git branch This is implemented using the wonderful Moose feature called method modifiers =head3 Method Modifiers package **App::TimeTracker::Command::Git**; use Moose::Role; use Git::Repository; has 'branch' => ( is=>'rw', isa=>'Str', documentation=>'Git: Branch name', ); after 'cmd_start' => sub { my $self = shift; return unless $self->branch; return if $self->no_branch; # set up a new git branch named after the value of --branch # implementation is boring }; =for newslide package App::TimeTracker::Command::Git; use **Moose::Role**; use **Git::Repository**; has 'branch' => ( is=>'rw', isa=>'Str', documentation=>'Git: Branch name', ); =for newslide package App::TimeTracker::Command::Git; use Moose::Role; use Git::Repository; @@has@@ '**branch**' => ( is=>'rw', isa=>'Str', documentation=>'Git: Branch name', ); This new attribute C will be available from the commandline, thanks to MooseX::Getopt. =for newslide **after** '@@cmd_start@@' => sub { my $self = shift; return unless $self->branch; return if $self->no_branch; # set up a new git branch named after the value of --branch # implementation is boring }; After running the regular C method, run this code. Which sets up a new git branch based on what we passed in via C<--branch> =for newslide Now we can say: ~/perl/App-TimeTracker$ tracker start --branch rework_config_parsing And get: Started working on App-TimeTracker at 17:43:02 Switched to a new branch 'rework_config_parsing' =for newslide But using a little bit more Moose power we can do even fancier stuff =head3 Introspection I told you earlier that there's a plugin to talk to RequestTracker (or RT) Given an RT ticket number, it can use the Web-API provided by RT to get information about the ticket and store that info in the tracking file It would be very cool if we can combine this with the C-plugin to get nice branch names (If both Plugins are active for the current project!) Which we can! =for newslide package **App::TimeTracker::Command::RT**; use Moose::Role; use RT::Client::REST; use Unicode::Normalize; has 'rt' => (is=>'rw',isa=>'TT::RT',coerce=>1,documentation=>'RT: Ticket number', predicate => 'has_rt'); before ['cmd_start','cmd_continue'] => sub { my $self = shift; # fetch the ticket name from RT and store it in $ticketname if ($self->meta->does_role('App::TimeTracker::Command::Git')) { my $branch = $ticketname; if ( $ticket ) { my $subject = $ticket->subject; $subject = NFKD($subject); # more $subject cleanup $branch .= '_'.$subject; } $self->branch($branch) unless $self->branch; } }; =for newslide package App::TimeTracker::Command::RT; use Moose::Role; use **RT::Client::REST**; use Unicode::Normalize; has 'rt' => ( is=>'rw', isa=>'TT::RT', coerce=>1, documentation=>'RT: Ticket number', predicate => 'has_rt' ); =for newslide package App::TimeTracker::Command::RT; use Moose::Role; use RT::Client::REST; use Unicode::Normalize; **has** '@@rt@@' => ( is=>'rw', isa=>'TT::RT', coerce=>1, documentation=>'RT: Ticket number', predicate => 'has_rt' ); So we can say C<--rt 1234> =for newslide @@before@@ ['**cmd_start**','**cmd_continue**'] => sub { my $self = shift; # fetch the ticket name from RT and store it in $ticketname if ($self->meta->does_role('App::TimeTracker::Command::Git')) { my $branch = $ticketname; if ( $ticket ) { my $subject = $ticket->subject; $subject = NFKD($subject); # more $subject cleanup $branch .= '_'.$subject; } $self->branch($branch) unless $self->branch; } }; =for newslide before ['cmd_start','cmd_continue'] => sub { my $self = shift; # @@fetch the ticket name from RT and store it in@@ **$ticketname** if ($self->meta->does_role('App::TimeTracker::Command::Git')) { my $branch = $ticketname; if ( $ticket ) { my $subject = $ticket->subject; $subject = NFKD($subject); # more $subject cleanup $branch .= '_'.$subject; } $self->branch($branch) unless $self->branch; } }; =for newslide before ['cmd_start','cmd_continue'] => sub { my $self = shift; # fetch the ticket name from RT and store it in $ticketname if ($self->@@meta@@->%%does_role%%('**App::TimeTracker::Command::Git**')) { my $branch = $ticketname; if ( $ticket ) { my $subject = $ticket->subject; $subject = NFKD($subject); # more $subject cleanup $branch .= '_'.$subject; } $self->branch($branch) unless $self->branch; } }; =for newslide before ['cmd_start','cmd_continue'] => sub { my $self = shift; # fetch the ticket name from RT and store it in $ticketname if ($self->meta->does_role('App::TimeTracker::Command::Git')) { my **$branch** = @@$ticketname@@; # %%RT1234%% if ( $ticket ) { my $subject = $ticket->subject; $subject = NFKD($subject); # more $subject cleanup $branch .= '_'.$subject; } $self->branch($branch) unless $self->branch; } }; =for newslide before ['cmd_start','cmd_continue'] => sub { my $self = shift; # fetch the ticket name from RT and store it in $ticketname if ($self->meta->does_role('App::TimeTracker::Command::Git')) { my $branch = $ticketname; if ( $ticket ) { my $subject = $ticket->subject; $subject = NFKD($subject); # more $subject cleanup **$branch** .= '_'.@@$subject@@; # %%RT1234_update_config%% } $self->branch($branch) unless $self->branch; } }; =for newslide before ['cmd_start','cmd_continue'] => sub { my $self = shift; # fetch the ticket name from RT and store it in $ticketname if ($self->meta->does_role('App::TimeTracker::Command::Git')) { my $branch = $ticketname; if ( $ticket ) { my $subject = $ticket->subject; $subject = NFKD($subject); # more $subject cleanup $branch .= '_'.$subject; } $self->@@branch@@(**$branch**) unless $self->branch; } }; =for newslide And now we finally can say: ~/perl/App-TimeTracker$ tracker start --rt 1234 And get: Started working on App-TimeTracker at 17:43:02 Switched to a new branch 'RT1234_rework_config_parsing' =head2 Wrap up C does all of this and a bit more like talking to IRC (or some other service) sending notification to your window manager reporting =for newslide and it could do a lot more, if somebody feels a certain need and writes a plugin. The code is on github: https://github.com/domm/App-TimeTracker And on CPAN: http://search.cpan.org/dist/App-TimeTracker/ =for newslide And there even is a project website: http://timetracker.plix.at =for newslide If you're interested in adding a command line interface like this to your code check out MooseX::App Maroš Kollar which implements a generalized version of the concepts I just presented =for newslide Use it! Questions?