summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--ranger/__main__.py2
-rw-r--r--ranger/core/actions.py47
-rw-r--r--ranger/core/fm.py15
-rw-r--r--ranger/core/loader.py85
-rw-r--r--ranger/ext/shutil_generatorized.py302
-rw-r--r--ranger/fsobject/directory.py3
6 files changed, 116 insertions, 338 deletions
diff --git a/ranger/__main__.py b/ranger/__main__.py
index 0ded8c95..a9a18537 100644
--- a/ranger/__main__.py
+++ b/ranger/__main__.py
@@ -231,7 +231,7 @@ def main():
 		return error.args[0]
 	finally:
 		try:
-			fm.ui.destroy()
+			fm.destroy()
 		except (AttributeError, NameError):
 			pass
 		if crash_traceback:
diff --git a/ranger/core/actions.py b/ranger/core/actions.py
index b88849ef..1493d84d 100644
--- a/ranger/core/actions.py
+++ b/ranger/core/actions.py
@@ -28,8 +28,7 @@ from ranger.ext.shell_escape import shell_quote
 from ranger import fsobject
 from ranger.shared import FileManagerAware, EnvironmentAware, SettingsAware
 from ranger.fsobject import File
-from ranger.ext import shutil_generatorized as shutil_g
-from ranger.core.loader import LoadableObject
+from ranger.core.loader import CommandLoader
 
 class _MacroTemplate(string.Template):
 	"""A template for substituting macros in commands"""
@@ -679,11 +678,12 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
 		if not copied_files:
 			return
 
+		def refresh(_):
+			cwd = self.env.get_directory(original_path)
+			cwd.load_content()
+
 		original_path = self.env.cwd.path
-		try:
-			one_file = copied_files[0]
-		except:
-			one_file = None
+		one_file = copied_files[0]
 
 		if self.env.cut:
 			self.env.copy.clear()
@@ -692,36 +692,21 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
 				descr = "moving: " + one_file.path
 			else:
 				descr = "moving files from: " + one_file.dirname
-			def generate():
-				for f in copied_files:
-					for _ in shutil_g.move(src=f.path,
-							dst=original_path,
-							overwrite=overwrite):
-						yield
-				cwd = self.env.get_directory(original_path)
-				cwd.load_content()
+			obj = CommandLoader(args=['mv', '--backup=existing',
+					'--suffix=_', '-ft', self.env.cwd.path] + \
+					[f.path for f in copied_files],
+					descr=descr, end_hook=refresh)
 		else:
 			if len(copied_files) == 1:
 				descr = "copying: " + one_file.path
 			else:
 				descr = "copying files from: " + one_file.dirname
-			def generate():
-				for f in self.env.copy:
-					if isdir(f.path):
-						for _ in shutil_g.copytree(src=f.path,
-								dst=join(original_path, f.basename),
-								symlinks=True,
-								overwrite=overwrite):
-							yield
-					else:
-						for _ in shutil_g.copy2(f.path, original_path,
-								symlinks=True,
-								overwrite=overwrite):
-							yield
-				cwd = self.env.get_directory(original_path)
-				cwd.load_content()
-
-		self.loader.add(LoadableObject(generate(), descr))
+			obj = CommandLoader(args=['cp', '--backup=existing', '--archive',
+					'--suffix=_', '-frt', self.env.cwd.path] + \
+					[f.path for f in self.env.copy],
+					descr=descr, end_hook=refresh)
+
+		self.loader.add(obj)
 
 	def delete(self):
 		self.notify("Deleting!", duration=1)
diff --git a/ranger/core/fm.py b/ranger/core/fm.py
index dfad3425..d2aa00ec 100644
--- a/ranger/core/fm.py
+++ b/ranger/core/fm.py
@@ -94,6 +94,21 @@ class FM(Actions, SignalDispatcher):
 
 		self.env.signal_bind('cd', self._update_current_tab)
 
+	def destroy(self):
+		debug = ranger.arg.debug
+		if self.ui:
+			try:
+				self.ui.destroy()
+			except:
+				if debug:
+					raise
+		if self.loader:
+			try:
+				self.loader.destroy()
+			except:
+				if debug:
+					raise
+
 	def block_input(self, sec=0):
 		self.input_blocked = sec != 0
 		self.input_blocked_until = time() + sec
diff --git a/ranger/core/loader.py b/ranger/core/loader.py
index 4f4424e4..e33c2e9e 100644
--- a/ranger/core/loader.py
+++ b/ranger/core/loader.py
@@ -14,9 +14,13 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 from collections import deque
-from time import time
-from ranger.shared import FileManagerAware
+from time import time, sleep
+from subprocess import Popen, PIPE
 import math
+import os
+import select
+
+from ranger.shared import FileManagerAware
 
 def status_generator():
 	"""Generate a rotating line which can be used as a throbber"""
@@ -26,7 +30,9 @@ def status_generator():
 		yield '\\'
 		yield '|'
 
-class LoadableObject(object):
+
+class Loadable(object):
+	paused = False
 	def __init__(self, gen, descr):
 		self.load_generator = gen
 		self.description = descr
@@ -34,6 +40,69 @@ class LoadableObject(object):
 	def get_description(self):
 		return self.description
 
+	def pause(self):
+		self.paused = True
+
+	def unpause(self):
+		try:
+			del self.paused
+		except:
+			pass
+
+	def destroy(self):
+		pass
+
+
+class CommandLoader(Loadable, FileManagerAware):
+	"""
+	Run an external command with the loader.
+
+	Output from stderr will be reported.  Ensure that the process doesn't
+	ever ask for input, otherwise the loader will be blocked until this
+	object is removed from the queue (type ^C in ranger)
+	"""
+	finished = False
+	def __init__(self, args, descr, begin_hook=None, end_hook=None):
+		Loadable.__init__(self, self.generate(), descr)
+		self.args = args
+		self.begin_hook = begin_hook
+		self.end_hook = end_hook
+
+	def generate(self):
+		self.process = process = Popen(self.args,
+				stdout=open(os.devnull, 'w'),
+				stderr=PIPE)
+		if self.begin_hook:
+			self.begin_hook(process)
+		while process.poll() is None:
+			try:
+				rd, _, __ = select.select(
+						[process.stderr], [], [], 0.05)
+				if rd:
+					error = process.stderr.readline().decode('utf-8')
+					self.fm.notify(error, bad=True)
+			except select.error:
+				pass
+			sleep(0.02)
+			yield
+		self.finished = True
+		if self.end_hook:
+			self.end_hook(process)
+
+	def pause(self):
+		if not self.finished and not self.paused:
+			self.process.send_signal(20)
+		Loadable.pause(self)
+
+	def unpause(self):
+		if not self.finished and self.paused:
+			self.process.send_signal(18)
+		Loadable.unpause(self)
+
+	def destroy(self):
+		if self.process:
+			self.process.kill()
+
 
 class Loader(FileManagerAware):
 	seconds_of_work_time = 0.03
@@ -70,6 +139,8 @@ class Loader(FileManagerAware):
 
 		if to == 0:
 			self.queue.appendleft(item)
+			if _from != 0:
+				self.queue[1].pause()
 		elif to == -1:
 			self.queue.append(item)
 		else:
@@ -89,6 +160,7 @@ class Loader(FileManagerAware):
 				item = self.queue[index]
 			if hasattr(item, 'unload'):
 				item.unload()
+			item.destroy()
 			del self.queue[index]
 
 	def work(self):
@@ -109,7 +181,10 @@ class Loader(FileManagerAware):
 
 		self.rotate()
 		if item != self.old_item:
+			if self.old_item:
+				self.old_item.pause()
 			self.old_item = item
+		item.unpause()
 
 		end_time = time() + self.seconds_of_work_time
 
@@ -125,3 +200,7 @@ class Loader(FileManagerAware):
 	def has_work(self):
 		"""Is there anything to load?"""
 		return bool(self.queue)
+
+	def destroy(self):
+		while self.queue:
+			self.queue.pop().destroy()
diff --git a/ranger/ext/shutil_generatorized.py b/ranger/ext/shutil_generatorized.py
deleted file mode 100644
index 436d2cb7..00000000
--- a/ranger/ext/shutil_generatorized.py
+++ /dev/null
@@ -1,302 +0,0 @@
-# This file was taken from the python standard library and has been
-# slightly modified to do a "yield" after every 16KB of copying
-"""Utility functions for copying files and directory trees.
-
-XXX The functions here don't copy the resource fork or other metadata on Mac.
-
-"""
-
-import os
-import sys
-import stat
-from os.path import abspath
-
-__all__ = ["copyfileobj","copyfile","copystat","copy2",
-           "copytree","move","rmtree","Error", "SpecialFileError"]
-
-APPENDIX = '_'
-
-class Error(EnvironmentError):
-    pass
-
-class SpecialFileError(EnvironmentError):
-    """Raised when trying to do a kind of operation (e.g. copying) which is
-    not supported on a special file (e.g. a named pipe)"""
-
-try:
-    WindowsError
-except NameError:
-    WindowsError = None
-
-def copyfileobj(fsrc, fdst, length=16*1024):
-    """copy data from file-like object fsrc to file-like object fdst"""
-    while 1:
-        buf = fsrc.read(length)
-        if not buf:
-            break
-        fdst.write(buf)
-        yield
-
-def _samefile(src, dst):
-    # Macintosh, Unix.
-    if hasattr(os.path,'samefile'):
-        try:
-            return os.path.samefile(src, dst)
-        except OSError:
-            return False
-
-    # All other platforms: check for same pathname.
-    return (os.path.normcase(abspath(src)) ==
-            os.path.normcase(abspath(dst)))
-
-def copyfile(src, dst):
-    """Copy data from src to dst"""
-    if _samefile(src, dst):
-        raise Error("`%s` and `%s` are the same file" % (src, dst))
-
-    fsrc = None
-    fdst = None
-    for fn in [src, dst]:
-        try:
-            st = os.stat(fn)
-        except OSError:
-            # File most likely does not exist
-            pass
-        else:
-            # XXX What about other special files? (sockets, devices...)
-            if stat.S_ISFIFO(st.st_mode):
-                raise SpecialFileError("`%s` is a named pipe" % fn)
-    try:
-        fsrc = open(src, 'rb')
-        fdst = open(dst, 'wb')
-        for _ in copyfileobj(fsrc, fdst):
-            yield
-    finally:
-        if fdst:
-            fdst.close()
-        if fsrc:
-            fsrc.close()
-
-def copystat(src, dst):
-    """Copy all stat info (mode bits, atime, mtime, flags) from src to dst"""
-    st = os.stat(src)
-    mode = stat.S_IMODE(st.st_mode)
-    if hasattr(os, 'utime'):
-        try: os.utime(dst, (st.st_atime, st.st_mtime))
-        except: pass
-    if hasattr(os, 'chmod'):
-        try: os.chmod(dst, mode)
-        except: pass
-    if hasattr(os, 'chflags') and hasattr(st, 'st_flags'):
-        try: os.chflags(dst, st.st_flags)
-        except: pass
-
-def copy2(src, dst, overwrite=False, symlinks=False):
-    """Copy data and all stat info ("cp -p src dst").
-
-    The destination may be a directory.
-
-    """
-    if os.path.isdir(dst):
-        dst = os.path.join(dst, os.path.basename(src))
-    if not overwrite:
-        dst = get_safe_path(dst)
-    if symlinks and os.path.islink(src):
-        linkto = os.readlink(src)
-        os.symlink(linkto, dst)
-    else:
-        for _ in copyfile(src, dst):
-            yield
-        copystat(src, dst)
-
-def get_safe_path(dst):
-    if not os.path.exists(dst):
-        return dst
-    if not dst.endswith(APPENDIX):
-        dst += APPENDIX
-        if not os.path.exists(dst):
-            return dst
-    n = 0
-    test_dst = dst + str(n)
-    while os.path.exists(test_dst):
-        n += 1
-        test_dst = dst + str(n)
-
-    return test_dst
-
-def copytree(src, dst, symlinks=False, ignore=None, overwrite=False):
-    """Recursively copy a directory tree using copy2().
-
-    The destination directory must not already exist.
-    If exception(s) occur, an Error is raised with a list of reasons.
-
-    If the optional symlinks flag is true, symbolic links in the
-    source tree result in symbolic links in the destination tree; if
-    it is false, the contents of the files pointed to by symbolic
-    links are copied.
-
-    The optional ignore argument is a callable. If given, it
-    is called with the `src` parameter, which is the directory
-    being visited by copytree(), and `names` which is the list of
-    `src` contents, as returned by os.listdir():
-
-        callable(src, names) -> ignored_names
-
-    Since copytree() is called recursively, the callable will be
-    called once for each directory that is copied. It returns a
-    list of names relative to the `src` directory that should
-    not be copied.
-
-    XXX Consider this example code rather than the ultimate tool.
-
-    """
-    names = os.listdir(src)
-    if ignore is not None:
-        ignored_names = ignore(src, names)
-    else:
-        ignored_names = set()
-
-    errors = []
-    try:
-        os.makedirs(dst)
-    except Exception as err:
-        if not overwrite:
-            dst = get_safe_path(dst)
-            os.makedirs(dst)
-    for name in names:
-        if name in ignored_names:
-            continue
-        srcname = os.path.join(src, name)
-        dstname = os.path.join(dst, name)
-        try:
-            if symlinks and os.path.islink(srcname):
-                linkto = os.readlink(srcname)
-                if os.path.lexists(dstname):
-                    if not os.path.islink(dstname) \
-                    or os.readlink(dstname) != linkto:
-                        os.unlink(dstname)
-                        os.symlink(linkto, dstname)
-            elif os.path.isdir(srcname):
-                for _ in copytree(srcname, dstname, symlinks,
-                        ignore, overwrite):
-                    yield
-            else:
-                # Will raise a SpecialFileError for unsupported file types
-                for _ in copy2(srcname, dstname,
-                        overwrite=overwrite, symlinks=symlinks):
-                    yield
-        # catch the Error from the recursive copytree so that we can
-        # continue with other files
-        except Error as err:
-            errors.extend(err.args[0])
-        except EnvironmentError as why:
-            errors.append((srcname, dstname, str(why)))
-    try:
-        copystat(src, dst)
-    except OSError as why:
-        if WindowsError is not None and isinstance(why, WindowsError):
-            # Copying file access times may fail on Windows
-            pass
-        else:
-            errors.extend((src, dst, str(why)))
-    if errors:
-        raise Error(errors)
-
-def rmtree(path, ignore_errors=False, onerror=None):
-    """Recursively delete a directory tree.
-
-    If ignore_errors is set, errors are ignored; otherwise, if onerror
-    is set, it is called to handle the error with arguments (func,
-    path, exc_info) where func is os.listdir, os.remove, or os.rmdir;
-    path is the argument to that function that caused it to fail; and
-    exc_info is a tuple returned by sys.exc_info().  If ignore_errors
-    is false and onerror is None, an exception is raised.
-
-    """
-    if ignore_errors:
-        def onerror(*args):
-            pass
-    elif onerror is None:
-        def onerror(*args):
-            raise
-    try:
-        if os.path.islink(path):
-            # symlinks to directories are forbidden, see bug #1669
-            raise OSError("Cannot call rmtree on a symbolic link")
-    except OSError:
-        onerror(os.path.islink, path, sys.exc_info())
-        # can't continue even if onerror hook returns
-        return
-    names = []
-    try:
-        names = os.listdir(path)
-    except os.error as err:
-        onerror(os.listdir, path, sys.exc_info())
-    for name in names:
-        fullname = os.path.join(path, name)
-        try:
-            mode = os.lstat(fullname).st_mode
-        except os.error:
-            mode = 0
-        if stat.S_ISDIR(mode):
-            rmtree(fullname, ignore_errors, onerror)
-        else:
-            try:
-                os.remove(fullname)
-            except os.error as err:
-                onerror(os.remove, fullname, sys.exc_info())
-    try:
-        os.rmdir(path)
-    except os.error:
-        onerror(os.rmdir, path, sys.exc_info())
-
-
-def _basename(path):
-    # A basename() variant which first strips the trailing slash, if present.
-    # Thus we always get the last component of the path, even for directories.
-    return os.path.basename(path.rstrip(os.path.sep))
-
-def move(src, dst, overwrite=False):
-    """Recursively move a file or directory to another location. This is
-    similar to the Unix "mv" command.
-
-    If the destination is a directory or a symlink to a directory, the source
-    is moved inside the directory. The destination path must not already
-    exist.
-
-    If the destination already exists but is not a directory, it may be
-    overwritten depending on os.rename() semantics.
-
-    If the destination is on our current filesystem, then rename() is used.
-    Otherwise, src is copied to the destination and then removed.
-    A lot more could be done here...  A look at a mv.c shows a lot of
-    the issues this implementation glosses over.
-
-    """
-    real_dst = os.path.join(dst, _basename(src))
-    if not overwrite:
-        real_dst = get_safe_path(real_dst)
-    try:
-        os.rename(src, real_dst)
-    except OSError:
-        if os.path.isdir(src):
-            if _destinsrc(src, dst):
-                raise Error("Cannot move a directory '%s' into itself '%s'." % (src, dst))
-            for _ in copytree(src, real_dst, symlinks=True, overwrite=overwrite):
-                yield
-            rmtree(src)
-        else:
-            for _ in copy2(src, real_dst, symlinks=True, overwrite=overwrite):
-                yield
-            os.unlink(src)
-
-def _destinsrc(src, dst):
-    src = abspath(src)
-    dst = abspath(dst)
-    if not src.endswith(os.path.sep):
-        src += os.path.sep
-    if not dst.endswith(os.path.sep):
-        dst += os.path.sep
-    return dst.startswith(src)
-
-# vi: expandtab sts=4 ts=4 sw=4
diff --git a/ranger/fsobject/directory.py b/ranger/fsobject/directory.py
index 9bdb4caa..2ac56120 100644
--- a/ranger/fsobject/directory.py
+++ b/ranger/fsobject/directory.py
@@ -21,6 +21,7 @@ from os.path import join, isdir, basename
 from collections import deque
 from time import time
 
+from ranger.core.loader import Loadable
 from ranger.ext.mount_path import mount_path
 from ranger.fsobject import BAD_INFO, File, FileSystemObject
 from ranger.shared import SettingsAware
@@ -51,7 +52,7 @@ def accept_file(fname, hidden_filter, name_filter):
 		return False
 	return True
 
-class Directory(FileSystemObject, Accumulator, SettingsAware):
+class Directory(FileSystemObject, Accumulator, Loadable, SettingsAware):
 	is_directory = True
 	enterable = False
 	load_generator = None