# 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 os import re import shutil import string from os.path import join, isdir from os import 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.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 = '%' idpattern = '\d?[a-z]' 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 hasattr(self.ui, 'notify'): self.ui.notify(text, duration=duration, bad=bad) 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""" if hasattr(self.ui, 'open_console'): self.ui.open_console(string, prompt=prompt, position=position) def execute_console(self, string=''): """Execute a command for the console""" self.open_console(string=string) self.ui.console.line = string self.ui.console.execute() def substitute_macros(self, string): return _MacroTemplate(string).safe_substitute(self._get_macros()) 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 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] return self.run(files=list(files), **kw) # -------------------------- # -- 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(): 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): parent = self.env.at_level(-1) 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(relative) def scroll(self, relative): """Scroll down by lines""" if hasattr(self.ui, 'scroll'): self.ui.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 hint(self, text): self.ui.hint(text) def toggle_boolean_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(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) if hasattr(self.ui, 'redraw_main_column'): self.ui.redraw_main_column() if hasattr(self.ui, 'status'): 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, 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(order='search') def search(self, order=None, 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, forward=forward) elif order in ('size', 'mimetype', 'ctime'): 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) 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, movedown=None): try: toggle = self.tags.toggle except AttributeError: return sel = self.env.get_selection() toggle(*tuple(map(lambda x: x.realpath, sel))) if movedown is None: movedown = len(sel) == 1 if movedown: self.move(down=1) if hasattr(self.ui, 'redraw_main_column'): self.ui.redraw_main_column() def tag_remove(self, movedown=None): try: remove = self.tags.remove except AttributeError: return sel = self.env.get_selection() remove(*tuple(map(lambda x: x.realpath, sel))) if movedown is None: movedown = len(sel) == 1 if movedown: self.move(down=1) if hasattr(self.ui, 'redraw_main_column'): self.ui.redraw_main_column() # -------------------------- # -- 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[key] cwd = self.env.cwd if destination.path != cwd.path: self.bookmarks.enter(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[key] = self.env.cwd def unset_bookmark(self, key): """Delete the bookmark with the name """ self.bookmarks.update_if_outdated() self.bookmarks.delete(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): if not hasattr(self.ui, 'open_pager'): return 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, topic='index', narg=None): if not hasattr(self.ui, 'open_pager'): return from ranger.help import get_help, get_help_by_index scroll_to_line = 0 if narg is not None: chapter, subchapter = int(str(narg)[0]), str(narg)[1:] help_text = get_help_by_index(chapter) lines = help_text.split('\n') if chapter: chapternumber = str(chapter) + '.' + subchapter + '. ' skip_to_content = True for line_number, line in enumerate(lines): if skip_to_content: if line[:10] == '==========': skip_to_content = False else: if line.startswith(chapternumber): scroll_to_line = line_number else: help_text = get_help(topic) lines = help_text.split('\n') pager = self.ui.open_pager() pager.markup = 'help' pager.set_source(lines) pager.move(down=scroll_to_line) def display_log(self): if not hasattr(self.ui, 'open_pager'): return 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 hasattr(self.ui, 'open_embedded_pager'): return pager = self.ui.open_embedded_pager() pager.set_source(self.env.cf.get_preview_source(pager.wid, pager.hei)) # -------------------------- # -- Previews # -------------------------- 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: data[(-1, -1)] = open(path, 'r').read(1024 * 32) else: data[(-1, -1)] = None if self.env.cf.realpath == path: self.ui.browser.need_redraw = True data['loading'] = False 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 open(path, 'r') 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): for i in range(1, 10): if not i in self.tabs: self.tab_open(i) 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(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 = ['--backup=numbered', '-af', '--'] mv_flags = ['--backup=numbered', '-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!", duration=1) 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.rename(src, dest) except OSError as err: self.notify(err)