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 )