# Copyright (C) 2009, 2010 Roman Zimbelmann # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import codecs import os import re import shutil import string from os.path import join, isdir, realpath from os import link, symlink, getcwd from inspect import cleandoc import ranger from ranger.ext.direction import Direction from ranger.ext.relative_symlink import relative_symlink from ranger.ext.keybinding_parser import key_to_string from ranger.ext.shell_escape import shell_quote from ranger import fsobject from ranger.core.shared import FileManagerAware, EnvironmentAware, \ SettingsAware from ranger.fsobject import File from ranger.core.loader import CommandLoader class _MacroTemplate(string.Template): """A template for substituting macros in commands""" delimiter = '%' class Actions(FileManagerAware, EnvironmentAware, SettingsAware): search_method = 'ctime' search_forward = False # -------------------------- # -- Basic Commands # -------------------------- def exit(self): """Exit the program""" raise SystemExit() def reset(self): """Reset the filemanager, clearing the directory buffer""" old_path = self.env.cwd.path self.previews = {} self.env.garbage_collect(-1) self.enter_dir(old_path) def reload_cwd(self): try: cwd = self.env.cwd except: pass cwd.unload() cwd.load_content() def notify(self, text, duration=4, bad=False): if isinstance(text, Exception): if ranger.arg.debug: raise bad = True text = str(text) self.log.appendleft(text) if self.ui and self.ui.is_on: self.ui.status.notify(text, duration=duration, bad=bad) else: print(text) def abort(self): try: item = self.loader.queue[0] except: self.notify("Type Q or :quit to exit Ranger") else: self.notify("Aborting: " + item.get_description()) self.loader.remove(index=0) def redraw_window(self): """Redraw the window""" self.ui.redraw_window() def open_console(self, string='', prompt=None, position=None): """Open the console if the current UI supports that""" self.ui.open_console(string, prompt=prompt, position=position) def execute_console(self, string='', wildcards=[], quantifier=None): """Execute a command for the console""" command_name = string.split()[0] try: cmd_class = self.commands.get_command(command_name) except: self.notify("Command not found: `%s'" % command_name, bad=True) else: cmd = cmd_class(string) if cmd.resolve_macros and _MacroTemplate.delimiter in string: macros = dict(('any%d'%i, key_to_string(char)) \ for i, char in enumerate(wildcards)) if 'any0' in macros: macros['any'] = macros['any0'] string = self.substitute_macros(string, additional=macros) try: cmd_class(string, quantifier=quantifier).execute() except Exception as error: self.notify(error) def substitute_macros(self, string, additional=dict()): return _MacroTemplate(string).safe_substitute(self._get_macros(), **additional) def _get_macros(self): macros = {} if self.fm.env.cf: macros['f'] = shell_quote(self.fm.env.cf.basename) else: macros['f'] = '' macros['s'] = ' '.join(shell_quote(fl.basename) \ for fl in self.fm.env.get_selection()) macros['c'] = ' '.join(shell_quote(fl.path) for fl in self.fm.env.copy) macros['t'] = ' '.join(shell_quote(fl.basename) for fl in self.fm.env.cwd.files if fl.realpath in self.fm.tags) if self.fm.env.cwd: macros['d'] = shell_quote(self.fm.env.cwd.path) else: macros['d'] = '.' # define d/f/s macros for each tab for i in range(1,10): try: tab_dir_path = self.fm.tabs[i] except: continue tab_dir = self.fm.env.get_directory(tab_dir_path) i = str(i) macros[i + 'd'] = shell_quote(tab_dir_path) macros[i + 'f'] = shell_quote(tab_dir.pointed_obj.path) macros[i + 's'] = ' '.join(shell_quote(fl.path) for fl in tab_dir.get_selection()) # define D/F/S for the next tab found_current_tab = False next_tab_path = None first_tab = None for tab in self.fm.tabs: if not first_tab: first_tab = tab if found_current_tab: next_tab_path = self.fm.tabs[tab] break if self.fm.current_tab == tab: found_current_tab = True if found_current_tab and not next_tab_path: next_tab_path = self.fm.tabs[first_tab] next_tab = self.fm.env.get_directory(next_tab_path) macros['D'] = shell_quote(next_tab) macros['F'] = shell_quote(next_tab.pointed_obj.path) macros['S'] = ' '.join(shell_quote(fl.path) for fl in next_tab.get_selection()) return macros def source_cmdlist(self, filename, narg=None): for line in open(filename, 'r'): line = line.rstrip("\r\n") if line.startswith("#") or not line.strip(): continue try: self.execute_console(line) except Exception as e: if ranger.arg.debug: raise else: self.notify('Error in line `%s\':\n %s' % (line, str(e)), bad=True) def execute_file(self, files, **kw): """Execute a file. app is the name of a method in Applications, without the "app_" flags is a string consisting of runner.ALLOWED_FLAGS mode is a positive integer. Both flags and mode specify how the program is run.""" if isinstance(files, set): files = list(files) elif type(files) not in (list, tuple): files = [files] if 'flags' in kw: from ranger.core.runner import Context context = Context(files=list(files), flags=kw['flags']) context.squash_flags() if 'c' in context.flags: files = [self.fm.env.cf] self.signal_emit('execute.before', keywords=kw) try: return self.run(files=list(files), **kw) finally: self.signal_emit('execute.after') # -------------------------- # -- Moving Around # -------------------------- def move(self, narg=None, **kw): """ A universal movement method. Accepts these parameters: (int) down, (int) up, (int) left, (int) right, (int) to, (bool) absolute, (bool) relative, (bool) pages, (bool) percentage to=X is translated to down=X, absolute=True Example: self.move(down=4, pages=True) # moves down by 4 pages. self.move(to=2, pages=True) # moves to page 2. self.move(to=1, percentage=True) # moves to 80% """ cwd = self.env.cwd direction = Direction(kw) if 'left' in direction or direction.left() > 0: steps = direction.left() if narg is not None: steps *= narg try: directory = os.path.join(*(['..'] * steps)) except: return self.env.enter_dir(directory) if cwd and cwd.accessible and cwd.content_loaded: if 'right' in direction: mode = 0 if narg is not None: mode = narg cf = self.env.cf selection = self.env.get_selection() if not self.env.enter_dir(cf) and selection: if self.execute_file(selection, mode=mode) is False: self.open_console('open_with ') elif direction.vertical() and cwd.files: newpos = direction.move( direction=direction.down(), override=narg, maximum=len(cwd), current=cwd.pointer, pagesize=self.ui.browser.hei) cwd.move(to=newpos) def move_parent(self, n, narg=None): if narg is not None: n *= narg parent = self.env.at_level(-1) if parent is not None: if parent.pointer + n < 0: n = 0 - parent.pointer try: self.env.enter_dir(parent.files[parent.pointer+n]) except IndexError: pass def history_go(self, relative): """Move back and forth in the history""" self.env.history_go(int(relative)) def scroll(self, relative): """Scroll down by lines""" if self.ui.browser and self.ui.browser.main_column: self.ui.browser.main_column.scroll(relative) self.env.cf = self.env.cwd.pointed_obj def enter_dir(self, path, remember=False, history=True): """Enter the directory at the given path""" if remember: cwd = self.env.cwd result = self.env.enter_dir(path, history=history) self.bookmarks.remember(cwd) return result return self.env.enter_dir(path, history=history) def cd(self, path, remember=True): """enter the directory at the given path, remember=True""" self.enter_dir(path, remember=remember) def traverse(self): cf = self.env.cf cwd = self.env.cwd if cf is not None and cf.is_directory: self.enter_dir(cf.path) elif cwd.pointer >= len(cwd) - 1: while True: self.move(left=1) cwd = self.env.cwd if cwd.pointer < len(cwd) - 1: break if cwd.path == '/': break self.move(down=1) self.traverse() else: self.move(down=1) self.traverse() # -------------------------- # -- Shortcuts / Wrappers # -------------------------- def execute_command(self, cmd, **kw): return self.run(cmd, **kw) def edit_file(self, file=None): """Calls execute_file with the current file and app='editor'""" if file is None: file = self.env.cf elif isinstance(file, str): file = File(os.path.expanduser(file)) if file is None: return self.execute_file(file, app = 'editor') def toggle_option(self, string): """Toggle a boolean option named """ if isinstance(self.env.settings[string], bool): self.env.settings[string] ^= True def set_option(self, optname, value): """Set the value of an option named """ self.env.settings[optname] = value def sort(self, func=None, reverse=None): if reverse is not None: self.env.settings['sort_reverse'] = bool(reverse) if func is not None: self.env.settings['sort'] = str(func) def set_filter(self, fltr): try: self.env.cwd.filter = fltr except: pass def mark_files(self, all=False, toggle=False, val=None, movedown=None, narg=1): """ A wrapper for the directory.mark_xyz functions. Arguments: all - change all files of the current directory at once? toggle - toggle the marked-status? val - mark or unmark? """ if self.env.cwd is None: return cwd = self.env.cwd if not cwd.accessible: return if movedown is None: movedown = not all if val is None and toggle is False: return if all: if toggle: cwd.toggle_all_marks() else: cwd.mark_all(val) else: for i in range(cwd.pointer, min(cwd.pointer + narg, len(cwd))): item = cwd.files[i] if item is not None: if toggle: cwd.toggle_mark(item) else: cwd.mark_item(item, val) if movedown: self.move(down=narg) self.ui.redraw_main_column() self.ui.status.need_redraw = True def mark_in_direction(self, val=True, dirarg=None): cwd = self.env.cwd direction = Direction(dirarg) pos, selected = direction.select(lst=cwd.files, current=cwd.pointer, pagesize=self.env.termsize[0]) cwd.pointer = pos cwd.correct_pointer() for item in selected: cwd.mark_item(item, val) # -------------------------- # -- Searching # -------------------------- def search_file(self, text, offset=1, regexp=True): if isinstance(text, str) and regexp: try: text = re.compile(text, re.L | re.U | re.I) except: return False self.env.last_search = text self.search_next(order='search', offset=offset) def search_next(self, order=None, offset=1, forward=True): original_order = order if self.search_forward: direction = bool(forward) else: direction = not bool(forward) if order is None: order = self.search_method else: self.set_search_method(order=order) if order in ('search', 'tag'): if order == 'search': arg = self.env.last_search if arg is None: return False if hasattr(arg, 'search'): fnc = lambda x: arg.search(x.basename) else: fnc = lambda x: arg in x.basename elif order == 'tag': fnc = lambda x: x.realpath in self.tags return self.env.cwd.search_fnc(fnc=fnc, offset=offset, forward=forward) elif order in ('size', 'mimetype', 'ctime', 'mtime', 'atime'): cwd = self.env.cwd if original_order is not None or not cwd.cycle_list: lst = list(cwd.files) if order == 'size': fnc = lambda item: -item.size elif order == 'mimetype': fnc = lambda item: item.mimetype elif order == 'ctime': fnc = lambda item: -int(item.stat and item.stat.st_ctime) elif order == 'atime': fnc = lambda item: -int(item.stat and item.stat.st_atime) elif order == 'mtime': fnc = lambda item: -int(item.stat and item.stat.st_mtime) lst.sort(key=fnc) cwd.set_cycle_list(lst) return cwd.cycle(forward=None) return cwd.cycle(forward=forward) def set_search_method(self, order, forward=True): if order in ('search', 'tag', 'size', 'mimetype', 'ctime'): self.search_method = order self.search_forward = forward # -------------------------- # -- Tags # -------------------------- # Tags are saved in ~/.config/ranger/tagged and simply mark if a # file is important to you in any context. def tag_toggle(self, paths=None, value=None, movedown=None, tag=None): if not self.tags: return if paths is None: tags = tuple(x.realpath for x in self.env.get_selection()) else: tags = [realpath(path) for path in paths] if value is True: self.tags.add(*tags, tag=tag or self.tags.default_tag) elif value is False: self.tags.remove(*tags) else: self.tags.toggle(*tags, tag=tag or self.tags.default_tag) if movedown is None: movedown = len(tags) == 1 and paths is None if movedown: self.move(down=1) self.ui.redraw_main_column() def tag_remove(self, paths=None, movedown=None): self.tag_toggle(paths=paths, value=False, movedown=movedown) def tag_add(self, paths=None, movedown=None): self.tag_toggle(paths=paths, value=True, movedown=movedown) # -------------------------- # -- Bookmarks # -------------------------- # Using ranger.container.bookmarks. def enter_bookmark(self, key): """Enter the bookmark with the name """ try: self.bookmarks.update_if_outdated() destination = self.bookmarks[str(key)] cwd = self.env.cwd if destination.path != cwd.path: self.bookmarks.enter(str(key)) self.bookmarks.remember(cwd) except KeyError: pass def set_bookmark(self, key): """Set the bookmark with the name to the current directory""" self.bookmarks.update_if_outdated() self.bookmarks[str(key)] = self.env.cwd def unset_bookmark(self, key): """Delete the bookmark with the name """ self.bookmarks.update_if_outdated() self.bookmarks.delete(str(key)) def draw_bookmarks(self): self.ui.browser.draw_bookmarks = True def hide_bookmarks(self): self.ui.browser.draw_bookmarks = False # -------------------------- # -- Pager # -------------------------- # These commands open the built-in pager and set specific sources. def display_command_help(self, console_widget): try: command = console_widget._get_cmd_class() except: self.notify("Feature not available!", bad=True) return if not command: self.notify("Command not found!", bad=True) return if not command.__doc__: self.notify("Command has no docstring. Try using python without -OO", bad=True) return pager = self.ui.open_pager() lines = cleandoc(command.__doc__).split('\n') pager.set_source(lines) def display_help(self, narg=None): manualpath = self.relpath('../doc/ranger.1') if os.path.exists(manualpath): process = self.run(['man', manualpath]) if process.poll() != 16: return process = self.run(['man', 'ranger']) if process.poll() == 16: self.notify("Could not find manpage.", bad=True) def display_log(self): pager = self.ui.open_pager() if self.log: pager.set_source(["Message Log:"] + list(self.log)) else: pager.set_source(["Message Log:", "No messages!"]) def display_file(self): if not self.env.cf or not self.env.cf.is_file: return pager = self.ui.open_embedded_pager() pager.set_source(self.env.cf.get_preview_source(pager.wid, pager.hei)) # -------------------------- # -- Previews # -------------------------- def update_preview(self, path): try: del self.previews[path] self.ui.need_redraw = True except: return False def get_preview(self, path, width, height): if self.settings.preview_script and self.settings.use_preview_script: # self.previews is a 2 dimensional dict: # self.previews['/tmp/foo.jpg'][(80, 24)] = "the content..." # self.previews['/tmp/foo.jpg']['loading'] = False # A -1 in tuples means "any"; (80, -1) = wid. of 80 and any hei. # The key 'foundpreview' is added later. Values in (True, False) try: data = self.previews[path] except: data = self.previews[path] = {'loading': False} else: if data['loading']: return None found = data.get((-1, -1), data.get((width, -1), data.get((-1, height), data.get((width, height), False)))) if found == False: data['loading'] = True loadable = CommandLoader(args=[self.settings.preview_script, path, str(width), str(height)], read=True, silent=True, descr="Getting preview of %s" % path) def on_after(signal): exit = signal.process.poll() content = signal.loader.stdout_buffer data['foundpreview'] = True if exit == 0: data[(width, height)] = content elif exit == 3: data[(-1, height)] = content elif exit == 4: data[(width, -1)] = content elif exit == 5: data[(-1, -1)] = content elif exit == 1: data[(-1, -1)] = None data['foundpreview'] = False elif exit == 2: f = codecs.open(path, 'r', errors='ignore') data[(-1, -1)] = f.read(1024 * 32) else: data[(-1, -1)] = None if self.env.cf.realpath == path: self.ui.browser.need_redraw = True data['loading'] = False pager = self.ui.browser.pager if self.env.cf and self.env.cf.is_file: pager.set_source(self.env.cf.get_preview_source( pager.wid, pager.hei)) def on_destroy(signal): try: del self.previews[path] except: pass loadable.signal_bind('after', on_after) loadable.signal_bind('destroy', on_destroy) self.loader.add(loadable) return None else: return found else: try: return codecs.open(path, 'r', errors='ignore') except: return None # -------------------------- # -- Tabs # -------------------------- # This implementation of tabs is very simple and keeps track of # directory paths only. def tab_open(self, name, path=None): do_emit_signal = name != self.current_tab self.current_tab = name if path or (name in self.tabs): self.enter_dir(path or self.tabs[name]) else: self._update_current_tab() if do_emit_signal: self.signal_emit('tab.change') def tab_close(self, name=None): if name is None: name = self.current_tab if name == self.current_tab: direction = -1 if name == self._get_tab_list()[-1] else 1 previous = self.current_tab self.tab_move(direction) if previous == self.current_tab: return # can't close last tab if name in self.tabs: del self.tabs[name] def tab_move(self, offset): assert isinstance(offset, int) tablist = self._get_tab_list() current_index = tablist.index(self.current_tab) newtab = tablist[(current_index + offset) % len(tablist)] if newtab != self.current_tab: self.tab_open(newtab) def tab_new(self, path=None): for i in range(1, 10): if not i in self.tabs: self.tab_open(i, path) break def _get_tab_list(self): assert len(self.tabs) > 0, "There must be >=1 tabs at all times" return sorted(self.tabs) def _update_current_tab(self): self.tabs[self.current_tab] = self.env.cwd.path # -------------------------- # -- File System Operations # -------------------------- def uncut(self): self.env.copy = set() self.env.cut = False self.ui.browser.main_column.request_redraw() def copy(self, mode='set', narg=None, dirarg=None): """Copy the selected items. Modes are: 'set', 'add', 'remove'.""" assert mode in ('set', 'add', 'remove') cwd = self.env.cwd if not narg and not dirarg: selected = (f for f in self.env.get_selection() if f in cwd.files) else: if not dirarg and narg: direction = Direction(down=1) offset = 0 else: direction = Direction(dirarg) offset = 1 pos, selected = direction.select( override=narg, lst=cwd.files, current=cwd.pointer, pagesize=self.env.termsize[0], offset=offset) cwd.pointer = pos cwd.correct_pointer() if mode == 'set': self.env.copy = set(selected) elif mode == 'add': self.env.copy.update(set(selected)) elif mode == 'remove': self.env.copy.difference_update(set(selected)) self.env.cut = False self.ui.browser.main_column.request_redraw() def cut(self, mode='set', narg=None, dirarg=None): self.copy(mode=mode, narg=narg, dirarg=dirarg) self.env.cut = True self.ui.browser.main_column.request_redraw() def paste_symlink(self, relative=False): copied_files = self.env.copy for f in copied_files: try: if relative: relative_symlink(f.path, join(getcwd(), f.basename)) else: symlink(f.path, join(getcwd(), f.basename)) except Exception as x: self.notify(x) def paste_hardlink(self): for f in self.env.copy: try: link(f.path, join(getcwd(), f.basename)) except Exception as x: self.notify(x) def paste(self, overwrite=False): """Paste the selected items into the current directory""" copied_files = tuple(self.env.copy) if not copied_files: return def refresh(_): cwd = self.env.get_directory(original_path) cwd.load_content() cwd = self.env.cwd original_path = cwd.path one_file = copied_files[0] if overwrite: cp_flags = ['-af', '--'] mv_flags = ['-f', '--'] else: cp_flags = ['--backup=numbered', '-a', '--'] mv_flags = ['--backup=numbered', '--'] if self.env.cut: self.env.copy.clear() self.env.cut = False if len(copied_files) == 1: descr = "moving: " + one_file.path else: descr = "moving files from: " + one_file.dirname obj = CommandLoader(args=['mv'] + mv_flags \ + [f.path for f in copied_files] \ + [cwd.path], descr=descr) else: if len(copied_files) == 1: descr = "copying: " + one_file.path else: descr = "copying files from: " + one_file.dirname if not overwrite and len(copied_files) == 1 \ and one_file.dirname == cwd.path: # Special case: yypp # copying a file onto itself -> create a backup obj = CommandLoader(args=['cp', '-f'] + cp_flags \ + [one_file.path, one_file.path], descr=descr) else: obj = CommandLoader(args=['cp'] + cp_flags \ + [f.path for f in copied_files] \ + [cwd.path], descr=descr) obj.signal_bind('after', refresh) self.loader.add(obj) def delete(self): self.notify("Deleting!") selected = self.env.get_selection() self.env.copy -= set(selected) if selected: for f in selected: if isdir(f.path) and not os.path.islink(f.path): try: shutil.rmtree(f.path) except OSError as err: self.notify(err) else: try: os.remove(f.path) except OSError as err: self.notify(err) self.env.ensure_correct_pointer() def mkdir(self, name): try: os.mkdir(os.path.join(self.env.cwd.path, name)) except OSError as err: self.notify(err) def rename(self, src, dest): if hasattr(src, 'path'): src = src.path try: os.renames(src, dest) except OSError as err: self.notify(err)