ACME-2026
view release on metacpan or search on metacpan
lib/ACME/2026.pm view on Meta::CPAN
updated_at => $now,
storage => $opts{storage},
autosave => $opts{autosave} ? 1 : 0,
};
return $plan;
}
sub plan_load {
my ($path, %opts) = @_;
croak 'plan_load requires a path' unless defined $path && length $path;
my $json = _read_file($path);
my $data = eval { JSON::PP->new->decode($json) };
croak "Failed to decode JSON from $path: $@" if $@;
_normalize_plan($data);
$data->{storage} = $path;
$data->{title} = $opts{title} if exists $opts{title};
$data->{autosave} = $opts{autosave} ? 1 : 0 if exists $opts{autosave};
return $data;
}
sub plan_save {
my ($plan, $path) = @_;
_ensure_plan($plan);
$path ||= $plan->{storage};
croak 'plan_save requires a path or plan storage' unless defined $path && length $path;
_normalize_plan($plan);
my $encoder = JSON::PP->new->canonical(1)->pretty(1);
my $json = $encoder->encode($plan);
_write_file_atomic($path, $json);
return 1;
}
lib/ACME/2026.pm view on Meta::CPAN
my ($title, %opts);
if (@args % 2 == 1) {
$title = shift @args;
%opts = @args;
} else {
%opts = @args;
$title = $opts{title};
}
croak 'add_item requires a title' unless defined $title && length $title;
_reject_unknown('add_item', \%opts, qw(title list tags tag priority due note));
my $now = _now();
my $item = {
id => $plan->{next_id}++,
title => $title,
status => 'todo',
list => defined $opts{list} ? $opts{list} : 'General',
tags => _normalize_tags($opts{tags}, $opts{tag}),
priority => defined $opts{priority} ? $opts{priority} : 3,
lib/ACME/2026.pm view on Meta::CPAN
sub get_item {
my ($plan, $id) = @_;
_ensure_plan($plan);
return _find_item($plan, $id);
}
sub add_note {
my ($plan, $id, $note) = @_;
_ensure_plan($plan);
croak 'add_note requires a note' unless defined $note && length $note;
my $item = _find_item($plan, $id);
croak "No item with id $id" unless $item;
_add_note($plan, $item, $note);
_maybe_autosave($plan);
return $item;
}
lib/ACME/2026.pm view on Meta::CPAN
} else {
push @tags, $filters{tags};
}
}
if (@tags) {
@items = grep {
my %item_tags = map { $_ => 1 } @{ $_->{tags} || [] };
my $match = 0;
for my $tag (@tags) {
next unless defined $tag && length $tag;
if ($item_tags{$tag}) {
$match = 1;
last;
}
}
$match;
} @items;
}
if (defined $filters{priority}) {
lib/ACME/2026.pm view on Meta::CPAN
sub _normalize_opts {
return %{ $_[0] } if @_ == 1 && ref $_[0] eq 'HASH';
return @_;
}
sub _normalize_plan {
my ($plan) = @_;
_ensure_plan($plan);
$plan->{title} = '2026' unless defined $plan->{title} && length $plan->{title};
$plan->{items} = [] unless ref $plan->{items} eq 'ARRAY';
$plan->{autosave} = $plan->{autosave} ? 1 : 0;
my $max_id = 0;
for my $item (@{ $plan->{items} }) {
next unless ref $item eq 'HASH';
$max_id = $item->{id} if defined $item->{id} && $item->{id} > $max_id;
}
$plan->{next_id} = $plan->{next_id} || ($max_id + 1);
lib/ACME/2026.pm view on Meta::CPAN
if (defined $tags) {
if (ref $tags eq 'ARRAY') {
@tags = @$tags;
} else {
@tags = ($tags);
}
}
push @tags, $tag if defined $tag;
@tags = grep { defined $_ && length $_ } @tags;
return \@tags;
}
sub _normalize_notes {
my ($notes) = @_;
return [] unless defined $notes;
if (ref $notes eq 'ARRAY') {
my @out;
for my $note (@$notes) {
if (ref $note eq 'HASH') {
lib/ACME/2026.pm view on Meta::CPAN
return unless defined $id;
for my $item (@{ $plan->{items} || [] }) {
next unless defined $item->{id};
return $item if $item->{id} == $id;
}
return;
}
sub _add_note {
my ($plan, $item, $note) = @_;
return unless defined $note && length $note;
push @{ $item->{notes} }, { note => $note, at => _now() };
$item->{updated_at} = _now();
_touch($plan);
}
sub _touch {
my ($plan) = @_;
$plan->{updated_at} = _now();
}
sub _maybe_autosave {
my ($plan) = @_;
return unless $plan->{autosave};
plan_save($plan);
}
sub _sort_items {
my ($items, $sort) = @_;
return @$items unless defined $sort && length $sort;
my $desc = ($sort =~ s/^-//);
if ($sort eq 'due') {
return sort {
my $ad = defined $a->{due} ? $a->{due} : ($desc ? '0000-00-00' : '9999-12-31');
my $bd = defined $b->{due} ? $b->{due} : ($desc ? '0000-00-00' : '9999-12-31');
my $cmp = $ad cmp $bd;
$desc ? -$cmp : $cmp;
} @$items;
lib/ACME/2026.pm view on Meta::CPAN
sub _write_file_atomic {
my ($path, $content) = @_;
my ($fh, $tmp) = tempfile('acme2026-XXXXXX', DIR => _temp_dir($path));
print {$fh} $content or croak "Unable to write $tmp: $!";
close $fh or croak "Unable to close $tmp: $!";
rename $tmp, $path or croak "Unable to move $tmp to $path: $!";
}
sub _temp_dir {
my ($path) = @_;
return '.' unless defined $path && length $path;
if ($path =~ /[\/\\]/) {
$path =~ s/[\/\\][^\/\\]+$//;
return length $path ? $path : '.';
}
return '.';
}
=head1 AUTHOR
Will Willis <wwillis@cpan.org>
=head1 BUGS
script/acme2026 view on Meta::CPAN
'list|l=s' => \$list,
'tag|t=s@' => \@tags,
'priority|p=i' => \$priority,
'due|d=s' => \$due,
'note|n=s' => \$note,
'help|h' => \$help,
) or usage('Invalid options for add');
usage() if $help;
my $title = shift @ARGV;
usage('add requires a title') unless defined $title && length $title;
my $plan = load_plan($file);
my %opts;
$opts{list} = $list if defined $list;
$opts{priority} = $priority if defined $priority;
$opts{due} = $due if defined $due;
$opts{note} = $note if defined $note;
$opts{tags} = \@tags if @tags;
my $id = add_item($plan, $title, %opts);
script/acme2026 view on Meta::CPAN
$filters{sort} = $sort if defined $sort;
my @items = items($plan, %filters);
if (!@items) {
print "No items\n";
exit 0;
}
for my $item (@items) {
my $tags = $item->{tags} && @{ $item->{tags} } ? join(',', @{ $item->{tags} }) : '-';
my $due = defined $item->{due} && length $item->{due} ? $item->{due} : '-';
my $line = join("\t",
$item->{id},
$item->{status},
$item->{list} || 'General',
$due,
'p' . ($item->{priority} || 0),
$tags,
$item->{title},
);
print "$line\n";
( run in 1.696 second using v1.01-cache-2.11-cpan-39bf76dae61 )