File Coverage

blib/lib/RecentInfo/Manager/XBEL.pm
Criterion Covered Total %
statement 148 157 94.2
branch 14 22 63.6
condition 12 26 46.1
subroutine 24 24 100.0
pod 1 7 14.2
total 199 236 84.3


line stmt bran cond sub pod time code
1             package RecentInfo::Manager::XBEL 0.04;
2 2     2   244286 use 5.020;
  2         8  
3 2     2   1192 use Moo 2;
  2         15901  
  2         11  
4 2     2   3762 use experimental 'signatures', 'postderef';
  2         4991  
  2         14  
5              
6             =head1 NAME
7              
8             RecentInfo::Manager::XBEL - manage recent documents XBEL files
9              
10             =cut
11              
12 2     2   1054 use XML::LibXML;
  2         24598  
  2         18  
13 2     2   1606 use XML::LibXML::PrettyPrint;
  2         18770  
  2         21  
14 2     2   1403 use IO::AtomicFile;
  2         5688  
  2         157  
15 2     2   1143 use Date::Format::ISO8601 'gmtime_to_iso8601_datetime';
  2         1488  
  2         137  
16 2     2   12 use List::Util 'first';
  2         3  
  2         120  
17 2     2   9 use File::Spec;
  2         5  
  2         118  
18 2     2   11 use File::Basename;
  2         3  
  2         133  
19              
20 2     2   1051 use RecentInfo::Entry;
  2         10  
  2         86  
21 2     2   1142 use RecentInfo::Application;
  2         8  
  2         79  
22 2     2   1105 use RecentInfo::GroupEntry;
  2         7  
  2         74  
23              
24 2     2   1191 use MIME::Detect;
  2         26448  
  2         4568  
25              
26             =head1 SYNOPSIS
27              
28             use RecentInfo::Manager::XBEL;
29             my $mgr = RecentInfo::Manager::XBEL->new();
30             $mgr->load();
31             $mgr->add('output.pdf');
32             $mgr->save();
33              
34             =cut
35              
36             has 'recent_path' => (
37             is => 'lazy',
38             default => sub { File::Spec->catfile( $ENV{ XDG_DATA_HOME }, 'recently-used.xbel' )},
39             );
40              
41             has 'app' => (
42             is => 'lazy',
43             default => sub { basename $0 },
44             );
45              
46             has 'exec' => (
47             is => 'lazy',
48             default => sub { sprintf "'%s %%u'", $_[0]->app },
49             );
50              
51             has 'entries' => (
52             is => 'lazy',
53             default => \&load,
54             );
55              
56 17     17 0 517 sub load( $self, $recent=$self->recent_path ) {
  17         27  
  17         326  
  17         92  
57 17 100 66     607 if( defined $recent && -f $recent && -s _ ) {
      100        
58 7         73 my $doc = XML::LibXML
59             ->new( load_ext_dtd => 0, keep_blanks => 1, expand_entities => 0, )
60             ->load_xml( location => $recent );
61 7         4573 return $self->_parse( $doc );
62             } else {
63 10         61 return [];
64             }
65             }
66              
67 8     8 0 230495 sub fromString( $self, $xml ) {
  8         13  
  8         14  
  8         11  
68 8         46 my $doc = XML::LibXML
69             ->new( load_ext_dtd => 0, keep_blanks => 1, expand_entities => 0, )
70             ->load_xml( string => \$xml );
71 8         3090 return $self->_parse( $doc );
72             }
73              
74 15     15   36 sub _parse( $self, $doc ) {
  15         29  
  15         22  
  15         27  
75             # Just to make sure we read in valid(ish) data
76             #validate_xml( $doc );
77             # Parse our tree from the document, instead of using the raw XML
78             # as we want to try out the Perl class?!
79             # this means we lose comments etc.
80              
81             my @bookmarks = map {
82 15 100       68 if( $_->nodeType == XML_TEXT_NODE ) {
  36         5234  
83             # ignore
84             ()
85 24         60 } else {
86 12         87 RecentInfo::Entry->from_XML_fragment( $_ )
87             }
88             } $doc->getElementsByTagName('xbel')->[0]->childNodes()->get_nodelist;
89              
90 15         191 return \@bookmarks;
91             }
92              
93 3     3 0 5 sub find( $self, $href ) {
  3         7  
  3         7  
  3         4  
94             # This is case sensitive, which might be unexpected on Windows
95             # and case-insensitive filesystems in general...
96 3     2   143 first { $_->href eq $href } $self->entries->@*;
  2         110  
97             }
98              
99 3     3 0 9 sub add( $self, $filename, $info = {} ) {
  3         5  
  3         7  
  3         18  
  3         5  
100              
101 3   33     24 $info->{when} //= time();
102 3   33     87 $info->{app } //= $self->app;
103 3   33     80 $info->{exec} //= $self->exec;
104 3   33     45 $info->{visited} //= time();
105              
106 3 50       9 if( ! exists $info->{mime_type}) {
107 3         7 state $md = MIME::Detect->new();
108 3   50     383238 $info->{mime_type} = $md->mime_type_from_name($filename) // 'application/octet-stream';
109             };
110              
111 3         126171 $filename = File::Spec->rel2abs($filename);
112              
113             # Ugh - do we really want to do this?!
114 3         44 my $href = "file://$filename";
115              
116 3         10 my ($added, $modified);
117 3 50       12 if( $info->{modified}) {
118 0         0 $modified = gmtime_to_iso8601_datetime( $modified );
119             };
120 3 50       13 if( $info->{added}) {
121 0         0 $added = gmtime_to_iso8601_datetime( $added );
122             };
123              
124             # Take added from existing entry
125 3         23 my $when = gmtime_to_iso8601_datetime( $info->{when} );
126 3         176 my $mime_type = $info->{mime_type};
127 3         9 my $app = $info->{app};
128 3         6 my $exec = $info->{exec};
129              
130 3         18 my $res = $self->find($href);
131              
132 3 100       20 if(! $res) {
133 1   33     6 $added //= gmtime_to_iso8601_datetime( $info->{when} );
134 1   33     51 $modified //= gmtime_to_iso8601_datetime( $info->{when} );
135 1         38 $res = RecentInfo::Entry->new(
136             href =>"file://$filename",
137             mime_type => $mime_type,
138             added => $added,
139             modified => $modified,
140             visited => $when,
141             applications => [RecentInfo::Application->new( name => $app, exec => $exec, count => 1, modified => $when )],
142             groups => [RecentInfo::GroupEntry->new( group => $app )],
143             );
144 1         17 push $self->entries->@*, $res;
145             } else {
146 2 50       8 $res->added($added) if $added;
147 2 50       6 $res->modified($modified) if $modified;
148 2         11 $res->visited($when);
149             # Check if we are in the group, otherwise add ourselves
150              
151 2 50       9 if(! grep { $_->group eq $app } $res->groups->@*) {
  2         17  
152 0         0 push $res->groups->@*, RecentInfo::GroupEntry->new( group => $app );
153             };
154 2 50       7 if(! grep { $_->name eq $app } $res->applications->@*) {
  2         13  
155             push $res->applications->@*,
156 0         0 RecentInfo::Application->new( name => $app, exec => $info->{exec}, count => 1, modified => $when )
157             } else {
158             # Update our most recent entry in ->applications
159 2     2   14 my $app_entry = first { $_->name eq $app } $res->applications->@*;
  2         7  
160 2         10 $app_entry->modified($when);
161 2         11 $app_entry->count( $app_entry->count+1);
162             };
163             };
164              
165 3         63 $self->entries->@* = sort { $a->visited cmp $b->visited } $self->entries->@*;
  0         0  
166              
167 3         52 return $res
168             }
169              
170             =head2 C<< ->remove $filename >>
171              
172             $mgr->remove('output.pdf');
173              
174             Removes the filename from the list of recently used files.
175              
176             =cut
177              
178 2     2 1 3 sub remove( $self, $filename ) {
  2         3  
  2         4  
  2         3  
179 2         65 $filename = File::Spec->rel2abs($filename);
180              
181             # Ugh - do we really want to do this?!
182 2         5 my $href = "file://$filename";
183              
184 2         3 my $res;
185              
186             $self->entries->@* = map {
187 2 50       33 if( $_->href eq $href ) {
  1         71  
188 1         3 $res = $_;
189 1         18 (); # discard the item
190             } else {
191 0         0 say $_->href;
192 0         0 say $href;
193 0         0 $_; # keep the item
194             }
195             } $self->entries->@*;
196              
197 2         31 $self->entries->@* = sort { $a->visited cmp $b->visited } $self->entries->@*;
  0         0  
198              
199 2         20 return $res
200             }
201              
202 7     7 0 21 sub toString( $self ) {
  7         10  
  7         10  
203 7         58 my $doc = XML::LibXML::Document->new('1.0', 'UTF-8');
204 7         64 my $xbel = $doc->createElement('xbel');
205 7         32 $doc->setDocumentElement($xbel);
206 7         107 $xbel->setAttribute("version" => '1.0');
207 7         89 $xbel->setAttribute("xmlns:bookmark" => "http://www.freedesktop.org/standards/desktop-bookmarks");
208 7         345 $xbel->setAttribute("xmlns:mime" => "http://www.freedesktop.org/standards/shared-mime-info");
209 7         291 for my $bm ($self->entries->@*) {
210 5         47 $xbel->addChild($bm->as_XML_fragment( $doc ));
211             };
212              
213 7         259 my $pp = XML::LibXML::PrettyPrint->new(
214             indent_string => ' ',
215             element => {
216             compact => [qw[ bookmark:group ]],
217             },
218             );
219 7         203 $pp->pretty_print( $xbel );
220              
221             #validate_xml( $doc );
222              
223 7         11523 my $str = $doc->toString(); # so we encode some entities?!
224              
225             # Now hardcore encode some entities within attributes/double quotes back
226             # because I can't find how to coax XML::LibXML to properly encode entities:
227 7         358 $str =~ s!exec="'!exec="'!g;
228 7         62 $str =~ s!'"( |>)!'"$1!g;
229              
230 7         49 return $str
231             }
232              
233 3     3 0 5 sub save( $self, $filename=$self->recent_path ) {
  3         5  
  3         40  
  3         18  
234 3         9 my $str = $self->toString;
235 3         90 my $fh = IO::AtomicFile->open( $filename, '>:raw' );
236 3         940 print $fh $str;
237 3         15 $fh->close;
238             }
239              
240             1;
241             =head1 REPOSITORY
242              
243             The public repository of this module is
244             L.
245              
246             =head1 SUPPORT
247              
248             The public support forum of this module is L.
249              
250             =head1 BUG TRACKER
251              
252             Please report bugs in this module via Github
253             at L
254              
255             =head1 AUTHOR
256              
257             Max Maischein C
258              
259             =head1 COPYRIGHT (c)
260              
261             Copyright 2024-2024 by Max Maischein C.
262              
263             =head1 LICENSE
264              
265             This module is released under the same terms as Perl itself.
266              
267             =cut
268