#!/usr/bin/perl # # gwp.pl-v1.0 # gwp is a Gtk2-Perl wallpaper setter and browser, similar to nitrogen, # but it will also set on a timer. # # http://www.gozer.org/programs/perl-gtk/gwp.php # # Requirements: # Perl - http://www.perl.org/ --- tested with 5.10.1 # Gtk2-Perl - http://gtk2-perl.sourceforge.net/ --- tested with 1.221 # # Files Created: # ~/.gwprc (if settings are saved in the Preferences window) # # Usage: # gwp.pl # # Examples: # gwp.pl & # gwp.pl --delay=3600 -r ~/lib/wp/db ~/lib/wp/windows_7 & # gwp.pl --help # # Remote control: # gwp.pl --next, --prev, --exit, --show, see --help for more # # Copyright (c) 2010 Mike Hokenson # This application is free software; you can redistribute it and/or # modify it under the same terms as Perl itself. use Gtk2 -init; use Getopt::Long; use strict; # config file (GKeyFile) my $config = $ENV{HOME} . "/.gwprc"; # misc options my $one_image; # image directories my @directories; # save a copy of @ARGV for --restart (GetOptions chews it up) my @_ARGV = @ARGV; # load config file load_config($config); # allow combining short options (-ras vs -r -a -s) Getopt::Long::Configure("bundling"); # override config with command line options GetOptions('c|command=s' => sub { set_command($_[1]); }, 'd|delay=i' => sub { set_delay($_[1]); }, 'r|recursive' => sub { set_recursive(1); }, 'no-recursive' => sub { set_recursive(0); }, 'a|random' => sub { set_random(1); }, 'no-random' => sub { set_random(0); }, 's|screensaver' => sub { set_screensaver(1); }, 'no-screensaver' => sub { set_screensaver(0); }, 'thumb-size=s' => sub { set_thumb_size($_[1]); }, '1|one' => \$one_image, 'next' => \&send_remote, 'prev' => \&send_remote, 'previous' => sub { send_remote("prev"); }, 'pause' => \&send_remote, 'stop' => \&send_remote, 'cont' => \&send_remote, 'continue' => sub { send_remote("cont"); }, 'reload' => \&send_remote, 'restart' => \&send_remote, 'show' => \&send_remote, 'open' => sub { send_remote("show"); }, 'browser' => sub { send_remote("show"); }, 'prefs' => \&send_remote, 'preferences' => sub { send_remote("prefs"); }, 'settings' => sub { send_remote("prefs"); }, 'exit' => \&send_remote, 'x|kill' => sub { send_remote("exit"); }, 'shutdown' => sub { send_remote("exit"); }, 'h|help' => sub { show_help(); exit(0); }) or exit(1); # override with command line if (@ARGV) { @directories = @ARGV; } if (my $pid = running_instance()) { message("existing instance found ($pid), see --help for remote commands or --exit to terminate"); exit(1); } if ($one_image) { set_random(1); update_images(); set_image(); exit(0); } $SIG{QUIT} = $SIG{TERM} = $SIG{INT} = $SIG{HUP} = sub { Gtk2->main_quit; }; xorg_set_win(); xorg_set_pid(); # remote command processing Gtk2::Gdk::Event->handler_set(\&xorg_event_handler); update_images(); # need to run this before main exits Gtk2->quit_add(Gtk2->main_level, \&xorg_cleanup); if (@directories) { set_image(); start_mainloop(); } else { open_browser(); open_preferences(); open_welcome(); } Gtk2->main; exit(0); # -- configuration ------------------------------------------------------------ my %config; sub set_command { my ($val) = @_; if (defined($val) && $val) { $config{command} = $val; } else { $config{command} = get_default_command(); } } sub get_command { unless (defined($config{command})) { set_command(); } return $config{command}; } sub set_delay { my ($val) = @_; if (defined($val) && $val >= 0 && $val <= 86400) { $config{delay} = $val; } else { $config{delay} = 1800; } } sub get_delay { unless (defined($config{delay})) { set_delay(); } return $config{delay}; } sub set_recursive { my ($val) = @_; if (defined($val)) { $config{recursive} = $val; } else { $config{recursive} = 0; } } sub get_recursive { unless (defined($config{recursive})) { set_recursive(); } return $config{recursive}; } sub set_random { my ($val) = @_; if (defined($val)) { $config{random} = $val; } else { $config{random} = 0; } } sub get_random { unless (defined($config{random})) { set_random(); } return $config{random}; } sub set_screensaver { my ($val) = @_; if (defined($val)) { $config{screensaver} = $val; } else { $config{screensaver} = 0; } } sub get_screensaver { unless (defined($config{screensaver})) { set_screensaver(); } return $config{screensaver}; } my ($thumb_w, $thumb_h); sub set_thumb_size { my ($size) = @_; unless ($size) { $size = $config{thumb_size}; } my ($w, $h) = (128, 96); # default to 4:3 if ($size && $size =~ /^(\d+)x(\d+)$/) { $w = $1; if ($w < 16 || $w > 256) { $w = 128; } $h = $2; if ($h < 16 || $h > 256) { $h = 128; } } $config{thumb_size} = $w . "x" . $h; $thumb_w = $w; $thumb_h = $h; } sub get_thumb_size { unless (defined($thumb_w) && defined($thumb_h)) { set_thumb_size(); } return ($thumb_w, $thumb_h); } # -- configuration file ------------------------------------------------------- sub get_key { my ($key_file, $func, $group_name, $key) = @_; return eval { $key_file->$func($group_name, $key); } } sub load_config { my ($file) = @_; unless (-f $file) { return; } my $key_file = Glib::KeyFile->new; eval { $key_file->load_from_file($file, "none"); }; if ($key_file->has_group("main")) { if ($key_file->has_key("main", "command")) { my $val = get_key($key_file, "get_string", "main", "command"); set_command($val); } if ($key_file->has_key("main", "delay")) { my $val = get_key($key_file, "get_integer", "main", "delay"); set_delay($val); } if ($key_file->has_key("main", "recursive")) { my $val = get_key($key_file, "get_boolean", "main", "recursive"); set_recursive($val); } if ($key_file->has_key("main", "random")) { my $val = get_key($key_file, "get_boolean", "main", "random"); set_random($val); } if ($key_file->has_key("main", "screensaver")) { my $val = get_key($key_file, "get_boolean", "main", "screensaver"); set_screensaver($val); } if ($key_file->has_key("main", "directories")) { my @vals = get_key($key_file, "get_string_list", "main", "directories"); if (@vals) { @directories = @vals; } } } if ($key_file->has_group("browser")) { if ($key_file->has_key("browser", "thumb-size")) { my $val = get_key($key_file, "get_string", "browser", "thumb-size"); set_thumb_size($val); } } } # -- image-related ------------------------------------------------------------ my @images; my $image_index = 0; sub get_image { my ($index) = @_; unless (defined($index)) { $index = $image_index; } #if ($index < 0 || $index > $#images) { # $index = 0; #} return $images[$index]; } my $set_image; # XXX: for set_image_if_index_changed, update_images_if_changed sub set_image { my ($index) = @_; my $image = get_image($index); if ($image && -f $image) { # XXX: bah! # no good if $image (or $command) contains shell characters like `'"! #system(get_command() . " \"$image\""); # ugly to do this every time, but caching the quoted image is overkill system(get_command() . " " . shell_quote($image)); # this is the cleanest way to execute a command, but would break if # someone entered "command -t 'special arg' -c". g_shell_parse_argv() # would be perfect here, but the Glib module doesn't have it ;/ #my @args = split(/\s+/, get_command()); #push(@args, $image); #system(@args); $set_image = $image; # update thumb if necessary unless (get_thumb_if_cached($image)) { Glib::Idle->add(sub { get_thumb($_[0]); return 0; }, $image); } } else { message("failed to get an image, check configuration"); } } # XXX: for open_browser() sub set_image_if_index_changed { my $image = get_image($image_index); if ($image && $set_image && $image ne $set_image) { set_image(); } } sub set_image_next { $image_index++; if ($image_index > $#images) { $image_index = 0; } set_image(); } sub set_image_prev { $image_index--; if ($image_index < 0) { $image_index = $#images; } set_image(); } # http://search.cpan.org/dist/String-ShellQuote/ sub shell_quote { my ($str) = @_; if ($str =~ /[^\w!%+,\-.\/:@^]/) { # ' -> '\'' $str =~ s/'/'\\''/g; # make multiple ' in a row look simpler # '\'''\'''\'' -> '"'''"' $str =~ s/((?:'\\''){2,})/q{'"} . (q{'} x (length($1) \/ 4)) . q{"'}/ge; $str = "'$str'"; $str =~ s/^''//; $str =~ s/''$//; } return $str; } sub get_dir_contents { my ($dir, $recursive, $contents, $last_updated) = @_; if ($dir && -d $dir) { opendir(DIR, $dir) or warn("opendir: $dir: $!\n"), return; my @dirents = readdir(DIR); closedir(DIR); my $l = (stat($dir))[9]; if ($l > $last_updated) { $$last_updated = $l; } foreach my $entry (@dirents) { if ($entry =~ /^\.{1,2}$/) { next; } my $path = $dir . "/" . $entry; if (-d $path) { if ($recursive) { get_dir_contents($path, $recursive, $contents, $last_updated); } } else { if (defined($contents)) { push(@$contents, $path); } } } } } sub get_last_update { my $last = 0; foreach my $dir (@directories) { get_dir_contents($dir, get_recursive(), undef, \$last); } return $last; } my $dirs_update = 0; sub update_images { @images = (); $image_index = 0; $dirs_update = 0; my @files; foreach my $dir (@directories) { get_dir_contents($dir, get_recursive(), \@files, \$dirs_update); } foreach (@files) { if (/\.(png|jpe?g?|tiff?)$/i) { push(@images, $_); } } if (@images && get_random()) { fisher_yates_shuffle(\@images); } } # should not be used when (directory) configuration changes are possible sub update_images_if_changed { # timestamp won't be any good for changes to @directories unless # $dirs_update is reset if ($#images < 0 || $dirs_update != get_last_update()) { update_images(); # try to maintain the current index if dir contents have changed if ($set_image) { for (my $i = 0; $i <= $#images; $i++) { my $image = get_image($i); if ($image eq $set_image) { $image_index = $i; last; } } } return 1; } return 0; } # http://docstore.mik.ua/orelly/perl/cookbook/ch04_18.htm sub fisher_yates_shuffle { my $array = shift; my $i; for ($i = @$array; --$i; ) { my $j = int rand ($i+1); next if $i == $j; @$array[$i,$j] = @$array[$j,$i]; } } # -- screensaver functions ---------------------------------------------------- sub xorg_get_screensaver_status { my $root = Gtk2::Gdk->get_default_root_window(); my ($type, $format, @data) = $root->property_get(Gtk2::Gdk::Atom->intern("_SCREENSAVER_STATUS", 0), Gtk2::Gdk::Atom->intern("INTEGER", 0), 0, 32, 0); # in case something else is using this and they have a different format return $#data == 2 ? @data : undef; } # xscreensaver-command -time # screen non-blanked since | screen blanked since | screen locked since # # gnome-screensaver-command -time # The screensaver is not currently active. # The screensaver has been active for %d seconds. # sub screensaver_active { if (get_screensaver()) { # xscreensaver, maybe others? my ($state, $time, $hack) = xorg_get_screensaver_status(); if (defined($state) && $time > 0 && defined($hack)) { if ($state != 0) { return 1; } } elsif (`pgrep gnome-screensaver`) { my $state = `gnome-screensaver-command -time 2>&1`; if ($state =~ / has been active for /) { return 1; } } elsif (`pgrep 'k(desktop|runner)_lock'`) { # ? return 1; } elsif (`pgrep xlock`) { return 1; } } return 0; } # -- mainloop ----------------------------------------------------------------- my $pause; sub mainloop { my $changed = update_images_if_changed(); unless ($pause || screensaver_active()) { set_image_next(); $changed++; } if ($changed) { refresh_browser(); } return 1; } my $timeout; sub start_mainloop { stop_mainloop(); if (get_delay()) { $timeout = Glib::Timeout->add(get_delay() * 1000, \&mainloop); } } sub stop_mainloop { if ($timeout) { Glib::Source->remove($timeout); $timeout = undef; } } # -- status / remote control -------------------------------------------------- my $control_window; sub xorg_get_win { unless ($control_window) { my $root = Gtk2::Gdk->get_default_root_window(); my ($type, $format, @data) = $root->property_get(Gtk2::Gdk::Atom->intern("_GWP_WINDOW", 1), Gtk2::Gdk::Atom->intern("WINDOW", 0), 0, 32, 0); my $xid = (@data) ? $data[0] : undef; if ($xid) { #$control_window = Gtk2::Gdk::Window->lookup($xid); $control_window = Gtk2::Gdk::Window->foreign_new($xid); } } return $control_window; } sub xorg_set_win { my ($window) = @_; unless ($window) { $window = Gtk2::Gdk::Window->new(undef, { window_type => 'toplevel', wclass => 'GDK_INPUT_ONLY', x => 0, y => 0, width => 1, height => 1}); } $control_window = $window; my $root = Gtk2::Gdk->get_default_root_window(); $root->property_change(Gtk2::Gdk::Atom->intern("_GWP_WINDOW", 0), Gtk2::Gdk::Atom->intern("WINDOW", 0), Gtk2::Gdk::ULONGS, "replace", $control_window->get_xid()); $control_window->set_events("property-change-mask"); } sub xorg_get_pid { my $window = xorg_get_win(); if ($window) { my ($type, $format, @data) = $window->property_get(Gtk2::Gdk::Atom->intern("_GWP_PID", 0), Gtk2::Gdk::Atom->intern("INTEGER", 0), 0, 16, 0); return (@data) ? $data[0] : undef; } } sub xorg_set_pid { my ($pid) = @_; my $window = xorg_get_win(); $window->property_change(Gtk2::Gdk::Atom->intern("_GWP_PID", 0), Gtk2::Gdk::Atom->intern("INTEGER", 0), Gtk2::Gdk::ULONGS, "replace", $pid ? $pid : $$); } sub xorg_get_cmd { my $window = xorg_get_win(); my ($type, $format, @data) = $window->property_get(Gtk2::Gdk::Atom->intern("_GWP_CMD", 1), Gtk2::Gdk::Atom->intern("STRING", 0), 0, 32, 0); return (@data) ? $data[0] : undef; } sub xorg_set_cmd { my ($cmd) = @_; my $window = xorg_get_win(); $window->property_change(Gtk2::Gdk::Atom->intern("_GWP_CMD", 0), Gtk2::Gdk::Atom->intern("STRING", 0), Gtk2::Gdk::CHARS, "replace", $cmd); # XXX: property rarely stays otherwise #while (Gtk2->events_pending) { # Gtk2->main_iteration; #} Gtk2::Gdk->flush(); } sub xorg_event_handler { my ($event) = @_; Gtk2->main_do_event($event); if ($event->window == xorg_get_win() && $event->type eq "property-notify" && $event->atom->name eq "_GWP_CMD") { my $cmd = xorg_get_cmd(); if ($cmd) { if ($cmd eq "next" || $cmd eq "prev") { update_images_if_changed(); if ($cmd eq "next") { set_image_next(); } else { set_image_prev(); } # reset timer start_mainloop(); refresh_browser(); } elsif ($cmd eq "pause") { $pause = !$pause; refresh_browser(); } elsif ($cmd eq "stop") { $pause = 1; refresh_browser(); } elsif ($cmd eq "cont") { $pause = 0; refresh_browser(); } elsif ($cmd eq "reload") { load_config($config); update_images(); set_image(); start_mainloop(); refresh_browser(); } elsif ($cmd eq "restart") { Gtk2->main_quit; unshift(@_ARGV, $0); exec { $_ARGV[0] } @_ARGV; } elsif ($cmd eq "show") { open_browser(); } elsif ($cmd eq "prefs") { open_preferences(); } elsif ($cmd eq "exit") { Gtk2->main_quit; } else { message("unhandled command \"$cmd\" (internal error)"); } } } } sub xorg_cleanup { my $root = Gtk2::Gdk->get_default_root_window(); $root->property_delete(Gtk2::Gdk::Atom->intern("_GWP_WINDOW", 0)); } sub running_instance { my $pid = xorg_get_pid(); return (defined($pid) && $pid > 1 && kill(0, $pid)) ? $pid : 0; } sub send_remote { my ($cmd) = @_; unless (running_instance()) { message("no existing instance found"); exit(1); } if ($cmd) { xorg_set_cmd($cmd); } exit(0); } # -- gui-related -------------------------------------------------------------- my %browser; sub open_browser { # draw attention to existing window if (%browser && $browser{window}) { ${$browser{window}}->window()->raise(); ${$browser{window}}->window()->focus(time()); update_images_if_changed(); refresh_browser(); return; } my $window = Gtk2::Window->new; $window->set_title("GWP: Wallpaper Browser"); $window->set_default_size(400, 425); $window->signal_connect(destroy => \&close_browser); $browser{window} = \$window; my $vbox = Gtk2::VBox->new(0, 1); $window->add($vbox); my $scrolled_window = Gtk2::ScrolledWindow->new; $scrolled_window->set_policy('automatic', 'automatic'); $vbox->pack_start($scrolled_window, 1, 1, 0); my $store = Gtk2::ListStore->new("Gtk2::Gdk::Pixbuf", "Glib::String"); my $tree_view = Gtk2::TreeView->new($store); $tree_view->set_headers_visible(0); $scrolled_window->add($tree_view); $browser{tree_view} = \$tree_view; my $rend = Gtk2::CellRendererPixbuf->new; $tree_view->insert_column_with_attributes(-1, "", $rend, pixbuf => 0); my $col = $tree_view->get_column(0); $col->set_sizing("autosize"); my $rend = Gtk2::CellRendererText->new; $tree_view->insert_column_with_attributes(-1, "Filename", $rend, text => 1); my $col = $tree_view->get_column(1); $col->set_sizing("autosize"); # XXX: the mouse is unresponsive for me during a big refresh (setting # lots of new thumbs), but the keyboard is alright... $tree_view->signal_connect(button_release_event => sub { my ($tree_view, $event) = @_; my ($path) = $tree_view->get_path_at_pos($event->x, $event->y); if ($path) { $tree_view->get_selection->select_path($path); } }); #$tree_view->get_selection->set_mode("single"); $tree_view->get_selection->signal_connect(changed => sub { my ($selection) = @_; my ($path) = $selection->get_selected_rows; if ($path) { my ($index) = $path->get_indices(); # selecting a new image if ($image_index != $index) { $image_index = $index; set_image($index); start_mainloop(); } else { # XXX: in case the index was lost during update_images_if_changed() # best the browser and the current wallpaper are in agreement # a bit of a catchall so checks don't have to be made all over set_image_if_index_changed(); } } }); my $hbox = Gtk2::HBox->new(1, 1); $vbox->pack_start($hbox, 0, 1, 0); my $tooltip = Gtk2::Tooltips->new; my $button = Gtk2::ToggleButton->new_with_label("Pause"); $tooltip->set_tip($button, "Pause timer"); $button->set_active($pause); $button->signal_connect(clicked => sub { $pause = $_[0]->get_active(); update_browser_statusbar(); }); $hbox->add($button); $browser{pause} = \$button; my $button = Gtk2::Button->new_from_stock('gtk-refresh'); $button->signal_connect(clicked => sub { update_images_if_changed(); refresh_browser(); }); $hbox->add($button); my $button = Gtk2::Button->new_from_stock('gtk-preferences'); $button->signal_connect(clicked => \&open_preferences); $hbox->add($button); my $button = Gtk2::Button->new_from_stock('gtk-close'); $button->signal_connect(clicked => \&close_browser); $hbox->add($button); my $statusbar = Gtk2::Statusbar->new; $vbox->pack_start($statusbar, 0, 1, 0); $browser{statusbar} = \$statusbar; $window->show_all; update_images_if_changed(); refresh_browser(); } sub update_browser_statusbar { my ($text) = @_; unless (%browser && $browser{statusbar}) { return; } unless ($text) { my $i = $#images + 1; my $d = $#directories + 1; $text = "$i wallpaper" . ($i == 1 ? "" : "s") . " in " . "$d director" . ($d == 1 ? "y" : "ies") . ", " . "auto-change " . ($timeout ? "every " . get_delay() . " seconds" . ($pause ? " (paused)" : "") : "disabled"); } ${$browser{statusbar}}->push(0, $text); } sub refresh_browser { unless (%browser && $browser{window}) { return; } trim_thumb_cache(); my $tree_view = ${$browser{tree_view}}; # if thumb size changed my $col = $tree_view->get_column(0); my ($rend) = $col->get_cell_renderers(); my ($w, $h) = get_thumb_size(); $rend->set_fixed_size($w + 2, $h + 2); my $store = $tree_view->get_model(); $store->clear(); my @thumbs; for (my $i = 0; $i <= $#images; $i++) { my $image = get_image($i); my $iter = $store->append; my $name = basename($image); my $thumb = get_thumb_if_cached($image); if ($thumb) { $store->set($iter, 0, $$thumb); } else { push(@thumbs, [ $image, $name, $iter ]); } $store->set($iter, 1, $name); if ($image_index == $i) { my $path = $store->get_path($iter); $tree_view->scroll_to_cell($path, undef, 1, 0.5, 0.0); $tree_view->get_selection->select_path($path); } } ${$browser{pause}}->set_active($pause); update_browser_statusbar(); if ($browser{worker}) { Glib::Source->remove($browser{worker}); $browser{worker} = undef; } # makes the window responsive (although not as much as gqview...) if (@thumbs) { $browser{worker} = Glib::Idle->add(sub { my ($store, $thumbs) = @{$_[0]}; my $entry = shift(@{$thumbs}); if ($entry) { my ($image, $name, $iter) = @{$entry}; update_browser_statusbar("Generating thumbnail for $name..."); my $thumb = get_thumb($image); if ($thumb) { $store->set($iter, 0, $$thumb); } return 1; } else { update_browser_statusbar(); return 0; } }, [ $store, \@thumbs ]); } } sub close_browser { if (%browser) { if ($browser{window}) { ${$browser{window}}->destroy; $browser{window} = undef; } $browser{statusbar} = undef; if ($browser{worker}) { Glib::Source->remove($browser{worker}); $browser{worker} = undef; } %browser = (); } } sub open_preferences { my $window = Gtk2::Window->new; $window->set_title("GWP: Preferences"); $window->signal_connect(destroy => sub { $window->destroy; }); my $tooltip = Gtk2::Tooltips->new; my $vbox = Gtk2::VBox->new(0, 5); $window->add($vbox); my $label = Gtk2::Label->new; $label->set_markup(" Main "); my $frame = Gtk2::Frame->new; $frame->set_label_widget($label); $vbox->add($frame); my $vbox2 = Gtk2::VBox->new(0, 0); $frame->add($vbox2); my $hbox = Gtk2::HBox->new(0, 5); my $label = Gtk2::Label->new("Wallpaper command:"); $tooltip->set_tip($label, "Command to set wallpaper"); $label->set_size_request(100, -1); #$label->set_alignment(0.0, 0.5); $hbox->pack_start($label, 0, 0, 0); my $command_entry = Gtk2::Entry->new; $tooltip->set_tip($command_entry, "default: " . get_default_command()); $command_entry->set_text(get_command()); $hbox->pack_start($command_entry, 1, 1, 0); $vbox2->pack_start($hbox, 0, 1, 0); my $hbox = Gtk2::HBox->new(0, 5); my $label = Gtk2::Label->new("Change interval:"); $tooltip->set_tip($label, "Delay in seconds between image change (0 disables)"); $label->set_size_request(100, -1); $hbox->pack_start($label, 0, 1, 0); my $delay_entry = Gtk2::SpinButton->new_with_range(0, 86400, 1); $tooltip->set_tip($delay_entry, "Delay in seconds between image change (0 disables)"); $delay_entry->set_value(get_delay()); $hbox->pack_start($delay_entry, 1, 1, 0); $vbox2->pack_start($hbox, 0, 1, 0); my $hbox = Gtk2::HBox->new(0, 5); my $label = Gtk2::Label->new("Recursive dirs:"); $tooltip->set_tip($label, "Recursively process image directories"); $label->set_size_request(100, -1); $hbox->pack_start($label, 0, 1, 0); my $recursive_entry = Gtk2::CheckButton->new; $tooltip->set_tip($recursive_entry, "Recursively process image directories"); $recursive_entry->set_active(get_recursive()); $hbox->pack_start($recursive_entry, 1, 1, 0); $vbox2->pack_start($hbox, 0, 1, 0); my $hbox = Gtk2::HBox->new(0, 5); my $label = Gtk2::Label->new("Randomize order:"); $tooltip->set_tip($label, "Randomize order of wallpapers"); $label->set_size_request(100, -1); $hbox->pack_start($label, 0, 1, 0); my $random_entry = Gtk2::CheckButton->new; $tooltip->set_tip($random_entry, "Randomize order of wallpapers"); $random_entry->set_active(get_random()); $hbox->pack_start($random_entry, 1, 1, 0); $vbox2->pack_start($hbox, 0, 1, 0); my $hbox = Gtk2::HBox->new(0, 5); my $label = Gtk2::Label->new("Idle for screensaver:"); $tooltip->set_tip($label, "Idle when screensaver is active"); $label->set_size_request(100, -1); $hbox->pack_start($label, 0, 1, 0); my $screensaver_entry = Gtk2::CheckButton->new; $tooltip->set_tip($screensaver_entry, "Idle when screensaver is active"); $screensaver_entry->set_active(get_screensaver()); $hbox->pack_start($screensaver_entry, 1, 1, 0); $vbox2->pack_start($hbox, 0, 1, 0); my @directory_entries; # always start with a minimum of 5 entries, allow room to grow if larger my $directories_max = $#directories + 3; if ($directories_max < 5) { $directories_max = 5; } for (my $i = 0; $i < $directories_max; $i++) { my $x = $i + 1; my $hbox = Gtk2::HBox->new(0, 5); my $label = Gtk2::Label->new("Wallpaper dir $x:"); $label->set_size_request(100, -1); $hbox->pack_start($label, 0, 1, 0); my $hbox2 = Gtk2::HBox->new(0, 0); $hbox->pack_start($hbox2, 0, 1, 0); my $entry = Gtk2::Entry->new; $tooltip->set_tip($entry, "Wallpaper directory #$x"); $entry->set_text($directories[$i]); $hbox2->pack_start($entry, 1, 1, 0); my $button = Gtk2::Button->new; my $image = Gtk2::Image->new_from_stock("gtk-open", "menu"); $button->add($image); $tooltip->set_tip($button, "Browse for directory"); $button->signal_connect(clicked => sub { my ($button, $entry) = @_; my $chooser = Gtk2::FileChooserDialog->new(undef, undef, "select-folder", "gtk-cancel" => "cancel", "gtk-ok" => "ok"); my $dir = trim_whitespace($$entry->get_text()); if ($dir) { $chooser->set_current_folder($dir); } if ($chooser->run eq 'ok') { $$entry->set_text($chooser->get_filename); } $chooser->destroy; }, \$entry); $hbox2->pack_start($button, 1, 1, 0); $vbox2->pack_start($hbox, 0, 1, 0); push(@directory_entries, $entry); } my $label = Gtk2::Label->new; $label->set_markup(" Wallpaper Browser "); my $frame = Gtk2::Frame->new; $frame->set_label_widget($label); $vbox->pack_start($frame, 1, 1, 0); my $hbox = Gtk2::HBox->new(0, 5); my $label = Gtk2::Label->new("Thumbnail size:"); $tooltip->set_tip($label, "Thumbnail size for wallpaper browser"); $label->set_size_request(100, -1); $hbox->pack_start($label, 0, 1, 0); my $_hbox = Gtk2::HBox->new(1, 1); my $thumb_width_entry = Gtk2::ComboBox->new_text; $tooltip->set_tip($thumb_width_entry, "Thumbnail width"); $_hbox->add($thumb_width_entry); my $thumb_aspect_entry = Gtk2::ComboBox->new_text; $tooltip->set_tip($thumb_aspect_entry, "Thumbnail aspect ratio"); $_hbox->add($thumb_aspect_entry); $hbox->pack_start($_hbox, 1, 1, 0); $frame->add($hbox); my @widths = ("64", "96", "128", "192", "256"); my ($width, $height) = get_thumb_size(); my $m; for (my $i = 0; $i <= $#widths; $i++) { my $w = $widths[$i]; $thumb_width_entry->append_text($w); if ($w == $width) { $m = $i; } } # allow for custom size set in config file unless (defined($m)) { $thumb_width_entry->append_text($width); $m = $#widths + 1; } $thumb_width_entry->set_active($m); my %aspects = ("4:3" => 1.333, "16:9" => 1.777, "16:10" => 1.6, "-" => -1); my $i = 0; my $m; $thumb_aspect_entry->set_active(0); foreach my $a (sort(keys(%aspects))) { $thumb_aspect_entry->append_text($a); if (!defined($m) && $height == int($width / $aspects{$a})) { $m = $i; } $i++; } $thumb_aspect_entry->set_active(defined($m) ? $m : 0); my $hbox = Gtk2::HBox->new(1, 1); $vbox->pack_start($hbox, 0, 1, 0); my $apply = Gtk2::Button->new_from_stock('gtk-apply'); $tooltip->set_tip($apply, "Apply settings (does not save)"); $apply->signal_connect(clicked => sub { my $command_changed = 0; my $_command = $command_entry->get_text(); unless ($_command) { $_command = get_default_command(); } if ($_command ne get_command()) { set_command($_command); $command_changed = 1; } set_delay($delay_entry->get_value()); set_recursive($recursive_entry->get_active()); set_random($random_entry->get_active()); set_screensaver($screensaver_entry->get_active()); my @dirs; foreach my $entry (@directory_entries) { my $dir = trim_whitespace($entry->get_text()); if ($dir) { push(@dirs, $dir); } } my $dirs_changed = 0; if (@dirs) { # don't care about the order if randomizing @images array; too much? my @a = get_random() ? sort(@dirs) : @dirs; my @b = get_random() ? sort(@directories) : @directories; # just a simple test $dirs_changed = cmp_array(\@a, \@b); if ($dirs_changed) { @directories = @dirs; } } my $_width = $thumb_width_entry->get_active_text(); my $_aspect = $thumb_aspect_entry->get_active_text(); my $_height = ($_aspect ne "-") ? int($_width / $aspects{$_aspect}) : $_width; my $size = $_width . "x" . $_height; if ($size ne get_thumb_size()) { set_thumb_size($size); empty_thumb_cache(); } # ugly, but if there's no immediate changes... if ($command_changed || $dirs_changed) { update_images(); set_image(); } else { # may as well check for changed dir contents in case a new # image was added, will try to save the index if possible update_images_if_changed(); } # reset timer if unchanged? start_mainloop(); # in case the image window is bypassed (with --prefs) refresh_browser(); }); $hbox->add($apply); my $save = Gtk2::Button->new_from_stock('gtk-save'); $tooltip->set_tip($save, "Saves settings (does not apply)"); $save->signal_connect(clicked => sub { my $_command = $command_entry->get_text(); unless ($_command) { $_command = get_default_command(); } my $_delay = $delay_entry->get_value(); my $_recursive = $recursive_entry->get_active(); my $_random = $random_entry->get_active(); my $_screensaver = $screensaver_entry->get_active(); my @_directories; foreach my $entry (@directory_entries) { my $dir = trim_whitespace($entry->get_text()); if ($dir) { push(@_directories, $dir); } } unless (@_directories) { @_directories = @directories; } my $_width = $thumb_width_entry->get_active_text(); my $_aspect = $thumb_aspect_entry->get_active_text(); my $_height = ($_aspect ne "-") ? int($_width / $aspects{$_aspect}) : $_width; my $_thumb_size = $_width . "x" . $_height; my $key_file = Glib::KeyFile->new; $key_file->set_string("main", "command", $_command); $key_file->set_integer("main", "delay", $_delay); $key_file->set_boolean("main", "recursive", $_recursive); $key_file->set_boolean("main", "random", $_random); $key_file->set_boolean("main", "screensaver", $_screensaver); $key_file->set_string_list("main", "directories", @_directories); $key_file->set_string("browser", "thumb-size", $_thumb_size); if (open(my $fh, "> $config")) { print $fh $key_file->to_data(); close($fh); } }); $hbox->add($save); directory_entries_have_values(undef, [ $apply, $save, \@directory_entries ]); foreach my $entry (@directory_entries) { $entry->signal_connect(changed => \&directory_entries_have_values, [ $apply, $save, \@directory_entries ]); } my $button = Gtk2::Button->new_from_stock('gtk-close'); $tooltip->set_tip($button, "Close window (without applying or saving)"); $button->signal_connect(clicked => sub { $window->destroy; }); $hbox->add($button); $window->show_all; } sub directory_entries_have_values { my ($widget, $data) = @_; my ($apply, $save, $directory_entries) = @{$data}; my $have_values = 0; foreach my $entry (@{$directory_entries}) { if (trim_whitespace($entry->get_text())) { $have_values++; } } $apply->set_sensitive($have_values); $save->set_sensitive($have_values); } sub trim_whitespace { my ($text) = @_; $text =~ s/(^\s+|\s+$)//g; return $text; } sub basename { my ($file) = @_; $file =~ s,[^/]*/,,g; return $file; } sub gmessage { my ($title, $text) = @_; my $window = Gtk2::Window->new; $window->set_title("GWP: " . ($title ? $title : "Message")); $window->set_default_size(425, -1); $window->signal_connect(destroy => sub { $window->destroy; }); $window->set_border_width(5); my $vbox = Gtk2::VBox->new(0, 5); $window->add($vbox); my $hbox = Gtk2::HBox->new(0, 5); $vbox->pack_start($hbox, 1, 1, 5); my $image = Gtk2::Image->new_from_stock("gtk-dialog-warning", "dialog"); $image->set_size_request(75, -1); $image->set_alignment(0.5, 0.5); $hbox->pack_start($image, 0, 0, 0); my $label = Gtk2::Label->new; $label->set_markup($text); $label->set_size_request(350, -1); $label->set_alignment(0.5, 0.5); $label->set_line_wrap(1); $hbox->pack_start($label, 0, 0, 0); my $button = Gtk2::Button->new_from_stock('gtk-close'); $button->signal_connect(clicked => sub { $window->destroy; }); $vbox->pack_start($button, 0, 0, 0); $window->show_all; } sub open_welcome { gmessage("Welcome!", "It looks like this is the first time you've run GWP.\n\nYou can configure your wallpaper directories and other settings in the Preferences window. Hit Apply to apply the settings and Save to write out the config file. This message will continue to appear at startup unless directories are specified in the configuration file or on the command line.\n\nHere's a quick summary of the configuration directives:\n\nWallpaper command: specifies the command to set an image as the background. The default value is " . get_default_command() . ", which is auto-detected. If you don't have this program, you'll need to install it or select something else.\nChange interval: allows you to set the auto-change frequency (in seconds). A value of 0 disables the timer completely and the Pause button on the Wallpaper Browser window will disable it for the duration of the session, or until it's toggled back on. The default value is 1800 seconds (30 minutes).\nRecursive dirs: recurses into the specified directories.\nRandomize order: shuffles the order of the wallpapers.\nIdle for screensaver: won't change the wallpaper if an active screensaver is detected. Supported screensavers include xscreensaver, gnome-screensaver, kde (untested), and xlock.\nWallpaper dir 1-5+: are for your wallpaper directories. Enter one directory per entry. If you need more entries, Apply your settings and reopen the Preferences window, and additional entries will be added.\n\nYour settings will be stored in $config and thumbnails are cached in memory, so you don't have to worry about a mess. All configuration directives (including directories) can be specified on the command line to override those stored in the config file.\n\nOnce configured, the Wallpaper Browser and Preferences windows will only open if requested.\n\nThere are a number of remote commands available to control the daemon, including --next to go to the next wallpaper and --prev to go back to the previous one, --show will open the Wallpaper Browser and --prefs will open the Preferences window, and --exit will shutdown the daemon.\n\nSee --help for a list of remote commands and other options."); } # -- thumb cache -------------------------------------------------------------- my %thumb_cache; sub get_thumb { my ($image) = @_; my $mtime = (stat($image))[9]; my $thumb = get_thumb_if_cached($image, $mtime); unless ($thumb) { my ($w, $h) = get_thumb_size(); $thumb_cache{$image}{thumb} = eval { Gtk2::Gdk::Pixbuf->new_from_file_at_scale($image, $w, $h, 1); }; if ($thumb_cache{$image}{thumb}) { $thumb_cache{$image}{mtime} = $mtime; $thumb = \$thumb_cache{$image}{thumb}; } else { delete $thumb_cache{$image}; $thumb = undef; } } return $thumb; } sub get_thumb_if_cached { my ($image, $mtime) = @_; if (defined($thumb_cache{$image})) { unless (defined($mtime)) { $mtime = (stat($image))[9]; } if ($thumb_cache{$image}{mtime} == $mtime && $thumb_cache{$image}{thumb}) { return \$thumb_cache{$image}{thumb}; } } } sub trim_thumb_cache { # don't let the cache get out of control but don't be too aggressive either if (keys(%thumb_cache) > ($#images + 32)) { # this could be improved a bit if @images was a hash, but ordering would # be a problem without the help of Tie::IxHash or Tie::Hash::Indexed and # that's overkill foreach my $image (keys(%thumb_cache)) { $thumb_cache{$image}{trim} = 1; } foreach my $image (@images) { if (defined($thumb_cache{$image})) { $thumb_cache{$image}{trim} = 0; } } foreach my $image (keys(%thumb_cache)) { if ($thumb_cache{$image}{trim}) { delete $thumb_cache{$image}; } } } } sub empty_thumb_cache { %thumb_cache = (); } # -- misc --------------------------------------------------------------------- sub max { my ($a, $b) = @_; return ($a > $b) ? $a : $b; } sub cmp_array { my ($a, $b) = @_; my $m = max($#{$a}, $#{$b}); my $i = 0; while ($i <= $m) { if (@$a[$i] ne @$b[$i]) { return 1; } $i++; } return 0; } my $default_command; sub get_default_command { unless ($default_command) { # a hash would be prettier, but we'd lose the order and feh, Esetroot, # and wmsetbg are preferred since they can do pseudo-transparency my @commands = ([ "feh", "feh --bg-center" ], [ "Esetroot", "Esetroot -c" ], [ "wmsetbg", "wmsetbg" ], [ "qiv", "qiv -xt" ], [ "xsetbg", "xsetbg" ]); foreach my $entry (@commands) { my ($prog, $command) = @{$entry}; if (find_program_in_path($prog)) { $default_command = $command; last; } } # nothing found, return something so they have an idea of what to install unless ($default_command) { $default_command = $commands[0][1]; } } return $default_command; } sub find_program_in_path { my ($prog) = @_; foreach my $p (split(/:/, $ENV{PATH})) { my $path = "$p/$prog"; if (-x $path) { return $path; } } } sub message { print basename($0) . ": " . $_[0] . "\n"; } sub show_help { print "Usage: " . basename($0) . " [opts] [ ...]\n"; print " -c / --command wallpaper command (default: " . get_default_command() . ")\n"; print " -d / --delay seconds between image change (default: 1800)\n"; print " or 0 to disable\n"; print " -r / --recursive recursively process directories\n"; print " --no-recursive disable recursion\n"; print " -a / --random randomize wallpapers\n"; print " --no-random disable randomization of wallpapers\n"; print " -s / --screensaver idle if xscreensaver/xlock is running\n"; print " --no-screensaver disable screensaver checks\n"; print " -1 / --one set one wallpaper and exit, assumes --random\n"; print " --next remote: set next image\n"; print " --prev(ious) remote: set previous image\n"; print " --pause remote: pause timer (toggles)\n"; print " --stop remote: stop timer\n"; print " --cont(inue) remote: continue timer\n"; print " --reload remote: reload config and restart timers\n"; print " --restart remote: restart daemon (closes any open windows)\n"; print " --show remote: open the wallpaper browser\n"; print " --prefs remote: open the preferences window\n"; print " -x / --exit remote: shutdown process\n"; print " -h / --help these messages\n"; }