ACME-2026
view release on metacpan or search on metacpan
lib/ACME/2026.pm view on Meta::CPAN
=head1 DATA MODEL
Plan hashref:
{
title => '2026',
items => [ ... ],
next_id => 1,
created_at => '2026-01-01T12:00:00Z',
updated_at => '2026-01-01T12:00:00Z',
storage => '2026.json',
autosave => 1,
}
Item hashref:
{
id => 1,
title => 'Run a marathon',
status => 'todo',
list => 'Health',
tags => ['fitness'],
priority => 2,
due => '2026-10-01',
notes => [ { note => 'Signed up', at => '2026-02-10T09:00:00Z' } ],
created_at => '2026-01-01T12:00:00Z',
updated_at => '2026-02-10T09:00:00Z',
}
Status values are C<todo>, C<done>, or C<skipped>. Dates are ISO 8601 strings
(C<YYYY-MM-DD> or C<YYYY-MM-DDTHH:MM:SSZ>).
=head1 FUNCTIONS
=head2 plan_new
my $plan = plan_new(%opts);
lib/ACME/2026.pm view on Meta::CPAN
=head2 items
my @items = items($plan, %filters);
Filters items with any of:
status, list, tag, tags, priority, min_priority, max_priority,
due_before, due_after, sort
For C<tag> or C<tags>, any matching tag is enough. C<sort> supports:
C<due>, C<priority>, C<created>, C<updated>, or C<title>. Prefix with C<->
for descending order.
=head2 stats
my $stats = stats($plan, %filters);
Returns a hashref with C<total>, C<todo>, C<done>, C<skipped>, and
C<complete_pct>.
=cut
sub plan_new {
my %opts = _normalize_opts(@_);
my $now = _now();
my $plan = {
title => defined $opts{title} ? $opts{title} : '2026',
items => [],
next_id => 1,
created_at => $now,
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;
lib/ACME/2026.pm view on Meta::CPAN
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,
due => $opts{due},
notes => [],
created_at => $now,
updated_at => $now,
};
push @{ $plan->{items} }, $item;
if (defined $opts{note}) {
_add_note($plan, $item, $opts{note});
} else {
_touch($plan);
}
_maybe_autosave($plan);
lib/ACME/2026.pm view on Meta::CPAN
$changed = 1;
}
if (exists $attrs{tags} || exists $attrs{tag}) {
$item->{tags} = _normalize_tags($attrs{tags}, $attrs{tag});
$changed = 1;
}
return $item unless $changed;
$item->{updated_at} = _now();
_touch($plan);
_maybe_autosave($plan);
return $item;
}
sub delete_item {
my ($plan, $id) = @_;
_ensure_plan($plan);
lib/ACME/2026.pm view on Meta::CPAN
sub _set_status {
my ($plan, $id, $status, %opts) = @_;
_ensure_plan($plan);
_reject_unknown('_set_status', \%opts, qw(note));
my $item = _find_item($plan, $id);
croak "No item with id $id" unless $item;
$item->{status} = _normalize_status($status);
$item->{updated_at} = _now();
if (defined $opts{note}) {
_add_note($plan, $item, $opts{note});
} else {
_touch($plan);
}
_maybe_autosave($plan);
return $item;
}
lib/ACME/2026.pm view on Meta::CPAN
next unless ref $item eq 'HASH';
if (!defined $item->{id}) {
$item->{id} = $next_id++;
}
$item->{status} = _normalize_status($item->{status});
$item->{tags} = _normalize_tags($item->{tags});
$item->{notes} = _normalize_notes($item->{notes});
$item->{priority} = defined $item->{priority} ? $item->{priority} : 3;
$item->{list} = defined $item->{list} ? $item->{list} : 'General';
$item->{created_at} = _now() unless defined $item->{created_at};
$item->{updated_at} = $item->{created_at} unless defined $item->{updated_at};
}
$plan->{next_id} = $next_id if $next_id > $plan->{next_id};
$plan->{created_at} = _now() unless defined $plan->{created_at};
$plan->{updated_at} = $plan->{created_at} unless defined $plan->{updated_at};
return $plan;
}
sub _normalize_status {
my ($status) = @_;
$status = 'todo' if !defined $status || $status eq '';
return $status if $status eq 'todo' || $status eq 'done' || $status eq 'skipped';
croak "Unknown status '$status'";
}
lib/ACME/2026.pm view on Meta::CPAN
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) = @_;
lib/ACME/2026.pm view on Meta::CPAN
} @$items;
}
if ($sort eq 'created') {
return sort {
my $cmp = ($a->{created_at} || '') cmp ($b->{created_at} || '');
$desc ? -$cmp : $cmp;
} @$items;
}
if ($sort eq 'updated') {
return sort {
my $cmp = ($a->{updated_at} || '') cmp ($b->{updated_at} || '');
$desc ? -$cmp : $cmp;
} @$items;
}
if ($sort eq 'title') {
return sort {
my $cmp = lc($a->{title} || '') cmp lc($b->{title} || '');
$desc ? -$cmp : $cmp;
} @$items;
}
script/acme2026 view on Meta::CPAN
warn "$msg\n";
}
print <<'USAGE';
Usage:
acme2026 add "Title" [--list NAME] [--tag TAG ...] [--priority N] [--due YYYY-MM-DD] [--note TEXT] [--file PATH]
acme2026 complete ID [--note TEXT] [--file PATH]
acme2026 list [--status todo|done|skipped] [--list NAME] [--tag TAG ...] [--sort FIELD] [--file PATH]
Options:
--file, -f Path to JSON storage (default: 2026.json or $ACME_2026_FILE)
--sort One of: due, -due, priority, -priority, created, updated, title, -title
--help, -h Show this help
USAGE
exit($msg ? 1 : 0);
}
sub default_file {
return $ENV{ACME_2026_FILE} || '2026.json';
}
sub load_plan {
( run in 0.748 second using v1.01-cache-2.11-cpan-39bf76dae61 )