no longer use internalized rexml
[booh] / bin / booh-classifier
index ad2e942ecfd59014c80f1863af61b975de7a8501..3be4fab23d05dc1e5ca9ee46b0649b64d269b3c6 100644 (file)
@@ -10,7 +10,7 @@
 # called Boo, so this one will be it "Booh". Or whatever.
 #
 #
-# Copyright (c) 2004-2006 Guillaume Cottenceau <http://zarb.org/~gc/resource/gc_mail.png>
+# Copyright (c) 2004-2013 Guillaume Cottenceau <http://zarb.org/~gc/resource/gc_mail.png>
 #
 # This software may be freely redistributed under the terms of the GNU
 # public license version 2.
 # along with this program; if not, write to the Free Software
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
+begin
+    require 'rubygems'
+rescue LoadError
+end
+
 require 'getoptlong'
 require 'tempfile'
 
@@ -39,10 +44,10 @@ require 'booh/UndoHandler'
 
 #- options
 $options = [
-    [ '--help',          '-h', GetoptLong::NO_ARGUMENT,       _("Get help message") ],
-
-    [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ],
+    [ '--help',          '-h', GetoptLong::NO_ARGUMENT, _("Get help message") ],
     [ '--sort-by-exif-date', '-s', GetoptLong::NO_ARGUMENT, _("Sort entries by EXIF date") ],
+    [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ],
+    [ '--version',       '-V', GetoptLong::NO_ARGUMENT, _("Print version and exit") ],
 ]
 
 $preloader_allowed = false
@@ -64,11 +69,20 @@ def handle_options
                 usage
                 exit(0)
 
+            when '--sort-by-exif-date'
+                $sort_by_exif_date = true
+
             when '--verbose-level'
                 $verbose_level = arg.to_i
 
-            when '--sort-by-exif-date'
-                $sort_by_exif_date = true
+            when '--version'
+                puts _("Booh version %s
+
+Copyright (c) 2005-2013 Guillaume Cottenceau.
+This is free software; see the source for copying conditions.  There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.") % $VERSION
+
+                exit(0)
 
             end
         end
@@ -79,6 +93,14 @@ def handle_options
     end
 end
 
+def total_memory
+    meminfo = IO.readlines('/proc/meminfo').join
+    meminfo =~ /MemTotal:.*?(\d+)/ or return -1
+    memory = $1.to_i
+    meminfo =~ /SwapTotal:.*?(\d+)/ or return -1
+    return memory + $1.to_i
+end
+
 def startup_memfree
     if $startup_memfree.nil?
         meminfo = IO.readlines('/proc/meminfo').join
@@ -98,7 +120,13 @@ def set_cache_memory_use_figure
     else
         $config['cache-memory-use-figure'] = $config['cache-memory-use'].to_i
     end
-    msg 2, _("Cache memory used: %s kB") % $config['cache-memory-use-figure']
+    #- cannot fork if process is > 0.5 total memory
+    if $config['cache-memory-use-figure'] > total_memory * 0.4
+        $config['cache-memory-use-figure'] = total_memory * 0.4
+        msg 2, _("Cache memory used: %s kB (reduced because cannot exceed 50%% of total memory)") % $config['cache-memory-use-figure']
+    else
+        msg 2, _("Cache memory used: %s kB") % $config['cache-memory-use-figure']
+    end
 end
 
 def read_config
@@ -125,11 +153,12 @@ def read_config
             end
         }
     end
-    $config['video-viewer'] ||= '/usr/bin/mplayer %f'
-    $config['browser'] ||= "/usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f"
+    $config['video-viewer'] ||= '/usr/bin/mplayer %f || /usr/bin/vlc %f'
+    $config['browser'] ||= "/usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f || /usr/bin/firefox -remote 'openURL(%f,new-window)' || /usr/bin/firefox %f"
     $config['preload-distance'] ||= '5'
     $config['cache-memory-use'] ||= 'memfree_80%'
     $config['rotate-set-exif'] ||= 'true'
+    $config['thumbnails-height'] ||= '64'
     set_cache_memory_use_figure
 end
 
@@ -151,13 +180,7 @@ Problem was: '%s' is not an executable file.
 Hint: don't forget to specify the full path to the executable,
 e.g. '/usr/bin/mplayer' is correct but 'mplayer' only is not.") % viewer_binary), { :pos_centered => true, :not_transient => true })
     end
-    browser_binary = $config['browser'].split.first
-    if browser_binary && ! File.executable?(browser_binary)
-        show_popup($main_window, utf8(_("The configured browser seems to be unavailable.
-You should fix this in Edit/Preferences so that you can open URLs.
-
-Problem was: '%s' is not an executable file.") % browser_binary), { :pos_centered => true, :not_transient => true })
-    end
+    check_browser
 end
 
 def write_config
@@ -181,7 +204,7 @@ def write_config
             end
         end
     }
-    $xmldoc.write(ios, 0)
+    $xmldoc.write(ios)
     ios.close
 end
 
@@ -192,8 +215,8 @@ def save_undo(name, closure, *params)
 end
 
 def get_mem
-    IO.readlines('/proc/self/status').join =~ /VmRSS.*?(\d+)\s*kB/
-    msg 3, "RSS: #{$1}"
+    IO.readlines('/proc/self/status').join =~ /VmSize.*?(\d+)\s*kB/
+    msg 3, "VmSize: #{$1}"
     return $1.to_i
 end
 
@@ -227,35 +250,45 @@ $colors = [ Gdk::Color.new(0, 65535, 0),
             Gdk::Color.new(65535, 0, 65535) ]
 
 class Label
-    attr_accessor :color, :name, :button
+    attr_accessor :color, :name, :button, :counter
     def initialize(name)
         @name = name
     end
 end
 
+class InterruptedLoading < Exception
+    #- not a StandardError, not catched by a simple rescue
+end
+
+def show_pixbufs_present
+    if 3 <= $verbose_level
+        out = 'Full pixbufs ['
+        for entry in $allentries
+            out += entry.pixbuf_full_present? ? 'F' : '.'
+        end
+        msg 3, out + ']'
+    end
+end
+
 class Entry
-    @@thumbnails_height = 64
     @@max_width = nil
     def Entry.thumbnails_height
-        return @@thumbnails_height
+        return $config['thumbnails-height'].to_i
     end
 
-    attr_accessor :path, :type, :angle, :button, :image, :alignment, :removed, :labeled
+    attr_accessor :path, :guipath, :type, :angle, :button, :image, :alignment, :removed, :labeled, :loader
 
-    def initialize(path, type)
+    def initialize(path, type, guipath)
         @path = path
         @type = type
+        @guipath = guipath
         if @@max_width.nil?
             @@max_width = $main_window.root_window.size[0] - $labels_vbox.allocation.width - ( $videoborder_pixbuf.width + MainView.borders_thickness) * 2
         end
     end
 
-    def pixbuf_full
-        if @pixbuf_full.nil?
-            msg 3, ">>> pixbuf_full #{path}"
-            load_into_pixbuf_full
-        end
-        return @pixbuf_full
+    def pixbuf_full_present?
+        return ! @pixbuf_full.nil?
     end
     def free_pixbuf_full
         if @pixbuf_full.nil?
@@ -266,12 +299,13 @@ class Entry
             return true
         end
     end
+
     def pixbuf_main
+        Gtk.main_iteration while Gtk.events_pending?
         width, height = $mainview.window.size 
         width = MainView.get_usable_width(width)
         height = MainView.get_usable_height(height)
         if @pixbuf_main.nil? || width != @width || height != @height
-            msg 3, ">>> pixbuf_main #{path}"
             @width = width
             @height = height
             load_into_pixbuf_full  #- make sure it is loaded
@@ -281,14 +315,14 @@ class Entry
             if @pixbuf_full.width.to_f / @pixbuf_full.height > width.to_f / height
                 resized_height = @pixbuf_full.height * (width.to_f/@pixbuf_full.width)
                 if @pixbuf_full.width > width || @pixbuf_full.height > resized_height
-                    @pixbuf_main = @pixbuf_full.scale(width, resized_height, Gdk::Pixbuf::INTERP_BILINEAR)
+                    @pixbuf_main = @pixbuf_full.scale(width, resized_height, :bilinear)
                 else
                     @pixbuf_main = @pixbuf_full
                 end
             else
                 resized_width = @pixbuf_full.width * (height.to_f/@pixbuf_full.height)
                 if @pixbuf_full.width > resized_width || @pixbuf_full.height > height
-                    @pixbuf_main = @pixbuf_full.scale(resized_width, height, Gdk::Pixbuf::INTERP_BILINEAR)
+                    @pixbuf_main = @pixbuf_full.scale(resized_width, height, :bilinear)
                 else
                     @pixbuf_main = @pixbuf_full
                 end
@@ -296,6 +330,9 @@ class Entry
         end
         return @pixbuf_main
     end
+    def pixbuf_main_present?
+        return ! @pixbuf_main.nil?
+    end
     def free_pixbuf_main
         if @pixbuf_main.nil?
             return false
@@ -305,23 +342,25 @@ class Entry
             return true
         end
     end
+
     def pixbuf_thumbnail
+        Gtk.main_iteration while Gtk.events_pending?
         if @pixbuf_thumbnail.nil?
             if @pixbuf_main
                 msg 3, ">>> pixbuf_thumbnail from main #{path}"
-                @pixbuf_thumbnail = @pixbuf_main.scale(@pixbuf_main.width * (@@thumbnails_height.to_f/@pixbuf_main.height), @@thumbnails_height, Gdk::Pixbuf::INTERP_BILINEAR)
+                @pixbuf_thumbnail = @pixbuf_main.scale(@pixbuf_main.width * (Entry.thumbnails_height.to_f/@pixbuf_main.height), Entry.thumbnails_height, :bilinear)
             else
                 msg 3, ">>> pixbuf_thumbnail from file #{path}"
                 @pixbuf_thumbnail = load_into_pixbuf_at_size { |w, h|
                     if @angle == 0
-                        if h > @@thumbnails_height
-                            [ w * @@thumbnails_height.to_f/h, @@thumbnails_height ]
+                        if h > Entry.thumbnails_height
+                            [ w * Entry.thumbnails_height.to_f/h, Entry.thumbnails_height ]
                         else
                             [ w, h ]
                         end
                     else
-                        if w > @@thumbnails_height
-                            [ @@thumbnails_height, h * @@thumbnails_height.to_f/w ]
+                        if w > Entry.thumbnails_height
+                            [ Entry.thumbnails_height, h * Entry.thumbnails_height.to_f/w ]
                         else
                             [ w, h ]
                         end
@@ -366,22 +405,30 @@ class Entry
     def get_beautified_name
         if type == 'image'
             size = get_image_size(path)
-            return _("%s (%sx%s, %s KB)") % [File.basename(@path).gsub(/\.[^.]+$/, ''),
+            return _("%s (%sx%s, %s KB)") % [@guipath.gsub(/\.[^.]+$/, ''),
                                              size[:x],
                                              size[:y],
                                              commify(file_size(path)/1024)]
         else
-            return _("%s (video - %s KB)") % [File.basename(@path).gsub(/\.[^.]+$/, ''),
+            return _("%s (video - %s KB)") % [@guipath.gsub(/\.[^.]+$/, ''),
                                              commify(file_size(path)/1024)]
         end
     end
 
-    private
-    def cleanup_dir(dir)
-        Dir.entries(dir).each { |file| file != '.' && file != '..' and File.delete(File.join(dir, file)) }
-        Dir.delete(dir)
+    def cancel_loader
+        if ! @loader.nil?
+            #- avoid unneeded memory allocation
+            @loader.signal_handler_disconnect(@area_prepared_cb)
+            begin
+                @loader.close
+            rescue
+                #- ignore loader errors, at that point they are fairly normal, we're canceling a partial load
+            end
+            @loader = nil
+        end
     end
 
+    private
     def load_into_pixbuf_full
         if @pixbuf_full.nil?
             msg 3, ">>> load_into_pixbuf_full #{path}"
@@ -401,21 +448,21 @@ class Entry
                     end
                 end
             }
+            show_pixbufs_present
         end
     end
 
     def load_into_pixbuf_at_size(&specify_size)
-        pixbuf = nil
         if @type == 'video'
-            tmp = Tempfile.new("boohclassifiertemp")
-            tmp.close!
-            Dir.mkdir(dest_dir = tmp.path)
-            orig_base = File.basename(path)
-            tmpdir = gen_video_thumbnail(path, false, 0)
-            if tmpdir.nil?
-                return
+            if @video_image_path.nil?
+                orig_base = File.basename(path)
+                tmpdir = gen_video_thumbnail(path, false, 0)
+                if tmpdir.nil?
+                    return
+                end
+                @video_image_path = "#{tmpdir}/00000001.jpg"
             end
-            image_path = "#{tmpdir}/00000001.jpg"
+            image_path = @video_image_path
         else
             image_path = @path
         end
@@ -427,44 +474,45 @@ class Entry
             end
         end
         begin
-            #- use a pixbuf loader and trigger Gtk.main_iteration on each chunk if needed, to keep the UI responsive even
+            #- use a pixbuf loader and check Gtk.events_pending? on each chunk, to keep the UI responsive even
             #- if loaded pictures are several MBs large
-            loader = Gdk::PixbufLoader.new
-            loader.signal_connect('size-prepared') { |l, w, h|
-                r = specify_size.call(w, h)
-                msg 3, "specified sizes: #{r[0]} #{r[1]}"
-                loader.set_size(*specify_size.call(w, h))
-            }
-            loader.signal_connect('area-prepared') { pixbuf = loader.pixbuf }
-            file = File.new(image_path)
-            while (chunk = file.read(4096)) != nil
-                loader.write(chunk)
-                Gtk.main_iteration while Gtk.events_pending?
+            if @loader.nil?
+                @loader = GdkPixbuf::PixbufLoader.new
+                @loader.signal_connect('size-prepared') { |l, w, h|
+                    @loader.set_size(*specify_size.call(w, h))
+                }
+                @area_prepared_cb = @loader.signal_connect('area-prepared') { @loaded_pixbuf = @loader.pixbuf }
+                @loader_offset = 0
             end
-            file.close
-            loader.close
-            if pixbuf.nil?
+            msg 3, "calling load_not_freezing_ui on #{image_path}, offset #{@loader_offset}"
+            @loader_offset = @loader.load_not_freezing_ui(image_path, @loader_offset)
+            if @loader_offset > 0
+                #- interrupted
+                raise InterruptedLoading
+            end
+            @loader = nil
+            if @loaded_pixbuf.nil?
                 raise "Loaded pixbuf nil - #{path} #{image_path}"
             end
-        rescue Gdk::PixbufError
+        rescue
             msg 0, "Cannot load #{image_path}: #{$!}"
             return
         ensure
-            if @type == 'video'
-                File.delete(image_path)
-                Dir.rmdir(tmpdir)
+            if @video_image_path && @loader.nil?
+                File.delete(@video_image_path)
+                Dir.rmdir(File.dirname(@video_image_path))
+                @video_image_path = nil
             end
         end
-        if pixbuf
+        if @loaded_pixbuf
             if @angle != 0
                 msg 3, ">>> load_into_pixbuf_full #{image_path} => rotate #{@angle}"
-                pixbuf = rotate_pixbuf(pixbuf, @angle)
+                @loaded_pixbuf = rotate_pixbuf(@loaded_pixbuf, @angle)
             end
         end
-        if @type == 'video'
-            cleanup_dir(dest_dir)
-        end
-        return pixbuf
+        retval = @loaded_pixbuf
+        @loaded_pixbuf = nil
+        return retval
     end
 
     def to_s
@@ -474,53 +522,133 @@ end
 
 $allentries = []
 
-def run_preloader_real
-    msg 3, "*** >> main preloading triggered..."
-    if $preloader_running
-        msg 3, "*** >>>>>> already running, return <<<<<<<<"
+def gc
+    start = Time.now
+    GC.start
+    msg 3, "GC in #{Time.now - start} s"
+end
+
+def free_cache_if_needed
+    i = $allentries.index($mainview.get_shown_entry)
+    return if i.nil?
+    if get_mem > $config['cache-memory-use-figure']
+        msg 3, "too much RSS, triggering GC"
+        gc
+    end
+    if get_mem < $config['cache-memory-use-figure']
         return
     end
-    $preloader_running = true
-    if $mainview.get_shown_entry
-        mem = get_mem
-        if mem > $config['cache-memory-use-figure']
-            msg 3, "too much RSS, stopping preloading, triggering GC"
-            $preloader_running = false
-            GC.start
-            msg 3, "GC finished"
-            return
+    msg 3, "too much RSS, freeing some cache"
+    start = Time.now
+    freed = 0
+    ($allentries.size - 1).downto($config['preload-distance'].to_i + 1) { |j|
+        index = i + j
+        if i + j < $allentries.size
+            $allentries[i + j].free_pixbuf_full
+            if $allentries[i + j].free_pixbuf_main
+                freed += 1
+            end
         end
-        index = $allentries.index($mainview.get_shown_entry)
-        for j in 1 .. $config['preload-distance'].to_i
-            i = index + j
-            if i < $allentries.size
-                $allentries[i].pixbuf_main
+        if i - j >= 0
+            $allentries[i - j].free_pixbuf_full
+            if $allentries[i - j].free_pixbuf_main
+                freed += 1
             end
-            #- in case just loaded another directory
-            if $preloader_force_exit
-                $preloader_running = false
-                $preloader_force_exit = false
+        end
+        if freed >= 10
+            gc
+            if get_mem < $config['cache-memory-use-figure'] * 3 / 4
+                msg 3, "RSS down enough - freeing done in #{Time.now - start} s"
+                show_pixbufs_present
                 return
             end
-            i = index - j
-            if i >= 0
-                $allentries[i].pixbuf_main
+            freed = 0
+        end
+    }
+    msg 3, "freeing done in #{Time.now - start} s"
+    show_pixbufs_present
+end
+
+def run_preloader_real
+    msg 3, "*** >> main preloading triggered..."
+    if $mainview.get_shown_entry
+        free_cache_if_needed
+        if $config['preload-distance'].to_i == 0
+            return true
+        end
+        index = $allentries.index($mainview.get_shown_entry)
+        index_right = index
+        index_left = index
+        loaded_right = 0
+        loaded_left = 0
+        right_done = false
+        left_done = false
+        loaded = []
+        while ! right_done || ! left_done
+            if ! right_done
+                index_right += 1
+                while index_right < $allentries.size && ! visible($allentries[index_right])
+                    index_right += 1
+                end
+                if index_right == $allentries.size
+                    right_done = true
+                else
+                    if ! $allentries[index_right].pixbuf_main_present?
+                        msg 3, "preloading #{$allentries[index_right].path}"
+                        begin
+                            $allentries[index_right].pixbuf_main
+                        rescue InterruptedLoading
+                            msg 3, "*** >>>> interrupted, rerun"
+                            return false
+                        end
+                    end
+                    loaded << index_right
+                    loaded_right += 1
+                    if loaded_right == $config['preload-distance'].to_i
+                        right_done = true
+                    end
+                end
+            end
+
+            if ! left_done
+                index_left -= 1
+                while index_left >= 0 && ! visible($allentries[index_left])
+                    index_left -= 1
+                end
+                if index_left == -1
+                    left_done = true
+                else
+                    if ! $allentries[index_left].pixbuf_main_present?
+                        msg 3, "preloading #{$allentries[index_left].path}"
+                        begin
+                            $allentries[index_left].pixbuf_main
+                        rescue InterruptedLoading
+                            msg 3, "*** >>>> interrupted, rerun"
+                            return false
+                        end
+                    end
+                    loaded << index_left
+                    loaded_left += 1
+                    if loaded_left == $config['preload-distance'].to_i
+                        left_done = true
+                    end
+                end
             end
+
             #- in case just loaded another directory
             if $preloader_force_exit
-                $preloader_running = false
                 $preloader_force_exit = false
-                return
+                return true
+            end
+            #- in case moved fast
+            if index != $allentries.index($mainview.get_shown_entry)
+                msg 3, "*** >>>> moved already, rerun"
+                return false
             end
         end
-        check_memory_free_cache_if_needed
     end
-    $preloader_running = false
     msg 3, "*** << main preloading finished"
-    #- if we're already on a different image, rerun the preloader
-    if index != $allentries.index($mainview.get_shown_entry)
-        run_preloader_real
-    end
+    return true
 end
 
 def run_preloader
@@ -528,9 +656,21 @@ def run_preloader
         msg 3, "*** preloader not yet allowed"
         return
     end
-    Gtk.timeout_add(10) {
-        run_preloader_real
-        false
+
+    if $preloader_running
+        msg 3, "preloader already running"
+        return
+    end
+    msg 3, "run preloader"
+    $preloader_running = true
+    Gtk.idle_add {
+        msg 3, "begin preloader from timeout "
+        if run_preloader_real
+            $preloader_running = false
+            false
+        else
+            true
+        end
     }
 end
 
@@ -538,6 +678,7 @@ class MainView < Gtk::DrawingArea
 
     @@borders_thickness = 5
     @@borders_length = 25
+    @@redraw_pending = nil
 
     def MainView.borders_thickness
         return @@borders_thickness
@@ -555,25 +696,30 @@ class MainView < Gtk::DrawingArea
         super()
         signal_connect('expose-event') { draw }
         signal_connect('configure-event') { update_shown }
-        @preloader_running = false
+    end
+
+    def try_show_entry(entry)
+        if entry && entry.button
+            if entry.button.has_focus?
+                redraw
+            else
+                entry.button.grab_focus
+            end
+        end
     end
 
     def set_shown_entry(entry)
         t1 = Time.now
-        if ! entry.nil? && entry == @entry
+        if entry && entry == @entry
             return
         end
-        if ! entry.nil?
-            if ! entry.button
-                #- not loaded yet
-                return
-            else
-                entry.button.grab_focus
-            end
+        if entry && ! entry.button
+            #- not loaded yet
+            return
         end
         @entry = entry
-        redraw        
-        run_preloader
+        @entry and msg 3, "*** set entry to #{@entry.path}"
+        redraw
         msg 3, "entry shown in: #{Time.now - t1} s"
     end
 
@@ -582,14 +728,55 @@ class MainView < Gtk::DrawingArea
     end
 
     def show_next_entry(entry)
-        index = $allentries.index(entry) + 1
-        if index < $allentries.size
-            set_shown_entry($allentries[index])
+        index = $allentries.index(entry)
+        if index < $allentries.size - 1
+            index += 1
+        end
+        while index < $allentries.size - 1 && $allentries[index] && $allentries[index].button && ! $allentries[index].button.visible?
+            index += 1
+        end
+        while $allentries[index] && $allentries[index].button && ! $allentries[index].button.visible? && index > 0
+            index -= 1
+        end
+        if index < $allentries.size && $allentries[index] && $allentries[index].button && $allentries[index].button.visible?
+            try_show_entry($allentries[index])
+            return
+        end
+        #- find a fallback before
+        while index < $allentries.size && index > 0 && $allentries[index] && (! $allentries[index].button || ! $allentries[index].button.visible?)
+            index -= 1
+        end
+        if index < $allentries.size && index > 0 && $allentries[index] && $allentries[index].button && $allentries[index].button.visible?
+            try_show_entry($allentries[index])
         end
     end
 
     def redraw
-        update_shown
+        if @@redraw_pending
+            msg 3, "redraw already pending"
+            return
+        end
+        msg 3, "redraw"
+        @@redraw_pending = Gtk.idle_add {
+            msg 3, "begin redraw from timeout "
+            begin
+                msg 3, "try redraw from timeout"
+                redraw_real
+                @@redraw_pending = nil
+                run_preloader
+                false
+            rescue InterruptedLoading
+                msg 3, "interrupted, will retry"
+                true
+            end
+        }
+    end
+
+    def redraw_real
+        @entry and sb_msg(_("Selected %s") % @entry.get_beautified_name)
+        if ! update_shown
+            return
+        end
         w, h = window.size
         window.begin_paint(Gdk::Rectangle.new(0, 0, w, h))
         window.clear
@@ -599,12 +786,20 @@ class MainView < Gtk::DrawingArea
 
     def update_shown
         if @entry
-            @pixbuf = @entry.pixbuf_main
-            width, height = window.size 
-            @xpos = (width - @pixbuf.width)/2
-            @ypos = (height - @pixbuf.height)/2
+            msg 3, "################################################ trying to show #{@entry.path}"
+            pixbuf = @entry.pixbuf_main
+            if pixbuf
+                @pixbuf = pixbuf
+                width, height = window.size 
+                @xpos = (width - @pixbuf.width)/2
+                @ypos = (height - @pixbuf.height)/2
+                return true
+            else
+                return false
+            end
         else
             @pixbuf = nil
+            return true
         end
     end
 
@@ -614,11 +809,11 @@ class MainView < Gtk::DrawingArea
             if @entry && @entry.type == 'video'
                 window.draw_borders($videoborder_pixbuf, @xpos - $videoborder_pixbuf.width, @xpos + @pixbuf.width, @ypos, @ypos + @pixbuf.height)
             end
-            if ! @entry.outline_color.nil?
+            if @entry && ! @entry.outline_color.nil?
                 gc = Gdk::GC.new(window)
                 colormap.alloc_color(@entry.outline_color, false, true)
                 gc.set_foreground(@entry.outline_color)
-                if @entry && @entry.type == 'video'
+                if @entry.type == 'video'
                     xleft = @xpos - $videoborder_pixbuf.width
                     xright = @xpos + @pixbuf.width + $videoborder_pixbuf.width
                 else
@@ -640,49 +835,27 @@ class MainView < Gtk::DrawingArea
     end
 end
 
-def check_memory_free_cache_if_needed
-    i = $allentries.index($mainview.get_shown_entry)
-    return if i.nil?
-    if get_mem < $config['cache-memory-use-figure'] * 2 / 3
-        return
-    end
-    msg 3, "too much RSS, triggering GC"
-    GC.start
-    msg 3, "GC finished"
-    ($allentries.size - 1).downto(1) { |j|
-        if get_mem < $config['cache-memory-use-figure'] / 2
-            break
-        end
-        index = i + j
-        msg 3, "too much RSS, freeing full size of #{i+j} and #{i-j}..."
-        if i + j < $allentries.size
-            $allentries[i+j].free_pixbuf_full
-        end
-        if i - j > 0
-            $allentries[i-j].free_pixbuf_full
-        end
-    }
-end
-
-def autoscroll_if_needed(button)
+def autoscroll_if_needed(button, center)
     xpos_left = button.allocation.x
     xpos_right = button.allocation.x + button.allocation.width
     hadj = $imagesline_sw.hadjustment
     current_minx_visible = hadj.value
     current_maxx_visible = hadj.value + hadj.page_size
-    if xpos_left < current_minx_visible
-        #- autoscroll left
-        newval = hadj.value - (current_minx_visible - xpos_left)
-        hadj.value = newval
-        button.queue_draw  #- TOREMOVE: the visual focus is displayed incorrectly
-    elsif xpos_right > current_maxx_visible
-        #- autoscroll right
-        newval = hadj.value + (xpos_right - current_maxx_visible)
-        if newval > hadj.upper - hadj.page_size
-            newval = hadj.upper - hadj.page_size
-        end
-        hadj.value = newval
-        button.queue_draw  #- TOREMOVE: the visual focus is displayed incorrectly
+    if ! center
+        if xpos_left < current_minx_visible
+            #- autoscroll left
+            newval = hadj.value - (current_minx_visible - xpos_left)
+            hadj.value = newval
+        elsif xpos_right > current_maxx_visible
+            #- autoscroll right
+            newval = hadj.value + (xpos_right - current_maxx_visible)
+            if newval > hadj.upper - hadj.page_size
+                newval = hadj.upper - hadj.page_size
+            end
+            hadj.value = newval
+        end
+    else
+        hadj.value = clamp((xpos_left + xpos_right) / 2 - hadj.page_size / 2, 0, hadj.upper - hadj.page_size)
     end
 end
 
@@ -710,9 +883,6 @@ def show_popup(parent, msg, *options)
     if options[:selectable]
         lbl.selectable = true
     end
-    if options[:topwidget]
-        dialog.vbox.add(options[0][:topwidget])
-    end
     if options[:scrolled]
         sw = Gtk::ScrolledWindow.new(nil, nil)
         sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
@@ -727,9 +897,21 @@ def show_popup(parent, msg, *options)
         dialog.vbox.add(options[:bottomwidget])
     end
     if options[:okcancel]
-        dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
+        cancel = dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
+        dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
+    elsif options[:yestoall]
+        cancel = dialog.add_button(Gtk::Stock::NO, Gtk::Dialog::RESPONSE_NO)
+        if ! options[:bottomwidget]
+            cancel.grab_focus
+        end
+        dialog.add_button(Gtk::Stock::YES, Gtk::Dialog::RESPONSE_YES)
+        dialog.add_button(utf8(_("Yes to all")), Gtk::Dialog::RESPONSE_ACCEPT)
+    else
+        ok = dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
+        if ! options[:bottomwidget]
+            ok.grab_focus
+        end
     end
-    dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
 
     if options[:pos_centered]
         dialog.window_position = Gtk::Window::POS_CENTER
@@ -766,6 +948,8 @@ def show_popup(parent, msg, *options)
             dialog.destroy
             if options[:okcancel]
                 return response == Gtk::Dialog::RESPONSE_OK
+            elsif options[:yestoall]
+                return response == Gtk::Dialog::RESPONSE_YES ? 'yes' : response == Gtk::Dialog::RESPONSE_ACCEPT ? 'yestoall' : 'no'
             end
         }
     else
@@ -785,6 +969,32 @@ def view_entry(entry)
     end
 end
 
+def update_counters
+    value = 0
+    $allentries.each { |entry|
+        if ! entry.removed && entry.labeled.nil?
+            value += 1
+        end
+    }
+    $unlabelled_counter.set_markup('<tt>' + value.to_s + '</tt>')
+    value = 0
+    $allentries.each { |entry|
+        if entry.removed 
+            value += 1
+        end
+    }
+    $toremove_counter.set_markup('<tt>' + value.to_s + '</tt>')
+    $labels.values.each { |label|
+        value = 0
+        $allentries.each { |entry|
+            if entry.labeled == label
+                value += 1
+            end
+        }
+        label.counter.set_markup('<tt>' + value.to_s + '</tt>')
+    }
+end
+
 def thumbnail_keypressed(entry, event)
     if event.state & Gdk::Window::MOD1_MASK != 0
         #- ALT pressed: Alt-Left and Alft-Right rotate
@@ -797,6 +1007,7 @@ def thumbnail_keypressed(entry, event)
             entry.free_pixbuf_full
             entry.free_pixbuf_main
             entry.free_pixbuf_thumbnail
+            show_pixbufs_present
             $mainview.redraw
             entry.image.pixbuf = entry.pixbuf_thumbnail
             if $config['rotate-set-exif'] == 'true' && entry.type == 'image'
@@ -805,24 +1016,32 @@ def thumbnail_keypressed(entry, event)
         end
 
     elsif event.state & Gdk::Window::CONTROL_MASK != 0
-        #- CONTROL pressed: Ctrl-z and Ctrl-r for undo/redo
+        #- CONTROL pressed: Ctrl-z and Ctrl-r for undo/redo, Ctrl-space for recentre
         if event.keyval == Gdk::Keyval::GDK_z
             perform_undo
         end
         if event.keyval == Gdk::Keyval::GDK_r
             perform_redo
         end
+        if event.keyval == Gdk::Keyval::GDK_space
+            shown = $mainview.get_shown_entry
+            shown and autoscroll_if_needed(shown.button, true)
+        end
 
     else
         removed_before = entry.removed
         label_before = entry.labeled
 
         if event.keyval == Gdk::Keyval::GDK_Delete
+            if ! FileTest.writable?(entry.path)
+                show_popup($main_window, utf8(_("Notice: no write access to '%s', permission will be denied at execute step.") % entry.path))
+            end
             entry.removed = true
             entry.labeled = nil
             entry.show_bg
-            $mainview.show_next_entry(entry)
             update_visibility(entry)
+            update_counters
+            $mainview.show_next_entry(entry)
 
             save_undo(_("set for removal"),
                       proc {
@@ -830,16 +1049,18 @@ def thumbnail_keypressed(entry, event)
                           entry.labeled = label_before
                           entry.show_bg
                           update_visibility(entry)
+                          update_counters
                           if entry.button.visible?
-                              $mainview.set_shown_entry(entry)
+                              $mainview.try_show_entry(entry)
                           end
                           proc {
                               entry.removed = true
                               entry.labeled = nil
                               entry.show_bg
                               update_visibility(entry)
+                              update_counters
                               if entry.button.visible?
-                                  $mainview.set_shown_entry(entry)
+                                  $mainview.try_show_entry(entry)
                               end
                           }
                       })
@@ -853,6 +1074,7 @@ def thumbnail_keypressed(entry, event)
             entry.removed = false
             entry.labeled = nil
             entry.show_bg
+            update_counters
             $mainview.show_next_entry(entry)
 
             save_undo(msg,
@@ -860,18 +1082,42 @@ def thumbnail_keypressed(entry, event)
                           entry.removed = removed_before
                           entry.labeled = label_before
                           entry.show_bg
-                          $mainview.set_shown_entry(entry)
+                          update_counters
+                          $mainview.try_show_entry(entry)
                           proc {
                               entry.removed = false
                               entry.labeled = nil
                               entry.show_bg
-                              $mainview.set_shown_entry(entry)
+                              update_counters
+                              $mainview.try_show_entry(entry)
                           }
                       })
 
         elsif event.keyval == Gdk::Keyval::GDK_Return
             view_entry(entry)
 
+        elsif event.keyval == Gdk::Keyval::GDK_Home
+            index = 0
+            while $allentries[index] && $allentries[index].button && !visible($allentries[index])
+                index += 1
+            end
+            if $allentries[index] && $allentries[index].button
+                $allentries[index].button.grab_focus
+            end
+
+        elsif event.keyval == Gdk::Keyval::GDK_End
+            index = $allentries.size - 1
+            while $allentries[index] && ! $allentries[index].button
+                #- not yet loaded
+                index -= 1
+            end
+            while $allentries[index] && $allentries[index].button && !visible($allentries[index])
+                index -= 1
+            end
+            if $allentries[index] && $allentries[index].button
+                $allentries[index].button.grab_focus
+            end
+
         else
             char = [ Gdk::Keyval.to_unicode(event.keyval) ].pack("C*")
             if char =~ /^[a-zA-z0-9]$/
@@ -879,29 +1125,32 @@ def thumbnail_keypressed(entry, event)
                 
                 if label.nil?
                     vb = Gtk::VBox.new(false, 0)
-                    vb.pack_start(entry = Gtk::Entry.new.set_text(char), false, false)
+                    vb.pack_start(labelentry = Gtk::Entry.new.set_text(char), false, false)
                     vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(bt = Gtk::ColorButton.new))
                     color = bt.color = Gdk::Color.new(16384 + rand(49151), 16384 + rand(49151), 16384 + rand(49151))
                     bt.signal_connect('color-set') { color = bt.color }
                     text = nil
-                    entry.signal_connect('changed') {  #- cannot add a new label with first letter of an existing label
-                        while $labels.has_key?(entry.text[0,1])
-                            entry.text = entry.text.sub(/./, '')
+                    labelentry.signal_connect('changed') {  #- cannot add a new label with first letter of an existing label
+                        while $labels.has_key?(labelentry.text[0,1])
+                            labelentry.text = labelentry.text.sub(/./, '')
                         end
                     }
                     if show_popup($main_window,
                                   utf8(_("You typed the text character '%s', which is not associated with a label.\nType in the full name of the label below to create a new one.")) % char,
-                                  { :okcancel => true, :bottomwidget => vb, :data_getter => proc { text = entry.text },
-                                    :stuff_connector => proc { |stuff| entry.select_region(0, 0)
-                                                                       entry.position = -1
-                                                                       entry.signal_connect('activate') { stuff[:dialog].response(Gtk::Dialog::RESPONSE_OK) } } } )
+                                  { :okcancel => true, :bottomwidget => vb, :data_getter => proc { text = labelentry.text },
+                                    :stuff_connector => proc { |stuff| labelentry.select_region(0, 0)
+                                                                       labelentry.position = -1
+                                                                       labelentry.signal_connect('activate') { stuff[:dialog].response(Gtk::Dialog::RESPONSE_OK) } } } )
                         if text.length > 0
                             char = text[0,1]  #- in case it changed
                             label = Label.new(text)
                             label.color = color
                             $labels[char] = label
+                            $ordered_labels << label
                             lbl = Gtk::Label.new.set_markup('<b>(' + char + ')</b>' + text[1..-1]).set_justify(Gtk::Justification::CENTER)
-                            $labels_vbox.pack_start(label.button = Gtk::CheckButton.new.add(evt = Gtk::EventBox.new.add(lbl)).show_all)
+                            $labels_vbox.pack_start(Gtk::HBox.new(false, 5).pack_start(label.button = Gtk::CheckButton.new.add(evt = Gtk::EventBox.new.add(lbl))).
+                                                                            pack_start(Gtk::Label.new, true, true).
+                                                                            pack_start(label.counter = Gtk::Label.new.set_markup('<tt>0</tt>'), false, false).show_all)
                             label.button.active = true
                             label.button.signal_connect('toggled') { update_all_visibilities }
                             evt.modify_bg(Gtk::StateType::NORMAL, label.color)
@@ -909,13 +1158,15 @@ def thumbnail_keypressed(entry, event)
                             evt.modify_bg(Gtk::StateType::ACTIVE, label.color.lighter)
                         end
                     end
+                end
 
-                else
+                if label
                     entry.removed = false
                     entry.labeled = label
                     entry.show_bg
-                    $mainview.show_next_entry(entry)
                     update_visibility(entry)
+                    update_counters
+                    $mainview.show_next_entry(entry)
 
                     save_undo(_("set label"),
                               proc {
@@ -923,16 +1174,18 @@ def thumbnail_keypressed(entry, event)
                                   entry.labeled = label_before
                                   entry.show_bg
                                   update_visibility(entry)
+                                  update_counters
                                   if entry.button.visible?
-                                      $mainview.set_shown_entry(entry)
+                                      $mainview.try_show_entry(entry)
                                   end
                                   proc {
                                       entry.removed = false
                                       entry.labeled = label
                                       entry.show_bg
                                       update_visibility(entry)
+                                      update_counters
                                       if entry.button.visible?
-                                          $mainview.set_shown_entry(entry)
+                                          $mainview.try_show_entry(entry)
                                       end
                                   }
                               })
@@ -949,9 +1202,9 @@ def sb_msg(msg)
     end
 end
 
-def show_entry(entry, i)
+def show_entry(entry, i, tips)
     #- scope entry
-    msg 3, "showing entry #{entry}"
+    #msg 3, "showing entry #{entry}"
     entry.image = Gtk::Image.new(entry.pixbuf_thumbnail)
     if entry.type == 'video'
         entry.button = Gtk::Button.new.add(Gtk::HBox.new.pack_start(da1 = Gtk::DrawingArea.new.set_size_request($videoborder_pixbuf.width, -1), false, false).
@@ -960,16 +1213,17 @@ def show_entry(entry, i)
         da1.signal_connect('realize') { da1.window.set_back_pixmap($videoborder_pixmap, false) }
         da2.signal_connect('realize') { da2.window.set_back_pixmap($videoborder_pixmap, false) }
     else
-        entry.button = Gtk::Button.new.set_image(entry.image)
+        entry.button = Gtk::Button.new.add(entry.image)
     end
-    Gtk::Tooltips.new.set_tip(entry.button, entry.get_beautified_name, nil)
+    tips.set_tip(entry.button, entry.get_beautified_name, nil)
     $imagesline.pack_start(entry.alignment = Gtk::Alignment.new(0.5, 1, 0, 0).add(entry.button).show_all, false, false)
     entry.button.signal_connect('clicked') {
-        if (last_shown = $mainview.get_shown_entry) != entry
+        shown = $mainview.get_shown_entry
+        if shown != entry
+            shown and shown.alignment.set(0.5, 1, 0, 0)
             entry.alignment.set(0.5, 0, 0, 0)
-            last_shown and last_shown.alignment.set(0.5, 1, 0, 0)
+            autoscroll_if_needed(entry.button, false)
             $mainview.set_shown_entry(entry)
-            sb_msg(_("Selected %s") % entry.get_beautified_name)
         end
     }
     entry.button.signal_connect('button-press-event') { |w, event|
@@ -977,32 +1231,37 @@ def show_entry(entry, i)
             video_view(entry)
         end
     }
-    entry.button.signal_connect('focus-in-event') { entry.button.clicked; autoscroll_if_needed(entry.button) }
+    entry.button.signal_connect('focus-in-event') { entry.button.clicked }
     entry.button.signal_connect('key-press-event') { |w, e| thumbnail_keypressed(entry, e) }
     if i == 0
         entry.button.grab_focus
     end
     update_visibility(entry)
-    Gtk.main_iteration while Gtk.events_pending?
 end
 
 def show_entries(allentries)
+    update_counters
     sb_msg(_("Loading images..."))
     $loading_progressbar.fraction = 0
-    $loading_progressbar.text = utf8(_("Loading... %d%") % 0)
+    $loading_progressbar.text = utf8(_("Loading... %d%%") % 0)
     $loading_progressbar.show
     t1 = Time.now
     total_loaded_files = 0
     total_loaded_size = 0
     i = 0
+    tips = Gtk::Tooltips.new
     while i < allentries.size
-#        printf "%d %s\n", i, __LINE__
-        entry = allentries[i]
-        if i == 0
-            loaded_pixbuf = entry.pixbuf_main
-        else
-            loaded_pixbuf = entry.pixbuf_thumbnail
+        begin
+            entry = allentries[i]
+            if i == 0
+                loaded_pixbuf = entry.pixbuf_main
+            else
+                loaded_pixbuf = entry.pixbuf_thumbnail
+            end
+        rescue InterruptedLoading
+            redo
         end
+
         if $allentries != allentries
             #- loaded another directory while this one was not yet finished
             msg 3, "allentries differ, stopping this deprecated load"
@@ -1010,7 +1269,7 @@ def show_entries(allentries)
         end
 
         if loaded_pixbuf
-            show_entry(entry, i)
+            show_entry(entry, i, tips)
             if $allentries != allentries
                 #- loaded another directory while this one was not yet finished
                 msg 3, "allentries differ, stopping this deprecated load"
@@ -1018,14 +1277,14 @@ def show_entries(allentries)
             end
 
             total_loaded_size += file_size(entry.path)
-            if i % 4 == 0
-                check_memory_free_cache_if_needed
-            end
             total_loaded_files += 1
             i += 1
             if i > $config['preload-distance'].to_i && i <= $config['preload-distance'].to_i * 2
-                #- when we're at preload distance, beging preloading to preload distance
-                allentries[i - $config['preload-distance'].to_i].pixbuf_main
+                #- when we're at preload distance, begin preloading to preload distance
+                begin
+                    allentries[i - $config['preload-distance'].to_i].pixbuf_main
+                rescue InterruptedLoading
+                end
             end
             if i == $config['preload-distance'].to_i * 2 + 1
                 #- when we're after double preload distance, activate normal preloading
@@ -1036,11 +1295,17 @@ def show_entries(allentries)
             allentries.delete_at(i)
         end
         $loading_progressbar.fraction = i.to_f / allentries.size
-        $loading_progressbar.text = utf8(_("Loading... %d%") % (100 * $loading_progressbar.fraction))
+        $loading_progressbar.text = utf8(_("Loading... %d%%") % (100 * $loading_progressbar.fraction))
+        if $quit
+            return
+        end
+        if i % 25 == 0
+            gc
+        end
     end
+    $preloader_allowed = true
     if i <= $config['preload-distance'].to_i * 2
         #- not yet preloaded correctly
-        $preloader_allowed = true
         run_preloader
     end
     sb_msg(_("%d images of total %s kB loaded in %3.2f seconds.") % [ total_loaded_files, commify(total_loaded_size / 1024), Time.now - t1 ])
@@ -1054,41 +1319,64 @@ def reset_all
     $mainview.set_shown_entry(nil)
     sb_msg(nil)
     $preloader_allowed = false
+    $execute.sensitive = false
 end
 
-def open_dir(path)
+def open_dir(*paths)
     #- remove visual stuff, so that user will see something is happening
     reset_all
-    sb_msg(_("Scanning source directory..."))
+    sb_msg(_("Scanning source directory... %s") % "")
     Gtk.main_iteration while Gtk.events_pending?
 
-    path = File.expand_path(path.sub(%r|/$|, ''))
-    examined_dirs = `find '#{path}' -type d -follow`.sort.collect { |v| v.chomp }
-    #- validate first
-    examined_dirs.each { |dir|
-        if dir =~ /'/
-            return utf8(_("Source directory or sub-directories can't contain a single-quote character, sorry: %s") % dir)
-        end
-        Dir.entries(dir).each { |file|
-            if file =~ /'/ && type = entry2type(file) && type == 'video'
-                return utf8(_("Videos can't contain a single quote character ('), sorry: %s") % "#{dir}/#{file}")
-            end
-        }
-    }
+    for path in paths
+        path = File.expand_path(path.sub(%r|/$|, ''))
+        $workingdir = path
+        entries = []
+        if File.directory?(path)
+            examined_dirs = `find '#{path}' -type d -follow`.split("\n").sort.collect { |v| v.chomp }
+            #- validate first
+            examined_dirs.each { |dir|
+                if dir =~ /'/
+                    return utf8(_("Source directory or sub-directories can't contain a single-quote character, sorry: %s") % dir)
+                end
+                begin
+                    Dir.entries(dir).each { |file|
+                        if file =~ /'/ && type = entry2type(file) && type == 'video'
+                            return utf8(_("Videos can't contain a single quote character ('), sorry: %s") % "#{dir}/#{file}")
+                        end
+                    }
+                rescue
+                    puts "Failed to open directory #{dir}: #{$!}"
+                end
+            }
+            
+            #- scan for populate second
+            examined_dirs.each { |dir|
+                if File.basename(dir) =~ /^\./
+                    msg 1, _("Ignoring directory %s, begins with a dot (indicating a hidden directory)") % dir
+                    next
+                end
+                begin
+                    entries += Dir.entries(dir).collect { |file| File.join(dir, file) }
+                rescue
+                    #- already puts'ed 10 lines upper
+                end
+                sb_msg(_("Scanning source directory... %s") % (_("%d entries found") % entries.size))
+                Gtk.main_iteration while Gtk.events_pending?
+            }
 
-    #- scan for populate second
-    examined_dirs.each { |dir|
-        if File.basename(dir) =~ /^\./
-            msg 1, _("Ignoring directory %s, begins with a dot (indicating a hidden directory)") % dir
-            next
+        else
+            entries << path
         end
-        entries = Dir.entries(dir)
+
         if $sort_by_exif_date
             dates = {}
             entries.each { |file|
-                date_time = Exif.datetimeoriginal(File.join(dir, file))
+                date_time = Exif.datetimeoriginal(file)
                 if ! date_time.nil?
                     dates[file] = date_time
+                elsif file =~ /(20\d{2}).?(\d{2}).?(\d{2}).(\d{2}).?(\d{2}).?(\d{2})/
+                    dates[file] = "#$1:#$2:#$3 #$4:#$5:#$6"
                 end
             }
             entries = smartsort(entries, dates)
@@ -1098,11 +1386,14 @@ def open_dir(path)
         entries.each { |file|
             type = entry2type(file)
             if type
-                $allentries << Entry.new(File.join(dir, file), type)
+                if File.directory?(path)
+                    $allentries << Entry.new(file, type, file[path.length + 1 .. -1])
+                else
+                    $allentries << Entry.new(file, type, file)
+                end
             end
         }
-    }
-    $workingdir = path
+    end
     return nil
 end
 
@@ -1119,7 +1410,7 @@ def open_dir_popup
     ok = false
     load = false
     while !ok
-        if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
+        if fc.run == Gtk::Dialog::RESPONSE_ACCEPT && fc.filename
             msg = open_dir(fc.filename)
             if msg
                 show_popup(fc, msg)
@@ -1139,7 +1430,15 @@ def open_dir_popup
 end
 
 def try_quit(*options)
-    Gtk.main_quit
+    if ! $allentries.detect { |e| e.removed || e.labeled } || show_popup($main_window,
+                                                                         utf8(_("You have not executed the classification. Are you sure you want to quit?")),
+                                                                         { :okcancel => true })
+        Gtk.main_quit
+        $quit = true
+        return false
+    else
+        return true
+    end
 end
 
 def execute
@@ -1150,6 +1449,8 @@ def execute
     label = Gtk::Label.new.set_markup(utf8(_("You're about to <b>execute</b> actions on the marked images.\nPlease confirm below the actions. You cannot undo this operation!")))
     vb1.pack_start(label, false, false)
 
+    lastpath = $workingdir
+
     table = Gtk::Table.new(0, 0, false)
     table.set_row_spacings(5)
     table.set_column_spacings(5)
@@ -1183,14 +1484,18 @@ def execute
         table.attach(Gtk::Label.new(counter.to_s).set_justify(Gtk::Justification::CENTER), 1, 2, row, row + 1, 0, 0, 5, 5)
         table.attach(examples, 2, 3, row, row + 1, Gtk::FILL, Gtk::FILL, 5, 5)
 
+        if counter == 0
+            next {}
+        end
+
         combostore = Gtk::ListStore.new(Gdk::Pixbuf, String)
         iter = combostore.append
         if normal
-            iter[0] = $main_window.render_icon(Gtk::Stock::GO_FORWARD, Gtk::IconSize::MENU)
-            iter[1] = utf8(_("Move to:"))
-            iter = combostore.append
             iter[0] = $main_window.render_icon(Gtk::Stock::PASTE, Gtk::IconSize::MENU)
             iter[1] = utf8(_("Copy to:"))
+            iter = combostore.append
+            iter[0] = $main_window.render_icon(Gtk::Stock::GO_FORWARD, Gtk::IconSize::MENU)
+            iter[1] = utf8(_("Move to:"))
         else
             iter[0] = $main_window.render_icon(Gtk::Stock::DELETE, Gtk::IconSize::MENU)
             iter[1] = utf8(_("Permanently remove"))
@@ -1209,7 +1514,6 @@ def execute
 
         if normal
             pathbutton = Gtk::Button.new.add(pathlabel = Gtk::Label.new.set_markup(utf8(_("<i>(unset)</i>"))))
-            lastpath = $workingdir
             pathbutton.signal_connect('clicked') {
                 fc = Gtk::FileChooserDialog.new(utf8(_("Specify the directory where to move the pictures to")),
                                                 nil,
@@ -1217,9 +1521,12 @@ def execute
                                                 nil,
                                                 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
                 fc.transient_for = dialog
-                fc.current_folder = lastpath
-                if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
+                if lastpath
+                    fc.current_folder = lastpath
+                end
+                if fc.run == Gtk::Dialog::RESPONSE_ACCEPT && fc.filename
                     pathlabel.text = fc.filename
+                    pathlabel.set_alignment(0, 0.5)
                 end
                 lastpath = fc.filename
                 fc.destroy
@@ -1239,7 +1546,7 @@ def execute
     }
     stuff = {}
     stuff['toremove'] = add_row.call(1, utf8(_("<i>to remove</i>")), $color_red, proc { |entry| entry.removed }, false)
-    $labels.values.each_with_index { |label, row| stuff[label] = add_row.call(row + 2, label.name, label.color, proc { |entry| entry.labeled == label }, true) }
+    $ordered_labels.each_with_index { |label, row| stuff[label] = add_row.call(row + 2, label.name, label.color, proc { |entry| entry.labeled == label }, true) }
     vb1.pack_start(sw = Gtk::ScrolledWindow.new(nil, nil).add_with_viewport(table).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC), true, true)
 
     toremove_amount = $allentries.find_all { |entry| entry.removed }.size
@@ -1265,16 +1572,28 @@ def execute
     while true
         dialog.run { |response|        
             if response == Gtk::Dialog::RESPONSE_OK
-                if toremove_amount > 0 && ! check_removal.active? && stuff['toremove'][:combo].active == 0
-                    show_popup(dialog, utf8(_("You have not confirmed that you noticed the permanent removal of the pictures marked for deletion.")))
-                    break
-                end
                 problem = false
+                if toremove_amount > 0 && stuff['toremove'][:combo].active == 0
+                    if ! check_removal.active?
+                        show_popup(dialog, utf8(_("You have not confirmed that you noticed the permanent removal of the pictures marked for deletion.")))
+                        problem = true
+                        break
+                    end
+                    $allentries.each { |entry|
+                        if entry.removed
+                            if ! FileTest.writable?(entry.path)
+                                show_popup(dialog, utf8(_("Sorry, permission denied to remove '%s'.") % [ entry.path ]))
+                                problem = true
+                                break
+                            end
+                        end
+                    }
+                end
                 label2entries = {}
                 $labels.values.each { |label| label2entries[label] = [] }
                 $allentries.each { |entry| entry.labeled and label2entries[entry.labeled] << entry }
                 stuff.keys.each { |key|
-                    if key.is_a?(Label) && stuff[key][:combo].active <= 1
+                    if key.is_a?(Label) && stuff[key][:combo] && stuff[key][:combo].active <= 1
                         destination = stuff[key][:pathlabel].text
                         if destination[0] != ?/
                             show_popup(dialog, utf8(_("You have not selected a directory where to move/copy %s.") % key.name))
@@ -1283,7 +1602,7 @@ def execute
                         end
                         begin
                             Dir.mkdir(destination)
-                        rescue Errno::EEXIST
+                        rescue
                         end
                         begin
                             st = File.stat(destination)
@@ -1292,7 +1611,7 @@ def execute
                             problem = true
                             break
                         end
-                        if ! st.directory? || ! st.writable?
+                        if ! st.directory? || ! writable(destination)
                             show_popup(dialog, utf8(_("Directory %s, where to move/copy %s, is not valid or not writable.") % [destination, key.name]))
                             problem = true
                             break
@@ -1306,6 +1625,15 @@ def execute
                             rescue
                             end
                         }
+                        if stuff[key][:combo].active == 1
+                            label2entries[key].each { |entry|
+                                if ! FileTest.writable?(entry.path)
+                                    show_popup(dialog, utf8(_("Sorry, permission denied to move '%s'.") % [ entry.path ]))
+                                    problem = true
+                                    break
+                                end
+                            }
+                        end
                         if problem
                             break
                         end
@@ -1315,22 +1643,42 @@ def execute
                     begin
                         moved = 0
                         copied = 0
+                        ignored_errors = []
                         stuff.keys.each { |key|
-                            if key.is_a?(Label) && stuff[key][:combo].active <= 1
+                            if key.is_a?(Label) && stuff[key][:combo] && stuff[key][:combo].active <= 1
                                 destination = stuff[key][:pathlabel].text
                                 label2entries[key].each { |entry|
                                     if stuff[key][:combo].active == 0
-                                        File.rename(entry.path, File.join(destination, File.basename(entry.path)))
-                                        moved += 1
+                                        result = `cp -dp '#{entry.path}' '#{destination}' 2>&1`
                                     elsif stuff[key][:combo].active == 1
-                                        system("cp -dp '#{entry.path}' '#{destination}'")
-                                        copied += 1
+                                        result = `mv '#{entry.path}' '#{destination}' 2>&1`
+                                    end
+                                    if $?.exitstatus > 0
+                                        simplified_error = result.sub(/#{Regexp.quote(destination + '/' + File.basename(entry.path))}/, '').  #'
+                                                                  sub(/#{Regexp.quote(entry.path)}/, '').
+                                                                  sub(/#{Regexp.quote(File.basename(entry.path))}/, '')
+                                        if ! ignored_errors.include?(simplified_error)
+                                            response = show_popup($main_window,
+                                                                  utf8(_("Failure:\n\n%s\nDo you wish to continue?" % result)),
+                                                                  { :yestoall => true })
+                                            if response == 'no'
+                                                raise "failure on '#{entry.path}'"
+                                            elsif response == 'yestoall'
+                                                ignored_errors << simplified_error
+                                            end
+                                        end
+                                    else
+                                        if stuff[key][:combo].active == 0
+                                            copied += 1
+                                        else
+                                            moved += 1
+                                        end
                                     end
                                 }
                             end
                         }
                         removed = 0
-                        if stuff['toremove'][:combo].active == 0
+                        if stuff['toremove'][:combo] && stuff['toremove'][:combo].active == 0
                             $allentries.each { |entry|
                                 if entry.removed
                                     File.delete(entry.path)
@@ -1339,8 +1687,7 @@ def execute
                             }
                         end
                     rescue
-                        msg 1, "woops: #{$!}"
-                        show_popup(dialog, utf8(_("Unexpected system call error: '%s'.") % $!))
+                        msg 1, "woops: #{$!}\n" + $@.join("\n")
                     end
                     show_popup(dialog, utf8(_("Successfully moved %d files, copied %d file, and removed %d files.") % [ moved, copied, removed ]))
                     dialog.destroy
@@ -1356,40 +1703,54 @@ def execute
     end
 end
 
-def update_visibility(entry)
+def visible(entry)
+    if ! entry
+        #- just "executed"
+        return
+    end
     if ! entry.button
         #- not yet loaded
         return
     end
     if entry.labeled
         if entry.labeled.button.active?
-            entry.button.show
+            return true
         else
-            entry.button.hide
+            return false
         end
     elsif entry.removed
         if $toremove_button.active?
-            entry.button.show
+            return true
         else
-            entry.button.hide
+            return false
         end
     else
         if $unlabelled_button.active?
-            entry.button.show
+            return true
         else
-            entry.button.hide
+            return false
         end
     end
 end
+
+def update_visibility(entry)
+    v = visible(entry)
+    if v.nil?
+        return
+    end
+    if v
+        entry.button.show
+    else
+        entry.button.hide
+    end
+end
         
-def update_all_visibilities
+def update_all_visibilities_aux
     $allentries.each { |entry|
         update_visibility(entry)
     }
     shown = $mainview.get_shown_entry
-    if shown.nil?
-        return
-    end
+    shown or return
     while shown.button && ! shown.button.visible? && shown != $allentries.last
         shown = $allentries[$allentries.index(shown) + 1]
     end 
@@ -1405,6 +1766,14 @@ def update_all_visibilities
     }
 end
 
+def update_all_visibilities
+    update_all_visibilities_aux
+    Gtk.main_iteration while Gtk.events_pending?
+    shown = $mainview.get_shown_entry
+    shown and autoscroll_if_needed(shown.button, false)
+end
+
+
 def preferences
     dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
                              $main_window,
@@ -1413,30 +1782,42 @@ def preferences
                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
 
     tooltips = Gtk::Tooltips.new
-    
+
+    table_y = 0
+
     dialog.vbox.add(tbl = Gtk::Table.new(0, 0, false))
     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
-               0, 1, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
+               0, 1, table_y, table_y + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
     tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(video_viewer_entry = Gtk::Entry.new.set_text($config['video-viewer']).set_size_request(250, -1)),
-               1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
+               1, 2, table_y, table_y + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
     tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename;\nfor example: /usr/bin/mplayer %f")), nil)
 
+    table_y += 1
     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Browser's command: ")))),
-               0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
+               0, 1, table_y, table_y + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
     tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(browser_entry = Gtk::Entry.new.set_text($config['browser'])),
-               1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
+               1, 2, table_y, table_y + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
     tooltips.set_tip(browser_entry, utf8(_("Use %f to specify the filename;\nfor example: /usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f")), nil)
 
+    table_y += 1
+    tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Thumbnails height: ")))),
+               0, 1, table_y, table_y + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
+    tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(thumbnails_height = Gtk::SpinButton.new(32, 256, 16).set_value($config['thumbnails-height'].to_i)),
+               1, 2, table_y, table_y + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
+    tooltips.set_tip(thumbnails_height, utf8(_("The desired height of the thumbnails in the thumbnails line of the bottom")), nil)
+
+    table_y += 1
     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Preloading distance: ")))),
-               0, 1, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
+               0, 1, table_y, table_y + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
     tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(preload_distance = Gtk::SpinButton.new(0, 50, 1).set_value($config['preload-distance'].to_i)),
-               1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
+               1, 2, table_y, table_y + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
     tooltips.set_tip(preload_distance, utf8(_("Amount of pictures preloaded left and right to the currently shown")), nil)
 
+    table_y += 1
     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Cache memory use: ")))),
-               0, 1, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
+               0, 1, table_y, table_y + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
     tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(cache_vbox = Gtk::VBox.new(false, 0)),
-               1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
+               1, 2, table_y, table_y + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
     cache_vbox.pack_start(Gtk::HBox.new(false, 0).pack_start(cache_memfree_radio = Gtk::RadioButton.new(''), false, false).
                                                   pack_start(cache_memfree_spin = Gtk::SpinButton.new(0, 100, 10), false, false).
                                                   pack_start(cache_memfree_label = Gtk::Label.new(utf8(_("% of free memory"))), false, false), false, false)
@@ -1459,11 +1840,12 @@ def preferences
     if $config['cache-memory-use'] =~ /memfree_(\d+)/
         cache_memfree_spin.value = $1.to_i
     else
-        cache_specify_spin.value = $config['cache-memory-use'].to_i
+        cache_specify_spin.value = $config['cache-memory-use'].to_i / 1024
     end
 
+    table_y += 1
     tbl.attach(update_exif_orientation_check = Gtk::CheckButton.new(utf8(_("Update file's EXIF orientation when rotating a picture"))),
-               0, 2, 4, 5, Gtk::FILL, Gtk::SHRINK, 2, 2)
+               0, 2, table_y, table_y + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
     tooltips.set_tip(update_exif_orientation_check, utf8(_("When rotating a picture (Alt-Right/Left), also update EXIF orientation in the file itself")), nil)
     update_exif_orientation_check.active = $config['rotate-set-exif'] == 'true'
 
@@ -1472,12 +1854,13 @@ def preferences
         if response == Gtk::Dialog::RESPONSE_OK
             $config['video-viewer'] = from_utf8(video_viewer_entry.text)
             $config['browser'] = from_utf8(browser_entry.text)
+            $config['thumbnails-height'] = thumbnails_height.value
             $config['preload-distance'] = preload_distance.value
             $config['rotate-set-exif'] = update_exif_orientation_check.active?.to_s
             if cache_memfree_radio.active?
                 $config['cache-memory-use'] = "memfree_#{cache_memfree_spin.value}%"
             else
-                $config['cache-memory-use'] = cache_specify_spin.value
+                $config['cache-memory-use'] = cache_specify_spin.value.to_i * 1024
             end
             set_cache_memory_use_figure
         end
@@ -1550,7 +1933,7 @@ def create_menubar
         show_popup($main_window, utf8(_("<span size='large' weight='bold'>Help</span>
 
 1. Open a directory with <span foreground='darkblue'>File/Open</span>; the classifier will scan it (including subdirectories) and
-show thumbnails for all images and videos at the bottom.
+show thumbnails for all photos and videos at the bottom.
 
 2. You can then navigate through images with the <span foreground='darkblue'>Left/Right</span> keyboard keys, or by <span foreground='darkblue'>clicking</span>
 on thumbnails.
@@ -1564,7 +1947,8 @@ name of this label, and what color you want. To clear the current label, hit the
 some of them by unchecking the labels checkboxes on the left.
 
 5. Once you're finished reviewing all thumbnails, use <span foreground='darkblue'>File/Execute</span> to execute the desired
-actions on labels.
+actions according to associated labels. You can permanently remove (or not) images with
+the <i>to remove</i> label, and copy or move images with the labels you defined.
 ")), { :pos_centered => true, :not_transient => true })
     }
     speed.signal_connect('activate') {
@@ -1577,11 +1961,12 @@ actions on labels.
 <span foreground='darkblue'>Space</span>: clear any label on current image
 <span foreground='darkblue'>Control-z</span>: undo
 <span foreground='darkblue'>Control-r</span>: redo
+<span foreground='darkblue'>Control-Space</span>: recenter thumbnails on current item
 
 Any alphabetical key will assign (or popup for) the associated label on current image.
 ")), { :pos_centered => true, :not_transient => true })
     }
-    tutos.signal_connect('activate') { open_url('http://booh.org/tutorial.html') }
+    tutos.signal_connect('activate') { open_url('http://booh.org/tutorial') }
     about.signal_connect('activate') { call_about }
 
 
@@ -1596,12 +1981,17 @@ def reset_labels
     end
     $labels_vbox.pack_start(Gtk::Label.new(utf8(_("Labels list:"))).set_justify(Gtk::Justification::CENTER), false, false).show_all
     $labels = {}
+    $ordered_labels = []
     lbl = Gtk::Label.new.set_markup(utf8(_("<i>unlabelled</i>")))
-    $labels_vbox.pack_start($unlabelled_button = Gtk::CheckButton.new.add(Gtk::EventBox.new.add(lbl)).show_all)
+    $labels_vbox.pack_start(Gtk::HBox.new(false, 5).pack_start($unlabelled_button = Gtk::CheckButton.new.add(Gtk::EventBox.new.add(lbl)), false, false).
+                                                    pack_start(Gtk::Label.new, true, true).  #- I suck
+                                                    pack_start($unlabelled_counter = Gtk::Label.new.set_markup('<tt>0</tt>'), false, false).show_all)
     $unlabelled_button.active = true
     $unlabelled_button.signal_connect('toggled') { update_all_visibilities }
     lbl = Gtk::Label.new.set_markup(utf8(_("<i>to remove</i>")))
-    $labels_vbox.pack_start($toremove_button = Gtk::CheckButton.new.add(evt = Gtk::EventBox.new.add(lbl)).show_all)
+    $labels_vbox.pack_start(Gtk::HBox.new(false, 5).pack_start($toremove_button = Gtk::CheckButton.new.add(evt = Gtk::EventBox.new.add(lbl)), false, false).
+                                                    pack_start(Gtk::Label.new, true, true).
+                                                    pack_start($toremove_counter = Gtk::Label.new.set_markup('<tt>0</tt>'), false, false).show_all)
     $toremove_button.active = true
     $toremove_button.signal_connect('toggled') { update_all_visibilities }
     evt.modify_bg(Gtk::StateType::NORMAL, $color_red)
@@ -1609,7 +1999,14 @@ def reset_labels
     evt.modify_bg(Gtk::StateType::ACTIVE, $color_red.lighter)
 end
 
+def cleanup_loaders
+    $allentries.each { |e| 
+        e.cancel_loader
+    }
+end
+
 def reset_thumbnails
+    cleanup_loaders
     $allentries = []
     if $preloader_running
         $preloader_force_exit = true
@@ -1617,11 +2014,16 @@ def reset_thumbnails
     for child in $imagesline.children
         $imagesline.remove(child)
     end
+    set_imagesline_size_request
+end
+
+def set_imagesline_size_request
+    $imagesline.set_size_request(-1, Gtk::Button.new.size_request[1] + Entry.thumbnails_height + Entry.thumbnails_height/4)
 end
 
 def create_main_window
 
-    $videoborder_pixbuf = Gdk::Pixbuf.new("#{$FPATH}/images/video_border.png")
+    $videoborder_pixbuf = GdkPixbuf::Pixbuf.new(:file => "#{$FPATH}/images/video_border.png")
     $videoborder_pixmap, = $videoborder_pixbuf.render_pixmap_and_mask(0)
 
     mb = create_menubar
@@ -1631,9 +2033,7 @@ def create_main_window
     mainview_hbox = Gtk::HBox.new
     mainview_hbox.pack_start(Gtk::Alignment.new(0.5, 0, 1, 1).add(left_vbox = Gtk::VBox.new(false, 5)), false, true)
     left_vbox.pack_start(($labels_vbox = Gtk::VBox.new(false, 5)), false, true)
-    $labels_vbox.pack_start(Gtk::Label.new(utf8(_("Labels list:"))).set_justify(Gtk::Justification::CENTER), false, false).show_all
-    left_vbox.pack_end($loading_progressbar = Gtk::ProgressBar.new.set_text(utf8(_("Loading... %d%") % 0)), false, true)
-    reset_labels
+    left_vbox.pack_end($loading_progressbar = Gtk::ProgressBar.new.set_text(utf8(_("Loading... %d%%") % 0)), false, true)
     mainview_hbox.pack_start($mainview = MainView.new, true, true)
     main_vbox.pack_start(mainview_hbox, true, true)
     $imagesline_sw = Gtk::ScrolledWindow.new(nil, nil)
@@ -1642,12 +2042,12 @@ def create_main_window
     main_vbox.pack_start($imagesline_sw, false, false)
     main_vbox.pack_end($statusbar = Gtk::Statusbar.new, false, false)
 
-    $imagesline.set_size_request(-1, Gtk::Button.new.size_request[1] + Entry.thumbnails_height + 15)
+    set_imagesline_size_request
 
-    $main_window = Gtk::Window.new
+    $main_window = create_window
     $main_window.add(main_vbox)
     $main_window.signal_connect('delete-event') {
-        try_quit({ :disallow_cancel => true })
+        try_quit
     }
 
     #- read/save size and position of window
@@ -1657,7 +2057,7 @@ def create_main_window
         $main_window.window_position = Gtk::Window::POS_CENTER
     end
     msg 3, "size: #{$config['width']}x#{$config['height']}"
-    $main_window.set_default_size(($config['width'] || 700).to_i, ($config['height'] || 600).to_i)
+    $main_window.set_default_size(($config['width'] || 800).to_i, ($config['height'] || 600).to_i)
     $main_window.signal_connect('configure-event') {
         msg 3, "configure: pos: #{$main_window.window.root_origin.inspect} size: #{$main_window.window.size.inspect}"
         x, y = $main_window.window.root_origin
@@ -1683,12 +2083,17 @@ create_main_window
 check_config
 
 if ARGV[0]
-    if msg = open_dir(ARGV[0])
+    if msg = open_dir(*ARGV)
         puts msg
     else
-        show_entries($allentries)
+        Gtk.idle_add {
+            show_entries($allentries)
+            false
+        }
     end
 end
 Gtk.main
 
+cleanup_loaders
+
 write_config