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 )