ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/cvsroot/CV/bin/cv
Revision: 1.103
Committed: Mon Jun 19 21:58:40 2023 UTC (3 years, 4 months ago) by root
Branch: MAIN
Changes since 1.102: +3 -1 lines
Log Message:
*** empty log message ***

File Contents

# User Rev Content
1 root 1.1 #!/opt/bin/perl
2    
3 root 1.97 BEGIN {
4     # work around libraries (and perl) outputting numbers in weird formats
5     # when they shouldn't. this is somewhat brutal - better suggestions
6     # appreciated.
7     $ENV{LC_NUMERIC} = "C";
8     $ENV{LANG} = delete $ENV{LC_ALL} if exists $ENV{LC_ALL};
9     }
10    
11 root 1.103 use Gtk2; qw(-init -threads-init); BEGIN { Gtk2::Gdk::Threads->enter };
12    
13 root 1.98 BEGIN {
14     require Gtk2::CV::Plugin;
15     require "$ENV{HOME}/.cvrc" if -r "$ENV{HOME}/.cvrc";
16     }
17    
18 root 1.80 use common::sense;
19    
20 root 1.37 use Cwd ();
21     use Encode ();
22 root 1.72 use File::Glob ();
23 root 1.79 use Scalar::Util ();
24 root 1.37
25 root 1.12 use Gtk2::Gdk::Keysyms;
26    
27 root 1.48 use Gtk2::CV;
28    
29 root 1.12 use Gtk2::CV::ImageWindow;
30     use Gtk2::CV::Schnauzer;
31    
32 root 1.97 use Carp (); $Carp::MaxArgLen = 256;
33 root 1.82
34 root 1.48 use Gtk2::CV::Plugin::NameCluster;
35     use Gtk2::CV::Plugin::RCluster;
36 root 1.81 use Gtk2::CV::Plugin::PatRenamer;
37 root 1.96 use Gtk2::CV::Plugin::MetaCluster;
38 root 1.16
39 root 1.86 use AnyEvent::Fork::Template;
40    
41 root 1.87 Gtk2::CV::Jobber::set_template $AnyEvent::Fork::Template;
42    
43     # now we can initialize Gtk2 etc.
44     init Gtk2;
45 root 1.77
46 root 1.16 Gtk2::Rc->parse (Gtk2::CV::find_rcfile "gtkrc");
47    
48 root 1.20 use File::Spec;
49    
50     my $mainwin;
51 root 1.12 my $viewer;
52 root 1.62 my $viewer_count;
53 root 1.12 my $schnauzer;
54 root 1.20 my $info;
55 root 1.21 my $help;
56 root 1.12
57 root 1.25 my $schnauzer_idx = 0;
58    
59 root 1.12 sub new_schnauzer {
60 root 1.20 my $s = new Gtk2::CV::Schnauzer;
61 root 1.12
62     $s->signal_connect_after (key_press_event => \&std_keys);
63 root 1.20 $s->signal_connect (activate => sub {
64     my $label = sprintf "%s (%d)",
65 root 1.75 (Glib::filename_display_name +(File::Spec->splitpath ($_[1]))[2]),
66 root 1.20 -s $_[1];
67     $info->set_label ($label);
68 root 1.62 $viewer->load_image ($_[1]) if $viewer; # TODO: error, or chose ANY viewer
69 root 1.20 });
70 root 1.12
71 root 1.40 Gtk2::CV::Plugin->call (new_schnauzer => $s);
72    
73 root 1.53 $s
74 root 1.12 }
75    
76 root 1.79 our %VIEWER; # global viewer container so we can propagate signals
77    
78     $SIG{USR1} = sub {
79     # I assume glib calls us in a safe enough context to create an idle watcher
80     add Glib::Idle sub {
81     $_->reload for values %VIEWER;
82     0
83     };
84     };
85    
86 root 1.62 sub new_viewer {
87     my $self = new Gtk2::CV::ImageWindow;
88    
89 root 1.79 Scalar::Util::weaken ($VIEWER{$self+0} = $self);
90    
91 root 1.62 $viewer_count++;
92    
93     $self->set_title ("CV: Image");
94    
95     $self->signal_connect (key_press_event => sub {
96     $viewer = $_[0];
97    
98     my $key = $_[1]->keyval;
99     my $state = $_[1]->state;
100    
101     if ($state * "control-mask" && $key == $Gtk2::Gdk::Keysyms{c}) {
102     my $viewer = new_viewer ();
103     $viewer->set_image ($_[0]->{image});
104     $viewer->show_all;
105     1
106     } else {
107     &std_keys
108     or $schnauzer->signal_emit (key_press_event => $_[1])
109     }
110     });
111 root 1.63 $self->signal_connect (delete_event => sub { $_[0]->destroy; 0 });
112 root 1.62 $self->signal_connect (destroy => sub {
113 root 1.79 delete $VIEWER{$_[0]+0};
114 root 1.62 $viewer = undef if $viewer == $_[0];
115    
116     main_quit Gtk2 unless --$viewer_count;
117 root 1.63
118     0
119 root 1.62 });
120    
121     $self->signal_connect (button3_press_event => sub {
122     $mainwin->visible
123     ? $mainwin->hide
124     : $mainwin->show_all;
125 root 1.63
126 root 1.62 1
127     });
128    
129     Gtk2::CV::Plugin->call (new_imagewindow => $self);
130    
131     $self
132     }
133    
134 root 1.12 sub std_keys {
135     my $key = $_[1]->keyval;
136     my $state = $_[1]->state;
137    
138 root 1.21 my $ctrl = $state * "control-mask";
139 root 1.12
140     if ($key == $Gtk2::Gdk::Keysyms{q}) {
141 root 1.62 $viewer->destroy;
142 root 1.12 } elsif ($ctrl && $key == $Gtk2::Gdk::Keysyms{v}) {
143 root 1.20 my $w = new Gtk2::Window;
144 root 1.26
145 root 1.66 $w->set_role ("schnauzer");
146 root 1.26 $w->set_title ("CV: Schnauzer");
147 root 1.20 $w->add (my $s = new_schnauzer);
148     $s->set_dir (File::Spec->curdir);
149 root 1.25 $s->set_geometry_hints;
150 root 1.21 $w->show_all;
151 root 1.25
152 root 1.21 } elsif ($ctrl && $key == $Gtk2::Gdk::Keysyms{h}) {
153     unless ($help) {
154 root 1.73 require Gtk2::Ex::PodViewer;
155 root 1.21
156     $help = new Gtk2::Window;
157 root 1.67 $help->set_role ("help");
158 root 1.26 $help->set_title ("CV: Help");
159 root 1.21 $help->set_default_size (500, 300);
160     $help->signal_connect (delete_event => sub { $help->hide; 1 });
161    
162     $help->add (my $sw = new Gtk2::ScrolledWindow);
163 root 1.73 $sw->add (my $h = new Gtk2::Ex::PodViewer);
164 root 1.21
165     #binmode DATA, ":utf8";
166     $h->load_string (do { local $/; <DATA> });
167     }
168    
169     $help->show_all;
170 root 1.83 } elsif (!$state && $Gtk2::Gdk::Keysyms{a} <= $key && $key <= $Gtk2::Gdk::Keysyms{z}) {
171 root 1.82 #
172 root 1.12 } else {
173 root 1.43 return 0;
174 root 1.12 }
175    
176 root 1.43 1
177 root 1.12 }
178    
179 root 1.98 &cvrc_boot if defined &cvrc_boot;
180    
181 root 1.20 {
182 root 1.62 $viewer = new_viewer;
183     $::cur_viewer = $viewer;
184 root 1.40
185 root 1.20 $schnauzer = new_schnauzer;
186    
187     $mainwin = new Gtk2::Window;
188 root 1.66 $mainwin->set_role ("main");
189 root 1.20 $mainwin->set_title ("CV");
190     $mainwin->add (my $vbox = new Gtk2::VBox);
191 root 1.53 $mainwin->signal_connect (delete_event => sub { $mainwin->hide; 1 });
192 root 1.20
193     $vbox->add ($schnauzer);
194 root 1.22 $vbox->pack_end (my $frame = new Gtk2::Frame, 0, 0, 0);
195 root 1.20 $frame->add (my $hbox = new Gtk2::HBox 0, 0);
196 root 1.45 $hbox->pack_start ((new Gtk2::Label "Info: "), 0, 0, 0);
197     $hbox->pack_end (my $labelwindow = new Gtk2::EventBox, 1, 1, 0);
198     $labelwindow->add ($info = new Gtk2::Label);
199     $labelwindow->signal_connect_after (size_request => sub { $_[1]->width (0); 0 });
200     $info->set (selectable => 1, xalign => 0, justify => "left");
201 root 1.25
202     $schnauzer->set_geometry_hints;
203 root 1.20 }
204 root 1.12
205     if (@ARGV) {
206 root 1.68 my $show_first = sub {
207     $schnauzer->show_all;
208 root 1.85
209     # activate first file, but avoid dirs
210     my $entry = $schnauzer->{entry}[0];
211     my $path = "$entry->[0]/$entry->[1]";
212    
213 root 1.89 $schnauzer->handle_key ($Gtk2::Gdk::Keysyms{space}, new Gtk2::Gdk::ModifierType [])
214 root 1.85 unless -d $path;
215    
216 root 1.68 $viewer->show_all;
217     };
218    
219 root 1.102 my $force_sort = $ARGV[0] eq "--sort" ? shift @ARGV : 0;
220    
221 root 1.71 if (@ARGV == 1 && $ARGV[0] eq "-0r") {
222     local $/;
223 root 1.102 $schnauzer->set_paths ([split /\x00/, <STDIN>], !$force_sort, $show_first);
224 root 1.71 } elsif (@ARGV == 1 && -d $ARGV[0]) {
225 root 1.74 $schnauzer->set_dir (shift, $show_first);
226 root 1.71 } else {
227 root 1.72 if ($ARGV[0] eq "-g") {
228     shift @ARGV;
229     @ARGV = map +(File::Glob::bsd_glob $_, File::Glob::GLOB_BRACE | File::Glob::GLOB_QUOTE), @ARGV;
230     }
231 root 1.102 $schnauzer->set_paths ([@ARGV], !$force_sort, $show_first);
232 root 1.71 }
233 root 1.17 } else {
234 root 1.68 $schnauzer->set_dir (File::Spec->curdir, sub {
235     $mainwin->show_all;
236     $viewer->show_all;
237     });
238 root 1.12 }
239    
240 root 1.98 &cvrc_start if defined &cvrc_start;
241    
242 root 1.12 main Gtk2;
243 root 1.21
244 root 1.69 Gtk2::CV::flush_aio;
245    
246 root 1.21 __DATA__
247 root 1.12
248 root 1.58 =encoding utf-8
249    
250 root 1.11 =head1 NAME
251    
252 root 1.47 cv - a fast gtk+ image viewer loosely modeled after XV
253 root 1.11
254     =head1 SYNOPSIS
255    
256 root 1.84 cv
257    
258     cv directory
259    
260     cv path...
261    
262     cv -g <glob expression...>
263    
264     find .. -print0 | cv -0r
265 root 1.11
266 root 1.102 cv --sort ...
267    
268 root 1.47 =head1 FEATURES
269    
270     CV is supposed to work similar to the venerable XV image viewer, just
271     faster. Why faster?
272    
273     =over 4
274    
275     =item * optimized directory scanning algorithm
276    
277 root 1.55 The directory scanning in CV uses some tricks that - on most modern
278 root 1.47 filesystems - makes it possible to detect filetypes faster than stat()'ing
279     every file. This makes CV suitable for directories with lots of files
280     (10000+).
281    
282     This algorithm is quite unprecise - it doesn't make a difference between
283     files, device nodes, symlinks and the like, and filetype detection is done
284     using the file extension only.
285    
286 root 1.55 On the positive side, it is usually many orders of magnitude faster than
287     traditional scanning techniques (good for directories with 10000 or
288     100000+ files).
289    
290 root 1.53 =item * queuing for all time-consuming background tasks
291    
292     All tasks, such as unlinking files or generating thumbnails, that can be
293     done in the background will be done so - no waiting required, even when
294     changing directories.
295    
296 root 1.47 =item * use of asynchronous I/O
297    
298     CV tries to use asynchronous I/O whereever it makes sense, for example
299 root 1.53 while scanning directories, waiting for stat data, unlinking files or
300     generating thumbnails. This usually decreases scanning times for large
301     directories a bit (especially on RAID devices and over NFS) and makes CV
302     much more interactive.
303 root 1.47
304     =item * fast image loading
305    
306     The time span between the user issuing a command and displaying the new
307     image should be as small as possible. CV uses optimized (especially
308     for JPEG) loading functions and sacrifices some quality (e.g no gamma
309 root 1.55 correction, although this might change) to achieve this speed.
310 root 1.47
311     =item * fast thumbnail creation
312    
313 root 1.55 Thumbnail creation uses both CPU and Disk-I/O. CV interleaves both, so
314     on modern CPUs, thumbnailing is usually limited by I/O speed. Thumbnail
315     creation for JPEGs has been specially optimized and can even take
316     advantage of multiple CPUs.
317 root 1.47
318     =item * minimum optical clutter
319    
320     CV has no menus or other user interface elements that take up a lot of
321 root 1.55 screen space (or are useful for beginning users). The schnauzer windows
322     can also be somewhat crowded.
323 root 1.47
324     The point of an image viewer is viewing images, not a nice GUI. This is
325     similar to XV's behaviour.
326    
327     =item * efficient (and hard to learn) user interface
328    
329     CV uses key combinations. A lot. If you are an experienced XV user, you
330     will find most of these keys familiar. If not, CV might be hard to use at
331     first, but will be an efficient tool later.
332    
333 root 1.55 =item * multi-window GUI
334    
335     CV doesn't force you to use a specific layout, instead it relies on your
336     window manager, thus enabling you to chose whatever layout that suits you
337     most.
338    
339 root 1.47 =item * i18n'ed filename handling throughout
340    
341     As long as glib can recognize your filename encoding (either UTF-8 or
342 root 1.55 locale-specific, depending on the setting of G_BROKEN_FILENAMES) and you
343     have the relevant fonts, CV will display your filenames correctly.
344 root 1.47
345     =item * extensible through plug-ins
346    
347     I have weird plug-ins that access remote databases to find a
348     directory. This is not likely to be of any use to other people. Likewise,
349     others might have weird requirements I cannot dream of.
350    
351     =item * filename clustering
352    
353     Among the standard plug-ins is a filename clustering plug-in, that (in
354     case of tens of thousands images in one directory) might be able to
355     cluster similar names together.
356    
357     =back
358    
359 root 1.11 =head1 DESCRIPTION
360    
361     =head2 THE IMAGE WINDOW
362    
363     You can use the following keys in the image window:
364    
365 root 1.50 q quit the program
366     < half the image size
367     > double the image size
368 root 1.101 , shrink the image by ~9% (opposite of .)
369 root 1.50 . enlarge the image by 10%
370     n reset to normal size
371     m maximize to screensize
372     M maximize to screensize, respecting image aspect
373     ctrl-m toggle maxpect-always mode
374 root 1.59 ctrl-sift-m toggle using current image size as max image size
375 root 1.50 u uncrop
376     r set scaling mode to 'nearest' (fastest)
377     s set scaling mode to 'bilinear' (default)
378     t rotate clockwise 90°
379     T rotate counterclockwise°
380 root 1.64 a apply all rotations loslessly to a jpeg file (using exiftran)
381 root 1.90 ctrl-shift-t apply current rotation for future image loads
382 root 1.50 ctrl-v open a new visual schnauzer window for the current dir
383 root 1.63 ctrl-c clone the current image window
384 root 1.50 ctrl-e run an editor ($CV_EDITOR or "gimp") on the current image
385 root 1.57 ctrl-p fire up the print dialog
386 root 1.88 ctrl-shift-p same as ctrl-p, but automatically selects "ok"
387 root 1.50 escape cancel a crop action
388 root 1.11
389 root 1.33 And when playing movies, these additional keys are active:
390    
391 root 1.50 left rewind by 10 seconds
392     right forward by 10 seconds
393     down rewind by 60 seconds
394     up forward by 60 seconds
395     pg_up rewind by 600 seconds
396     pg_down forward by 600 seconds
397     o toggle on-screen display
398     p pause/unpause
399     escape stop playing
400     9 turn volume down
401     0 turn volume up
402 root 1.33
403 root 1.47 Any other keys will be sent to the default schnauzer window, which can be
404     toggled on and off by right-clicking into the image window.
405 root 1.11
406 root 1.47 Left-clicking into the image window will let you crop the image (usually
407     to zoom into large images that CV scales down).
408 root 1.11
409     =head2 THE VISUAL SCHNAUZER
410    
411 root 1.63 Any image-loading action in a schnauzer window acts on the
412     "last-recently-activated" imagewindow, which currently is simply the last
413     image window that received a keypress.
414    
415 root 1.11 You can use the following keys in the schnauzer window:
416    
417 root 1.47 ctrl-space,
418 root 1.50 space move to and display next image
419 root 1.47 ctrl-backspace,
420 root 1.50 backspace move to and display previous image
421 root 1.47 ctrl-return,
422 root 1.50 return display selected picture, or enter directory
423 root 1.11
424 root 1.50 cursor keys move selection
425     page-up move one page up
426     page-down move one page down
427     home move to first file
428     end move to last file
429    
430     ctrl-a select all files
431     ctrl-shift-a select all files currently displayed in the schnauzer window
432     ctrl-d delete selected files WITHOUT ASKING AGAIN
433 root 1.91 ctrl-g force generation of thumbnails for the selected files
434     ctrl-shift-g remove thumbnails for the selected files
435 root 1.50 ctrl-s rescan current direcory or files updates/deletes etc.
436     ctrl-u update selected (or all) icons if neccessary
437 root 1.76 ctrl-- unselected thumbnailed images
438     ctrl-+ keep only thumbnailed images, deselect others
439 root 1.47
440 root 1.56 ^ go to parent directory (caret).
441    
442 root 1.103 currently disabled:
443 root 1.47 0-9,
444 root 1.50 a-z find the first filename beginning with this letter
445 root 1.47
446     Right-clicking into the schnauzer window displays a pop-up menu with
447     additional actions.
448    
449 root 1.60 =head3 SELECTION
450    
451     You can select entries in the Schnauzer in a variety of ways:
452    
453     =over 4
454    
455     =item Keyboard
456    
457     Moving the cursor with the keyboard will first deselect all files and then
458     select the file you moved to.
459    
460 root 1.61 =item Clicking
461 root 1.60
462 root 1.61 Clicking on an entry will select the one you clicked and deselect all
463     others.
464 root 1.60
465 root 1.61 =item Shift-Clicking
466    
467     Shift-clicking will toggle the selection on the entry under the mouse.
468    
469     =item Dragging
470 root 1.60
471     Dragging will select all entries between the one selected when pushing the
472     button and the one selected when releasing the button. If you move above
473     or below the schnauzer area while drag-selecting, the schnauzer will move
474     up/down one row twice per second. In addition, horizontal mouse movement
475     acts as a kind of invisible horizontal scrollbar.
476    
477     =item Hint: double-click works while click-selecting
478    
479     You can double-click any image while click-selecting to display it
480     without stopping the selection process. This will act as if you normally
481     double-clicked the image to display it, and will toggle the selection
482     twice, resulting in no change.
483    
484     =back
485    
486 root 1.47 =head1 FILES
487    
488     When starting, CV runs the F<.cvrc> file in your F<$HOME> directory as if
489     it were a perl script. in that, you will mostly load plug-ins.
490    
491     Example:
492    
493     system "fping -q -t 10 ether"
494     or require "/fs/cv/cvplugin.pl";
495    
496     This will load a plug-in, but only if the machine I<ether> is reachable
497     (supposedly the plug-in is networked in some way :).
498 root 1.11
499 root 1.79 =head1 ENVIRONMENT VARIABLES
500 root 1.27
501     =over 4
502    
503 root 1.38 =item CV_EDITOR
504    
505     The program that gets executed when the user presses C<CTRL-e> in the
506     Schnauzer or image window. The default is C<gimp>.
507    
508 root 1.70 =item CV_AUDIO_PLAYER
509    
510 root 1.94 EXPERIMENTAL: audio playback is now via mpv, this variable is currently
511     ignored.
512    
513 root 1.70 Program used to play all sorts of audio (wav, aif, mp3, ogg...), default "play".
514     Will be called like C<< $CV_AUDIO_PLAYER -- <path> >>.
515    
516 root 1.92 =item CV_MPLAYER
517    
518     Program used to play all sorts of video files. Unlike C<CV_AUDIO_PLAYER>,
519 root 1.99 this really must be some version of the C<mpv> programs, or something that
520     is very command-line compatible to them.
521 root 1.92
522 root 1.93 Note: for video-thumbnailing, mplayer is still used (and hardcoded).
523    
524 root 1.27 =item CV_PRINT_DESTINATION
525    
526     The default (perl-style) destination to use in the print dialog.
527    
528 root 1.38 =item CV_TRASHCAN
529    
530 root 1.78 When set, must point to a directory where all files that are deleted by
531     the "Delete Physically" (ctrl-d) action are moved to (other deletion
532     actions still delete!). If unset, files that are deleted are really being
533     deleted.
534 root 1.38
535 root 1.27 =back
536    
537 root 1.79 =head1 SIGNALS
538    
539     Sending CV a SIGUSR1 signal will cause all image viewers to reload the
540     currently loaded image. This is useful if you use CV as a viewer for
541     changing data - just run it in the background with some path and each time
542     the image changes, send it a SIGUSR1.
543    
544 root 1.23 =head1 SECURITY CONSIDERATIONS
545    
546 root 1.54 CV uses Pixbuf to load non-JPEG images. Pixbuf is not considered safe for
547     this purpose, though (from the gtk-2.2 release notes):
548 root 1.23
549     "While efforts have been made to make gdk-pixbuf robust against invalid
550     images, using gdk-pixbuf to load untrusted data is not recommended, due to
551     the likelyhood that there are additional problems where an invalid image
552     could cause gdk-pixbuf to crash or worse."
553    
554 root 1.11 =head1 BUGS/TODO
555 root 1.23
556 root 1.47 Lots of functionality is missing.
557    
558     Pixbuf doesn't always honor G_BROKEN_FILENAMES, so accessing files with
559 root 1.54 names incompatible with utf-8 might fail.
560 root 1.11
561     rotate on disk
562     lots of ui issues
563     save(?)
564     preferences
565    
566     =head1 AUTHOR
567    
568     Marc Lehmann <cv@plan9.de>.
569    
570     =cut
571 root 1.1