diff options
Diffstat (limited to 'ranger/config/commands.py')
-rw-r--r-- | ranger/config/commands.py | 1013 |
1 files changed, 1013 insertions, 0 deletions
diff --git a/ranger/config/commands.py b/ranger/config/commands.py new file mode 100644 index 00000000..db411436 --- /dev/null +++ b/ranger/config/commands.py @@ -0,0 +1,1013 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2009, 2010, 2011 Roman Zimbelmann <romanz@lavabit.com> +# This configuration file is licensed under the same terms as ranger. +# =================================================================== +# This file contains ranger's commands. +# It's all in python; lines beginning with # are comments. +# +# Note that additional commands are automatically generated from the methods +# of the class ranger.core.actions.Actions. +# +# You can customize commands in the file ~/.config/ranger/commands.py. +# It has the same syntax as this file. In fact, you can just copy this +# file there with `ranger --copy-config=commands' and make your modifications. +# But make sure you update your configs when you update ranger. +# +# =================================================================== +# Every class defined here which is a subclass of `Command' will be used as a +# command in ranger. Several methods are defined to interface with ranger: +# execute(): called when the command is executed. +# cancel(): called when closing the console. +# tab(): called when <TAB> is pressed. +# quick(): called after each keypress. +# +# The return values for tab() can be either: +# None: There is no tab completion +# A string: Change the console to this string +# A list/tuple/generator: cycle through every item in it +# +# The return value for quick() can be: +# False: Nothing happens +# True: Execute the command afterwards +# +# The return value for execute() and cancel() doesn't matter. +# +# =================================================================== +# Commands have certain attributes and methods that facilitate parsing of +# the arguments: +# +# self.line: The whole line that was written in the console. +# self.args: A list of all (space-separated) arguments to the command. +# self.quantifier: If this command was mapped to the key "X" and +# the user pressed 6X, self.quantifier will be 6. +# self.arg(n): The n-th argument, or an empty string if it doesn't exist. +# self.rest(n): The n-th argument plus everything that followed. For example, +# If the command was "search foo bar a b c", rest(2) will be "bar a b c" +# self.start(n): The n-th argument and anything before it. For example, +# If the command was "search foo bar a b c", rest(2) will be "bar a b c" +# +# =================================================================== +# And this is a little reference for common ranger functions and objects: +# +# self.fm: A reference to the "fm" object which contains most information +# about ranger. +# self.fm.notify(string): Print the given string on the screen. +# self.fm.notify(string, bad=True): Print the given string in RED. +# self.fm.reload_cwd(): Reload the current working directory. +# self.fm.env.cwd: The current working directory. (A File object.) +# self.fm.env.cf: The current file. (A File object too.) +# self.fm.env.cwd.get_selection(): A list of all selected files. +# self.fm.execute_console(string): Execute the string as a ranger command. +# self.fm.open_console(string): Open the console with the given string +# already typed in for you. +# self.fm.move(direction): Moves the cursor in the given direction, which +# can be something like down=3, up=5, right=1, left=1, to=6, ... +# +# File objects (for example self.fm.env.cf) have these useful attributes and +# methods: +# +# cf.path: The path to the file. +# cf.basename: The base name only. +# cf.load_content(): Force a loading of the directories content (which +# obviously works with directories only) +# cf.is_directory: True/False depending on whether it's a directory. +# +# For advanced commands it is unavoidable to dive a bit into the source code +# of ranger. +# =================================================================== + +from ranger.api.commands import * +from ranger.ext.get_executables import get_executables +from ranger.core.runner import ALLOWED_FLAGS + +class alias(Command): + """ + :alias <newcommand> <oldcommand> + + Copies the oldcommand as newcommand. + """ + + context = 'browser' + resolve_macros = False + + def execute(self): + if not self.arg(1) or not self.arg(2): + self.fm.notify('Syntax: alias <newcommand> <oldcommand>', bad=True) + else: + self.fm.commands.alias(self.arg(1), self.rest(2)) + +class cd(Command): + """ + :cd [-r] <dirname> + + The cd command changes the directory. + The command 'cd -' is equivalent to typing ``. + Using the option "-r" will get you to the real path. + """ + + def execute(self): + if self.arg(1) == '-r': + import os.path + self.shift() + destination = os.path.realpath(self.rest(1)) + if os.path.isfile(destination): + destination = os.path.dirname(destination) + else: + destination = self.rest(1) + + if not destination: + destination = '~' + + if destination == '-': + self.fm.enter_bookmark('`') + else: + self.fm.cd(destination) + + def tab(self): + from os.path import dirname, basename, expanduser, join + + cwd = self.fm.env.cwd.path + rel_dest = self.rest(1) + + bookmarks = [v.path for v in self.fm.bookmarks.dct.values() + if rel_dest in v.path ] + + # expand the tilde into the user directory + if rel_dest.startswith('~'): + rel_dest = expanduser(rel_dest) + + # define some shortcuts + abs_dest = join(cwd, rel_dest) + abs_dirname = dirname(abs_dest) + rel_basename = basename(rel_dest) + rel_dirname = dirname(rel_dest) + + try: + # are we at the end of a directory? + if rel_dest.endswith('/') or rel_dest == '': + _, dirnames, _ = next(os.walk(abs_dest)) + + # are we in the middle of the filename? + else: + _, dirnames, _ = next(os.walk(abs_dirname)) + dirnames = [dn for dn in dirnames \ + if dn.startswith(rel_basename)] + except (OSError, StopIteration): + # os.walk found nothing + pass + else: + dirnames.sort() + dirnames = bookmarks + dirnames + + # no results, return None + if len(dirnames) == 0: + return + + # one result. since it must be a directory, append a slash. + if len(dirnames) == 1: + return self.start(1) + join(rel_dirname, dirnames[0]) + '/' + + # more than one result. append no slash, so the user can + # manually type in the slash to advance into that directory + return (self.start(1) + join(rel_dirname, dirname) for dirname in dirnames) + + +class chain(Command): + """ + :chain <command1>; <command2>; ... + Calls multiple commands at once, separated by semicolons. + """ + def execute(self): + for command in self.rest(1).split(";"): + self.fm.execute_console(command) + + +class search(Command): + def execute(self): + self.fm.search_file(self.rest(1), regexp=True) + + +class search_inc(Command): + def quick(self): + self.fm.search_file(self.rest(1), regexp=True, offset=0) + + +class shell(Command): + escape_macros_for_shell = True + + def execute(self): + if self.arg(1) and self.arg(1)[0] == '-': + flags = self.arg(1)[1:] + command = self.rest(2) + else: + flags = '' + command = self.rest(1) + + if not command and 'p' in flags: command = 'cat %f' + if command: + if '%' in command: + command = self.fm.substitute_macros(command) + self.fm.execute_command(command, flags=flags) + + def tab(self): + if self.arg(1) and self.arg(1)[0] == '-': + command = self.rest(2) + else: + command = self.rest(1) + start = self.line[0:len(self.line) - len(command)] + + try: + position_of_last_space = command.rindex(" ") + except ValueError: + return (start + program + ' ' for program \ + in get_executables() if program.startswith(command)) + if position_of_last_space == len(command) - 1: + selection = self.fm.env.get_selection() + if len(selection) == 1: + return self.line + selection[0].shell_escaped_basename + ' ' + else: + return self.line + '%s ' + else: + before_word, start_of_word = self.line.rsplit(' ', 1) + return (before_word + ' ' + file.shell_escaped_basename \ + for file in self.fm.env.cwd.files \ + if file.shell_escaped_basename.startswith(start_of_word)) + +class open_with(Command): + def execute(self): + app, flags, mode = self._get_app_flags_mode(self.rest(1)) + self.fm.execute_file( + files = [f for f in self.fm.env.cwd.get_selection()], + app = app, + flags = flags, + mode = mode) + + def _get_app_flags_mode(self, string): + """ + Extracts the application, flags and mode from a string. + + examples: + "mplayer d 1" => ("mplayer", "d", 1) + "aunpack 4" => ("aunpack", "", 4) + "p" => ("", "p", 0) + "" => None + """ + + app = '' + flags = '' + mode = 0 + split = string.split() + + if len(split) == 0: + pass + + elif len(split) == 1: + part = split[0] + if self._is_app(part): + app = part + elif self._is_flags(part): + flags = part + elif self._is_mode(part): + mode = part + + elif len(split) == 2: + part0 = split[0] + part1 = split[1] + + if self._is_app(part0): + app = part0 + if self._is_flags(part1): + flags = part1 + elif self._is_mode(part1): + mode = part1 + elif self._is_flags(part0): + flags = part0 + if self._is_mode(part1): + mode = part1 + elif self._is_mode(part0): + mode = part0 + if self._is_flags(part1): + flags = part1 + + elif len(split) >= 3: + part0 = split[0] + part1 = split[1] + part2 = split[2] + + if self._is_app(part0): + app = part0 + if self._is_flags(part1): + flags = part1 + if self._is_mode(part2): + mode = part2 + elif self._is_mode(part1): + mode = part1 + if self._is_flags(part2): + flags = part2 + elif self._is_flags(part0): + flags = part0 + if self._is_mode(part1): + mode = part1 + elif self._is_mode(part0): + mode = part0 + if self._is_flags(part1): + flags = part1 + + return app, flags, int(mode) + + def _is_app(self, arg): + return not self._is_flags(arg) and not arg.isdigit() + + def _is_flags(self, arg): + return all(x in ALLOWED_FLAGS for x in arg) + + def _is_mode(self, arg): + return all(x in '0123456789' for x in arg) + + +class find(Command): + """ + :find <string> + + The find command will attempt to find a partial, case insensitive + match in the filenames of the current directory and execute the + file automatically. + """ + + count = 0 + tab = Command._tab_directory_content + + def execute(self): + if self.quick(): + self.fm.move(right=1) + self.fm.block_input(0.5) + else: + self.fm.cd(self.rest(1)) + + def quick(self): + self.count = 0 + cwd = self.fm.env.cwd + arg = self.rest(1) + if not arg: + return False + + if arg == '.': + return False + if arg == '..': + return True + + deq = deque(cwd.files) + deq.rotate(-cwd.pointer) + i = 0 + case_insensitive = arg.lower() == arg + for fsobj in deq: + if case_insensitive: + filename = fsobj.basename_lower + else: + filename = fsobj.basename + if arg in filename: + self.count += 1 + if self.count == 1: + cwd.move(to=(cwd.pointer + i) % len(cwd.files)) + self.fm.env.cf = cwd.pointed_obj + if self.count > 1: + return False + i += 1 + + return self.count == 1 + + +class set_(Command): + """ + :set <option name>=<python expression> + + Gives an option a new value. + """ + name = 'set' # don't override the builtin set class + def execute(self): + name = self.arg(1) + name, value, _ = self.parse_setting_line() + if name and value: + from re import compile as regexp + try: + value = eval(value) + except: + pass + self.fm.settings[name] = value + + def tab(self): + name, value, name_done = self.parse_setting_line() + settings = self.fm.settings + if not name: + return (self.firstpart + setting for setting in settings) + if not value and not name_done: + return (self.firstpart + setting for setting in settings \ + if setting.startswith(name)) + if not value: + return self.firstpart + repr(settings[name]) + if bool in settings.types_of(name): + if 'true'.startswith(value.lower()): + return self.firstpart + 'True' + if 'false'.startswith(value.lower()): + return self.firstpart + 'False' + + +class quit(Command): + """ + :quit + + Closes the current tab. If there is only one tab, quit the program. + """ + + def execute(self): + if len(self.fm.tabs) <= 1: + self.fm.exit() + self.fm.tab_close() + + +class quitall(Command): + """ + :quitall + + Quits the program immediately. + """ + + def execute(self): + self.fm.exit() + + +class quit_bang(quitall): + """ + :quit! + + Quits the program immediately. + """ + name = 'quit!' + allow_abbrev = False + + +class terminal(Command): + """ + :terminal + + Spawns an "x-terminal-emulator" starting in the current directory. + """ + def execute(self): + command = os.environ.get('TERMCMD', os.environ.get('TERM')) + if command not in get_executables(): + command = 'x-terminal-emulator' + if command not in get_executables(): + command = 'xterm' + self.fm.run(command, flags='d') + + +class delete(Command): + """ + :delete + + Tries to delete the selection. + + "Selection" is defined as all the "marked files" (by default, you + can mark files with space or v). If there are no marked files, + use the "current file" (where the cursor is) + + When attempting to delete non-empty directories or multiple + marked files, it will require a confirmation: The last word in + the line has to start with a 'y'. This may look like: + :delete yes + :delete seriously? yeah! + """ + + allow_abbrev = False + + def execute(self): + lastword = self.arg(-1) + + if lastword.startswith('y'): + # user confirmed deletion! + return self.fm.delete() + elif self.line.startswith(DELETE_WARNING): + # user did not confirm deletion + return + + cwd = self.fm.env.cwd + cf = self.fm.env.cf + + if cwd.marked_items or (cf.is_directory and not cf.is_link \ + and len(os.listdir(cf.path)) > 0): + # better ask for a confirmation, when attempting to + # delete multiple files or a non-empty directory. + return self.fm.open_console(DELETE_WARNING) + + # no need for a confirmation, just delete + self.fm.delete() + + +class mark(Command): + """ + :mark <regexp> + + Mark all files matching a regular expression. + """ + do_mark = True + + def execute(self): + import re + cwd = self.fm.env.cwd + input = self.rest(1) + searchflags = re.UNICODE + if input.lower() == input: # "smartcase" + searchflags |= re.IGNORECASE + pattern = re.compile(input, searchflags) + for fileobj in cwd.files: + if pattern.search(fileobj.basename): + cwd.mark_item(fileobj, val=self.do_mark) + self.fm.ui.status.need_redraw = True + self.fm.ui.need_redraw = True + + +class console(Command): + """ + :console <command> + + Open the console with the given command. + """ + def execute(self): + position = None + if self.arg(1)[0:2] == '-p': + try: + position = int(self.arg(1)[2:]) + self.shift() + except: + pass + self.fm.open_console(self.rest(1), position=position) + + +class load_copy_buffer(Command): + """ + :load_copy_buffer + + Load the copy buffer from confdir/copy_buffer + """ + copy_buffer_filename = 'copy_buffer' + def execute(self): + from ranger.fsobject import File + from os.path import exists + try: + fname = self.fm.confpath(self.copy_buffer_filename) + f = open(fname, 'r') + except: + return self.fm.notify("Cannot open %s" % \ + (fname or self.copy_buffer_filename), bad=True) + self.fm.env.copy = set(File(g) \ + for g in f.read().split("\n") if exists(g)) + f.close() + self.fm.ui.redraw_main_column() + + +class save_copy_buffer(Command): + """ + :save_copy_buffer + + Save the copy buffer to confdir/copy_buffer + """ + copy_buffer_filename = 'copy_buffer' + def execute(self): + fname = None + try: + fname = self.fm.confpath(self.copy_buffer_filename) + f = open(fname, 'w') + except: + return self.fm.notify("Cannot open %s" % \ + (fname or self.copy_buffer_filename), bad=True) + f.write("\n".join(f.path for f in self.fm.env.copy)) + f.close() + + +class unmark(mark): + """ + :unmark <regexp> + + Unmark all files matching a regular expression. + """ + do_mark = False + + +class mkdir(Command): + """ + :mkdir <dirname> + + Creates a directory with the name <dirname>. + """ + + def execute(self): + from os.path import join, expanduser, lexists + from os import mkdir + + dirname = join(self.fm.env.cwd.path, expanduser(self.rest(1))) + if not lexists(dirname): + mkdir(dirname) + else: + self.fm.notify("file/directory exists!", bad=True) + + +class touch(Command): + """ + :touch <fname> + + Creates a file with the name <fname>. + """ + + def execute(self): + from os.path import join, expanduser, lexists + + fname = join(self.fm.env.cwd.path, expanduser(self.rest(1))) + if not lexists(fname): + open(fname, 'a').close() + else: + self.fm.notify("file/directory exists!", bad=True) + + +class edit(Command): + """ + :edit <filename> + + Opens the specified file in vim + """ + + def execute(self): + if not self.arg(1): + self.fm.edit_file(self.fm.env.cf.path) + else: + self.fm.edit_file(self.rest(1)) + + def tab(self): + return self._tab_directory_content() + + +class eval_(Command): + """ + :eval [-q] <python code> + + Evaluates the python code. + `fm' is a reference to the FM instance. + To display text, use the function `p'. + + Examples: + :eval fm + :eval len(fm.env.directories) + :eval p("Hello World!") + """ + name = 'eval' + resolve_macros = False + + def execute(self): + if self.arg(1) == '-q': + code = self.rest(2) + quiet = True + else: + code = self.rest(1) + quiet = False + import ranger + global cmd, fm, p, quantifier + fm = self.fm + cmd = self.fm.execute_console + p = fm.notify + quantifier = self.quantifier + try: + try: + result = eval(code) + except SyntaxError: + exec(code) + else: + if result and not quiet: + p(result) + except Exception as err: + p(err) + + +class rename(Command): + """ + :rename <newname> + + Changes the name of the currently highlighted file to <newname> + """ + + def execute(self): + from ranger.fsobject import File + from os import access + + new_name = self.rest(1) + + if not new_name: + return self.fm.notify('Syntax: rename <newname>', bad=True) + + if new_name == self.fm.env.cf.basename: + return + + if access(new_name, os.F_OK): + return self.fm.notify("Can't rename: file already exists!", bad=True) + + self.fm.rename(self.fm.env.cf, new_name) + f = File(new_name) + self.fm.env.cwd.pointed_obj = f + self.fm.env.cf = f + + def tab(self): + return self._tab_directory_content() + + +class chmod(Command): + """ + :chmod <octal number> + + Sets the permissions of the selection to the octal number. + + The octal number is between 0 and 777. The digits specify the + permissions for the user, the group and others. + + A 1 permits execution, a 2 permits writing, a 4 permits reading. + Add those numbers to combine them. So a 7 permits everything. + """ + + def execute(self): + mode = self.rest(1) + if not mode: + mode = str(self.quantifier) + + try: + mode = int(mode, 8) + if mode < 0 or mode > 0o777: + raise ValueError + except ValueError: + self.fm.notify("Need an octal number between 0 and 777!", bad=True) + return + + for file in self.fm.env.get_selection(): + try: + os.chmod(file.path, mode) + except Exception as ex: + self.fm.notify(ex) + + try: + # reloading directory. maybe its better to reload the selected + # files only. + self.fm.env.cwd.load_content() + except: + pass + + +class bulkrename(Command): + """ + :bulkrename + + This command opens a list of selected files in an external editor. + After you edit and save the file, it will generate a shell script + which does bulk renaming according to the changes you did in the file. + + This shell script is opened in an editor for you to review. + After you close it, it will be executed. + """ + def execute(self): + import sys + import tempfile + from ranger.fsobject.file import File + from ranger.ext.shell_escape import shell_escape as esc + py3 = sys.version > "3" + + # Create and edit the file list + filenames = [f.basename for f in self.fm.env.get_selection()] + listfile = tempfile.NamedTemporaryFile() + + if py3: + listfile.write("\n".join(filenames).encode("utf-8")) + else: + listfile.write("\n".join(filenames)) + listfile.flush() + self.fm.execute_file([File(listfile.name)], app='editor') + listfile.seek(0) + if py3: + new_filenames = listfile.read().decode("utf-8").split("\n") + else: + new_filenames = listfile.read().split("\n") + listfile.close() + if all(a == b for a, b in zip(filenames, new_filenames)): + self.fm.notify("No renaming to be done!") + return + + # Generate and execute script + cmdfile = tempfile.NamedTemporaryFile() + cmdfile.write(b"# This file will be executed when you close the editor.\n") + cmdfile.write(b"# Please double-check everything, clear the file to abort.\n") + if py3: + cmdfile.write("\n".join("mv -vi -- " + esc(old) + " " + esc(new) \ + for old, new in zip(filenames, new_filenames) \ + if old != new).encode("utf-8")) + else: + cmdfile.write("\n".join("mv -vi -- " + esc(old) + " " + esc(new) \ + for old, new in zip(filenames, new_filenames) if old != new)) + cmdfile.flush() + self.fm.execute_file([File(cmdfile.name)], app='editor') + self.fm.run(['/bin/sh', cmdfile.name], flags='w') + cmdfile.close() + + +class relink(Command): + """ + :relink <newpath> + + Changes the linked path of the currently highlighted symlink to <newpath> + """ + + def execute(self): + from ranger.fsobject import File + + new_path = self.rest(1) + cf = self.fm.env.cf + + if not new_path: + return self.fm.notify('Syntax: relink <newpath>', bad=True) + + if not cf.is_link: + return self.fm.notify('%s is not a symlink!' % cf.basename, bad=True) + + if new_path == os.readlink(cf.path): + return + + try: + os.remove(cf.path) + os.symlink(new_path, cf.path) + except OSError as err: + self.fm.notify(err) + + self.fm.reset() + self.fm.env.cwd.pointed_obj = cf + self.fm.env.cf = cf + + def tab(self): + if not self.rest(1): + return self.line+os.readlink(self.fm.env.cf.path) + else: + return self._tab_directory_content() + + +class help_(Command): + """ + :help + + Display ranger's manual page. + """ + name = 'help' + def execute(self): + if self.quantifier == 1: + self.fm.dump_keybindings() + elif self.quantifier == 2: + self.fm.dump_commands() + elif self.quantifier == 3: + self.fm.dump_settings() + else: + self.fm.display_help() + + +class copymap(Command): + """ + :copymap <keys> <newkeys1> [<newkeys2>...] + Copies a "browser" keybinding from <keys> to <newkeys> + """ + context = 'browser' + + def execute(self): + if not self.arg(1) or not self.arg(2): + return self.notify("Not enough arguments", bad=True) + + for arg in self.args[2:]: + self.fm.env.keymaps.copy(self.context, self.arg(1), arg) + + +class copypmap(copymap): + """ + :copypmap <keys> <newkeys1> [<newkeys2>...] + Copies a "pager" keybinding from <keys> to <newkeys> + """ + context = 'pager' + + +class copycmap(copymap): + """ + :copycmap <keys> <newkeys1> [<newkeys2>...] + Copies a "console" keybinding from <keys> to <newkeys> + """ + context = 'console' + + +class copytmap(copymap): + """ + :copycmap <keys> <newkeys1> [<newkeys2>...] + Copies a "taskview" keybinding from <keys> to <newkeys> + """ + context = 'taskview' + + +class unmap(Command): + """ + :unmap <keys> [<keys2>, ...] + Remove the given "browser" mappings + """ + context = 'browser' + + def execute(self): + for arg in self.args[1:]: + self.fm.env.keymaps.unbind(self.context, arg) + + +class cunmap(unmap): + """ + :cunmap <keys> [<keys2>, ...] + Remove the given "console" mappings + """ + context = 'browser' + + +class punmap(unmap): + """ + :punmap <keys> [<keys2>, ...] + Remove the given "pager" mappings + """ + context = 'pager' + + +class tunmap(unmap): + """ + :tunmap <keys> [<keys2>, ...] + Remove the given "taskview" mappings + """ + context = 'taskview' + + +class map_(Command): + """ + :map <keysequence> <command> + Maps a command to a keysequence in the "browser" context. + + Example: + map j move down + map J move down 10 + """ + name = 'map' + context = 'browser' + resolve_macros = False + + def execute(self): + self.fm.env.keymaps.bind(self.context, self.arg(1), self.rest(2)) + + +class cmap(map_): + """:cmap <keysequence> <command> + Maps a command to a keysequence in the "console" context. + + Example: + cmap <ESC> console_close + cmap <C-x> console_type test + """ + context = 'console' + + +class tmap(map_): + """:tmap <keysequence> <command> + Maps a command to a keysequence in the "taskview" context. + """ + context = 'taskview' + + +class pmap(map_): + """:pmap <keysequence> <command> + Maps a command to a keysequence in the "pager" context. + """ + context = 'pager' + + +class filter(Command): + """ + :filter <string> + + Displays only the files which contain <string> in their basename. + """ + + def execute(self): + self.fm.set_filter(self.rest(1)) + self.fm.reload_cwd() + + +class grep(Command): + """ + :grep <string> + + Looks for a string in all marked files or directories + """ + + def execute(self): + if self.rest(1): + action = ['grep', '--color=always', '--line-number'] + action.extend(['-e', self.rest(1), '-r']) + action.extend(f.path for f in self.fm.env.get_selection()) + self.fm.execute_command(action, flags='p') |