1993 lines
61 KiB
Python
1993 lines
61 KiB
Python
# -*- coding: utf-8 -*-
|
|
# This file is part of ranger, the console file manager.
|
|
# This configuration file is licensed under the same terms as ranger.
|
|
# ===================================================================
|
|
#
|
|
# NOTE: If you copied this file to /etc/ranger/commands_full.py or
|
|
# ~/.config/ranger/commands_full.py, then it will NOT be loaded by ranger,
|
|
# and only serve as a reference.
|
|
#
|
|
# ===================================================================
|
|
# 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 files /etc/ranger/commands.py (system-wide)
|
|
# and ~/.config/ranger/commands.py (per user).
|
|
# They have the same syntax as this file. In fact, you can just copy this
|
|
# file to ~/.config/ranger/commands_full.py with
|
|
# `ranger --copy-config=commands_full' and make your modifications, don't
|
|
# forget to rename it to commands.py. You can also use
|
|
# `ranger --copy-config=commands' to copy a short sample commands.py that
|
|
# has everything you need to get started.
|
|
# 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(tabnum): called when <TAB> is pressed.
|
|
# quick(): called after each keypress.
|
|
#
|
|
# tab() argument tabnum is 1 for <TAB> and -1 for <S-TAB> by default
|
|
#
|
|
# 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): Anything before the n-th argument. For example, if the
|
|
# command was "search foo bar a b c", start(2) will be "search foo"
|
|
#
|
|
# ===================================================================
|
|
# 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.thisdir: The current working directory. (A File object.)
|
|
# self.fm.thisfile: The current file. (A File object too.)
|
|
# self.fm.thistab.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.thisfile) have these useful attributes and
|
|
# methods:
|
|
#
|
|
# tfile.path: The path to the file.
|
|
# tfile.basename: The base name only.
|
|
# tfile.load_content(): Force a loading of the directories content (which
|
|
# obviously works with directories only)
|
|
# tfile.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 __future__ import (absolute_import, division, print_function)
|
|
|
|
from collections import deque
|
|
import os
|
|
import re
|
|
|
|
from ranger.api.commands import Command
|
|
|
|
|
|
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)
|
|
return
|
|
|
|
self.fm.commands.alias(self.arg(1), self.rest(2))
|
|
|
|
|
|
class echo(Command):
|
|
""":echo <text>
|
|
|
|
Display the text in the statusbar.
|
|
"""
|
|
|
|
def execute(self):
|
|
self.fm.notify(self.rest(1))
|
|
|
|
|
|
class cd(Command):
|
|
""":cd [-r] <path>
|
|
|
|
The cd command changes the directory.
|
|
If the path is a file, selects that file.
|
|
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':
|
|
self.shift()
|
|
destination = os.path.realpath(self.rest(1))
|
|
if os.path.isfile(destination):
|
|
self.fm.select_file(destination)
|
|
return
|
|
else:
|
|
destination = self.rest(1)
|
|
|
|
if not destination:
|
|
destination = '~'
|
|
|
|
if destination == '-':
|
|
self.fm.enter_bookmark('`')
|
|
else:
|
|
self.fm.cd(destination)
|
|
|
|
def _tab_args(self):
|
|
# dest must be rest because path could contain spaces
|
|
if self.arg(1) == '-r':
|
|
start = self.start(2)
|
|
dest = self.rest(2)
|
|
else:
|
|
start = self.start(1)
|
|
dest = self.rest(1)
|
|
|
|
if dest:
|
|
head, tail = os.path.split(os.path.expanduser(dest))
|
|
if head:
|
|
dest_exp = os.path.join(os.path.normpath(head), tail)
|
|
else:
|
|
dest_exp = tail
|
|
else:
|
|
dest_exp = ''
|
|
return (start, dest_exp, os.path.join(self.fm.thisdir.path, dest_exp),
|
|
dest.endswith(os.path.sep))
|
|
|
|
@staticmethod
|
|
def _tab_paths(dest, dest_abs, ends_with_sep):
|
|
if not dest:
|
|
try:
|
|
return next(os.walk(dest_abs))[1], dest_abs
|
|
except (OSError, StopIteration):
|
|
return [], ''
|
|
|
|
if ends_with_sep:
|
|
try:
|
|
return [os.path.join(dest, path) for path in next(os.walk(dest_abs))[1]], ''
|
|
except (OSError, StopIteration):
|
|
return [], ''
|
|
|
|
return None, None
|
|
|
|
def _tab_match(self, path_user, path_file):
|
|
if self.fm.settings.cd_tab_case == 'insensitive':
|
|
path_user = path_user.lower()
|
|
path_file = path_file.lower()
|
|
elif self.fm.settings.cd_tab_case == 'smart' and path_user.islower():
|
|
path_file = path_file.lower()
|
|
return path_file.startswith(path_user)
|
|
|
|
def _tab_normal(self, dest, dest_abs):
|
|
dest_dir = os.path.dirname(dest)
|
|
dest_base = os.path.basename(dest)
|
|
|
|
try:
|
|
dirnames = next(os.walk(os.path.dirname(dest_abs)))[1]
|
|
except (OSError, StopIteration):
|
|
return [], ''
|
|
|
|
return [os.path.join(dest_dir, d) for d in dirnames if self._tab_match(dest_base, d)], ''
|
|
|
|
def _tab_fuzzy_match(self, basepath, tokens):
|
|
""" Find directories matching tokens recursively """
|
|
if not tokens:
|
|
tokens = ['']
|
|
paths = [basepath]
|
|
while True:
|
|
token = tokens.pop()
|
|
matches = []
|
|
for path in paths:
|
|
try:
|
|
directories = next(os.walk(path))[1]
|
|
except (OSError, StopIteration):
|
|
continue
|
|
matches += [os.path.join(path, d) for d in directories
|
|
if self._tab_match(token, d)]
|
|
if not tokens or not matches:
|
|
return matches
|
|
paths = matches
|
|
|
|
return None
|
|
|
|
def _tab_fuzzy(self, dest, dest_abs):
|
|
tokens = []
|
|
basepath = dest_abs
|
|
while True:
|
|
basepath_old = basepath
|
|
basepath, token = os.path.split(basepath)
|
|
if basepath == basepath_old:
|
|
break
|
|
if os.path.isdir(basepath_old) and not token.startswith('.'):
|
|
basepath = basepath_old
|
|
break
|
|
tokens.append(token)
|
|
|
|
paths = self._tab_fuzzy_match(basepath, tokens)
|
|
if not os.path.isabs(dest):
|
|
paths_rel = self.fm.thisdir.path
|
|
paths = [os.path.relpath(os.path.join(basepath, path), paths_rel)
|
|
for path in paths]
|
|
else:
|
|
paths_rel = ''
|
|
return paths, paths_rel
|
|
|
|
def tab(self, tabnum):
|
|
from os.path import sep
|
|
|
|
start, dest, dest_abs, ends_with_sep = self._tab_args()
|
|
|
|
paths, paths_rel = self._tab_paths(dest, dest_abs, ends_with_sep)
|
|
if paths is None:
|
|
if self.fm.settings.cd_tab_fuzzy:
|
|
paths, paths_rel = self._tab_fuzzy(dest, dest_abs)
|
|
else:
|
|
paths, paths_rel = self._tab_normal(dest, dest_abs)
|
|
|
|
paths.sort()
|
|
|
|
if self.fm.settings.cd_bookmarks:
|
|
paths[0:0] = [
|
|
os.path.relpath(v.path, paths_rel) if paths_rel else v.path
|
|
for v in self.fm.bookmarks.dct.values() for path in paths
|
|
if v.path.startswith(os.path.join(paths_rel, path) + sep)
|
|
]
|
|
|
|
if not paths:
|
|
return None
|
|
if len(paths) == 1:
|
|
return start + paths[0] + sep
|
|
return [start + dirname + sep for dirname in paths]
|
|
|
|
|
|
class chain(Command):
|
|
""":chain <command1>; <command2>; ...
|
|
|
|
Calls multiple commands at once, separated by semicolons.
|
|
"""
|
|
resolve_macros = False
|
|
|
|
def execute(self):
|
|
if not self.rest(1).strip():
|
|
self.fm.notify('Syntax: chain <command1>; <command2>; ...', bad=True)
|
|
return
|
|
for command in [s.strip() for s in self.rest(1).split(";")]:
|
|
self.fm.execute_console(command)
|
|
|
|
|
|
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 command:
|
|
self.fm.execute_command(command, flags=flags)
|
|
|
|
def tab(self, tabnum):
|
|
from ranger.ext.get_executables import get_executables
|
|
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.thistab.get_selection()
|
|
if len(selection) == 1:
|
|
return self.line + selection[0].shell_escaped_basename + ' '
|
|
return self.line + '%s '
|
|
|
|
before_word, start_of_word = self.line.rsplit(' ', 1)
|
|
return (before_word + ' ' + file.shell_escaped_basename
|
|
for file in self.fm.thisdir.files or []
|
|
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.thistab.get_selection()],
|
|
app=app,
|
|
flags=flags,
|
|
mode=mode)
|
|
|
|
def tab(self, tabnum):
|
|
return self._tab_through_executables()
|
|
|
|
def _get_app_flags_mode(self, string): # pylint: disable=too-many-branches,too-many-statements
|
|
"""Extracts the application, flags and mode from a string.
|
|
|
|
examples:
|
|
"mplayer f 1" => ("mplayer", "f", 1)
|
|
"atool 4" => ("atool", "", 4)
|
|
"p" => ("", "p", 0)
|
|
"" => None
|
|
"""
|
|
|
|
app = ''
|
|
flags = ''
|
|
mode = 0
|
|
split = string.split()
|
|
|
|
if 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()
|
|
|
|
@staticmethod
|
|
def _is_flags(arg):
|
|
from ranger.core.runner import ALLOWED_FLAGS
|
|
return all(x in ALLOWED_FLAGS for x in arg)
|
|
|
|
@staticmethod
|
|
def _is_mode(arg):
|
|
return all(x in '0123456789' for x in arg)
|
|
|
|
|
|
class set_(Command):
|
|
""":set <option name>=<python expression>
|
|
|
|
Gives an option a new value.
|
|
|
|
Use `:set <option>!` to toggle or cycle it, e.g. `:set flush_input!`
|
|
"""
|
|
name = 'set' # don't override the builtin set class
|
|
|
|
def execute(self):
|
|
name = self.arg(1)
|
|
name, value, _, toggle = self.parse_setting_line_v2()
|
|
if toggle:
|
|
self.fm.toggle_option(name)
|
|
else:
|
|
self.fm.set_option_from_string(name, value)
|
|
|
|
def tab(self, tabnum): # pylint: disable=too-many-return-statements
|
|
from ranger.gui.colorscheme import get_all_colorschemes
|
|
name, value, name_done = self.parse_setting_line()
|
|
settings = self.fm.settings
|
|
if not name:
|
|
return sorted(self.firstpart + setting for setting in settings)
|
|
if not value and not name_done:
|
|
return sorted(self.firstpart + setting for setting in settings
|
|
if setting.startswith(name))
|
|
if not value:
|
|
value_completers = {
|
|
"colorscheme":
|
|
# Cycle through colorschemes when name, but no value is specified
|
|
lambda: sorted(self.firstpart + colorscheme for colorscheme
|
|
in get_all_colorschemes(self.fm)),
|
|
|
|
"column_ratios":
|
|
lambda: self.firstpart + ",".join(map(str, settings[name])),
|
|
}
|
|
|
|
def default_value_completer():
|
|
return self.firstpart + str(settings[name])
|
|
|
|
return value_completers.get(name, default_value_completer)()
|
|
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'
|
|
# Tab complete colorscheme values if incomplete value is present
|
|
if name == "colorscheme":
|
|
return sorted(self.firstpart + colorscheme for colorscheme
|
|
in get_all_colorschemes(self.fm) if colorscheme.startswith(value))
|
|
return None
|
|
|
|
|
|
class setlocal(set_):
|
|
""":setlocal path=<regular expression> <option name>=<python expression>
|
|
|
|
Gives an option a new value.
|
|
"""
|
|
PATH_RE_DQUOTED = re.compile(r'^setlocal\s+path="(.*?)"')
|
|
PATH_RE_SQUOTED = re.compile(r"^setlocal\s+path='(.*?)'")
|
|
PATH_RE_UNQUOTED = re.compile(r'^path=(.*?)$')
|
|
|
|
def _re_shift(self, match):
|
|
if not match:
|
|
return None
|
|
path = os.path.expanduser(match.group(1))
|
|
for _ in range(len(path.split())):
|
|
self.shift()
|
|
return path
|
|
|
|
def execute(self):
|
|
path = self._re_shift(self.PATH_RE_DQUOTED.match(self.line))
|
|
if path is None:
|
|
path = self._re_shift(self.PATH_RE_SQUOTED.match(self.line))
|
|
if path is None:
|
|
path = self._re_shift(self.PATH_RE_UNQUOTED.match(self.arg(1)))
|
|
if path is None and self.fm.thisdir:
|
|
path = self.fm.thisdir.path
|
|
if not path:
|
|
return
|
|
|
|
name, value, _ = self.parse_setting_line()
|
|
self.fm.set_option_from_string(name, value, localpath=path)
|
|
|
|
|
|
class setintag(set_):
|
|
""":setintag <tag or tags> <option name>=<option value>
|
|
|
|
Sets an option for directories that are tagged with a specific tag.
|
|
"""
|
|
|
|
def execute(self):
|
|
tags = self.arg(1)
|
|
self.shift()
|
|
name, value, _ = self.parse_setting_line()
|
|
self.fm.set_option_from_string(name, value, tags=tags)
|
|
|
|
|
|
class default_linemode(Command):
|
|
|
|
def execute(self):
|
|
from ranger.container.fsobject import FileSystemObject
|
|
|
|
if len(self.args) < 2:
|
|
self.fm.notify(
|
|
"Usage: default_linemode [path=<regexp> | tag=<tag(s)>] <linemode>", bad=True)
|
|
|
|
# Extract options like "path=..." or "tag=..." from the command line
|
|
arg1 = self.arg(1)
|
|
method = "always"
|
|
argument = None
|
|
if arg1.startswith("path="):
|
|
method = "path"
|
|
argument = re.compile(arg1[5:])
|
|
self.shift()
|
|
elif arg1.startswith("tag="):
|
|
method = "tag"
|
|
argument = arg1[4:]
|
|
self.shift()
|
|
|
|
# Extract and validate the line mode from the command line
|
|
lmode = self.rest(1)
|
|
if lmode not in FileSystemObject.linemode_dict:
|
|
self.fm.notify(
|
|
"Invalid linemode: %s; should be %s" % (
|
|
lmode, "/".join(FileSystemObject.linemode_dict)),
|
|
bad=True,
|
|
)
|
|
|
|
# Add the prepared entry to the fm.default_linemodes
|
|
entry = [method, argument, lmode]
|
|
self.fm.default_linemodes.appendleft(entry)
|
|
|
|
# Redraw the columns
|
|
if self.fm.ui.browser:
|
|
for col in self.fm.ui.browser.columns:
|
|
col.need_redraw = True
|
|
|
|
def tab(self, tabnum):
|
|
return (self.arg(0) + " " + lmode
|
|
for lmode in self.fm.thisfile.linemode_dict.keys()
|
|
if lmode.startswith(self.arg(1)))
|
|
|
|
|
|
class quit(Command): # pylint: disable=redefined-builtin
|
|
""":quit
|
|
|
|
Closes the current tab, if there's more than one tab.
|
|
Otherwise quits if there are no tasks in progress.
|
|
"""
|
|
def _exit_no_work(self):
|
|
if self.fm.loader.has_work():
|
|
self.fm.notify('Not quitting: Tasks in progress: Use `quit!` to force quit')
|
|
else:
|
|
self.fm.exit()
|
|
|
|
def execute(self):
|
|
if len(self.fm.tabs) >= 2:
|
|
self.fm.tab_close()
|
|
else:
|
|
self._exit_no_work()
|
|
|
|
|
|
class quit_bang(Command):
|
|
""":quit!
|
|
|
|
Closes the current tab, if there's more than one tab.
|
|
Otherwise force quits immediately.
|
|
"""
|
|
name = 'quit!'
|
|
allow_abbrev = False
|
|
|
|
def execute(self):
|
|
if len(self.fm.tabs) >= 2:
|
|
self.fm.tab_close()
|
|
else:
|
|
self.fm.exit()
|
|
|
|
|
|
class quitall(Command):
|
|
""":quitall
|
|
|
|
Quits if there are no tasks in progress.
|
|
"""
|
|
def _exit_no_work(self):
|
|
if self.fm.loader.has_work():
|
|
self.fm.notify('Not quitting: Tasks in progress: Use `quitall!` to force quit')
|
|
else:
|
|
self.fm.exit()
|
|
|
|
def execute(self):
|
|
self._exit_no_work()
|
|
|
|
|
|
class quitall_bang(Command):
|
|
""":quitall!
|
|
|
|
Force quits immediately.
|
|
"""
|
|
name = 'quitall!'
|
|
allow_abbrev = False
|
|
|
|
def execute(self):
|
|
self.fm.exit()
|
|
|
|
|
|
class terminal(Command):
|
|
""":terminal
|
|
|
|
Spawns an "x-terminal-emulator" starting in the current directory.
|
|
"""
|
|
|
|
def execute(self):
|
|
from ranger.ext.get_executables import get_term
|
|
self.fm.run(get_term(), flags='f')
|
|
|
|
|
|
class delete(Command):
|
|
""":delete
|
|
|
|
Tries to delete the selection or the files passed in arguments (if any).
|
|
The arguments use a shell-like escaping.
|
|
|
|
"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.
|
|
"""
|
|
|
|
allow_abbrev = False
|
|
escape_macros_for_shell = True
|
|
|
|
def execute(self):
|
|
import shlex
|
|
from functools import partial
|
|
|
|
def is_directory_with_files(path):
|
|
return os.path.isdir(path) and not os.path.islink(path) and len(os.listdir(path)) > 0
|
|
|
|
if self.rest(1):
|
|
files = shlex.split(self.rest(1))
|
|
many_files = (len(files) > 1 or is_directory_with_files(files[0]))
|
|
else:
|
|
cwd = self.fm.thisdir
|
|
tfile = self.fm.thisfile
|
|
if not cwd or not tfile:
|
|
self.fm.notify("Error: no file selected for deletion!", bad=True)
|
|
return
|
|
|
|
# relative_path used for a user-friendly output in the confirmation.
|
|
files = [f.relative_path for f in self.fm.thistab.get_selection()]
|
|
many_files = (cwd.marked_items or is_directory_with_files(tfile.path))
|
|
|
|
confirm = self.fm.settings.confirm_on_delete
|
|
if confirm != 'never' and (confirm != 'multiple' or many_files):
|
|
self.fm.ui.console.ask(
|
|
"Confirm deletion of: %s (y/N)" % ', '.join(files),
|
|
partial(self._question_callback, files),
|
|
('n', 'N', 'y', 'Y'),
|
|
)
|
|
else:
|
|
# no need for a confirmation, just delete
|
|
self.fm.delete(files)
|
|
|
|
def tab(self, tabnum):
|
|
return self._tab_directory_content()
|
|
|
|
def _question_callback(self, files, answer):
|
|
if answer == 'y' or answer == 'Y':
|
|
self.fm.delete(files)
|
|
|
|
|
|
class trash(Command):
|
|
""":trash
|
|
|
|
Tries to move the selection or the files passed in arguments (if any) to
|
|
the trash, using rifle rules with label "trash".
|
|
The arguments use a shell-like escaping.
|
|
|
|
"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 trash non-empty directories or multiple
|
|
marked files, it will require a confirmation.
|
|
"""
|
|
|
|
allow_abbrev = False
|
|
escape_macros_for_shell = True
|
|
|
|
def execute(self):
|
|
import shlex
|
|
from functools import partial
|
|
|
|
def is_directory_with_files(path):
|
|
return os.path.isdir(path) and not os.path.islink(path) and len(os.listdir(path)) > 0
|
|
|
|
if self.rest(1):
|
|
files = shlex.split(self.rest(1))
|
|
many_files = (len(files) > 1 or is_directory_with_files(files[0]))
|
|
else:
|
|
cwd = self.fm.thisdir
|
|
tfile = self.fm.thisfile
|
|
if not cwd or not tfile:
|
|
self.fm.notify("Error: no file selected for deletion!", bad=True)
|
|
return
|
|
|
|
# relative_path used for a user-friendly output in the confirmation.
|
|
files = [f.relative_path for f in self.fm.thistab.get_selection()]
|
|
many_files = (cwd.marked_items or is_directory_with_files(tfile.path))
|
|
|
|
confirm = self.fm.settings.confirm_on_delete
|
|
if confirm != 'never' and (confirm != 'multiple' or many_files):
|
|
self.fm.ui.console.ask(
|
|
"Confirm deletion of: %s (y/N)" % ', '.join(files),
|
|
partial(self._question_callback, files),
|
|
('n', 'N', 'y', 'Y'),
|
|
)
|
|
else:
|
|
# no need for a confirmation, just delete
|
|
self.fm.execute_file(files, label='trash')
|
|
|
|
def tab(self, tabnum):
|
|
return self._tab_directory_content()
|
|
|
|
def _question_callback(self, files, answer):
|
|
if answer == 'y' or answer == 'Y':
|
|
self.fm.execute_file(files, label='trash')
|
|
|
|
|
|
class jump_non(Command):
|
|
""":jump_non [-FLAGS...]
|
|
|
|
Jumps to first non-directory if highlighted file is a directory and vice versa.
|
|
|
|
Flags:
|
|
-r Jump in reverse order
|
|
-w Wrap around if reaching end of filelist
|
|
"""
|
|
def __init__(self, *args, **kwargs):
|
|
super(jump_non, self).__init__(*args, **kwargs)
|
|
|
|
flags, _ = self.parse_flags()
|
|
self._flag_reverse = 'r' in flags
|
|
self._flag_wrap = 'w' in flags
|
|
|
|
@staticmethod
|
|
def _non(fobj, is_directory):
|
|
return fobj.is_directory if not is_directory else not fobj.is_directory
|
|
|
|
def execute(self):
|
|
tfile = self.fm.thisfile
|
|
passed = False
|
|
found_before = None
|
|
found_after = None
|
|
for fobj in self.fm.thisdir.files[::-1] if self._flag_reverse else self.fm.thisdir.files:
|
|
if fobj.path == tfile.path:
|
|
passed = True
|
|
continue
|
|
|
|
if passed:
|
|
if self._non(fobj, tfile.is_directory):
|
|
found_after = fobj.path
|
|
break
|
|
elif not found_before and self._non(fobj, tfile.is_directory):
|
|
found_before = fobj.path
|
|
|
|
if found_after:
|
|
self.fm.select_file(found_after)
|
|
elif self._flag_wrap and found_before:
|
|
self.fm.select_file(found_before)
|
|
|
|
|
|
class mark_tag(Command):
|
|
""":mark_tag [<tags>]
|
|
|
|
Mark all tags that are tagged with either of the given tags.
|
|
When leaving out the tag argument, all tagged files are marked.
|
|
"""
|
|
do_mark = True
|
|
|
|
def execute(self):
|
|
cwd = self.fm.thisdir
|
|
tags = self.rest(1).replace(" ", "")
|
|
if not self.fm.tags or not cwd.files:
|
|
return
|
|
for fileobj in cwd.files:
|
|
try:
|
|
tag = self.fm.tags.tags[fileobj.realpath]
|
|
except KeyError:
|
|
continue
|
|
if not tags or tag in tags:
|
|
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:])
|
|
except ValueError:
|
|
pass
|
|
else:
|
|
self.shift()
|
|
self.fm.open_console(self.rest(1), position=position)
|
|
|
|
|
|
class load_copy_buffer(Command):
|
|
""":load_copy_buffer
|
|
|
|
Load the copy buffer from datadir/copy_buffer
|
|
"""
|
|
copy_buffer_filename = 'copy_buffer'
|
|
|
|
def execute(self):
|
|
import sys
|
|
from ranger.container.file import File
|
|
from os.path import exists
|
|
fname = self.fm.datapath(self.copy_buffer_filename)
|
|
unreadable = IOError if sys.version_info[0] < 3 else OSError
|
|
try:
|
|
fobj = open(fname, 'r')
|
|
except unreadable:
|
|
return self.fm.notify(
|
|
"Cannot open %s" % (fname or self.copy_buffer_filename), bad=True)
|
|
|
|
self.fm.copy_buffer = set(File(g)
|
|
for g in fobj.read().split("\n") if exists(g))
|
|
fobj.close()
|
|
self.fm.ui.redraw_main_column()
|
|
return None
|
|
|
|
|
|
class save_copy_buffer(Command):
|
|
""":save_copy_buffer
|
|
|
|
Save the copy buffer to datadir/copy_buffer
|
|
"""
|
|
copy_buffer_filename = 'copy_buffer'
|
|
|
|
def execute(self):
|
|
import sys
|
|
fname = None
|
|
fname = self.fm.datapath(self.copy_buffer_filename)
|
|
unwritable = IOError if sys.version_info[0] < 3 else OSError
|
|
try:
|
|
fobj = open(fname, 'w')
|
|
except unwritable:
|
|
return self.fm.notify("Cannot open %s" %
|
|
(fname or self.copy_buffer_filename), bad=True)
|
|
fobj.write("\n".join(fobj.path for fobj in self.fm.copy_buffer))
|
|
fobj.close()
|
|
return None
|
|
|
|
|
|
class unmark_tag(mark_tag):
|
|
""":unmark_tag [<tags>]
|
|
|
|
Unmark all tags that are tagged with either of the given tags.
|
|
When leaving out the tag argument, all tagged files are unmarked.
|
|
"""
|
|
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 makedirs
|
|
|
|
dirname = join(self.fm.thisdir.path, expanduser(self.rest(1)))
|
|
if not lexists(dirname):
|
|
makedirs(dirname)
|
|
else:
|
|
self.fm.notify("file/directory exists!", bad=True)
|
|
|
|
def tab(self, tabnum):
|
|
return self._tab_directory_content()
|
|
|
|
|
|
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.thisdir.path, expanduser(self.rest(1)))
|
|
if not lexists(fname):
|
|
open(fname, 'a').close()
|
|
else:
|
|
self.fm.notify("file/directory exists!", bad=True)
|
|
|
|
def tab(self, tabnum):
|
|
return self._tab_directory_content()
|
|
|
|
|
|
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.thisfile.path)
|
|
else:
|
|
self.fm.edit_file(self.rest(1))
|
|
|
|
def tab(self, tabnum):
|
|
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.directories)
|
|
:eval p("Hello World!")
|
|
"""
|
|
name = 'eval'
|
|
resolve_macros = False
|
|
|
|
def execute(self):
|
|
# The import is needed so eval() can access the ranger module
|
|
import ranger # NOQA pylint: disable=unused-import,unused-variable
|
|
if self.arg(1) == '-q':
|
|
code = self.rest(2)
|
|
quiet = True
|
|
else:
|
|
code = self.rest(1)
|
|
quiet = False
|
|
global cmd, fm, p, quantifier # pylint: disable=invalid-name,global-variable-undefined
|
|
fm = self.fm
|
|
cmd = self.fm.execute_console
|
|
p = fm.notify
|
|
quantifier = self.quantifier
|
|
try:
|
|
try:
|
|
result = eval(code) # pylint: disable=eval-used
|
|
except SyntaxError:
|
|
exec(code) # pylint: disable=exec-used
|
|
else:
|
|
if result and not quiet:
|
|
p(result)
|
|
except Exception as err: # pylint: disable=broad-except
|
|
fm.notify("The error `%s` was caused by evaluating the "
|
|
"following code: `%s`" % (err, code), bad=True)
|
|
|
|
|
|
class rename(Command):
|
|
""":rename <newname>
|
|
|
|
Changes the name of the currently highlighted file to <newname>
|
|
"""
|
|
|
|
def execute(self):
|
|
from ranger.container.file 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.thisfile.relative_path:
|
|
return None
|
|
|
|
if access(new_name, os.F_OK):
|
|
return self.fm.notify("Can't rename: file already exists!", bad=True)
|
|
|
|
if self.fm.rename(self.fm.thisfile, new_name):
|
|
file_new = File(new_name)
|
|
self.fm.bookmarks.update_path(self.fm.thisfile.path, file_new)
|
|
self.fm.tags.update_path(self.fm.thisfile.path, file_new.path)
|
|
self.fm.thisdir.pointed_obj = file_new
|
|
self.fm.thisfile = file_new
|
|
|
|
return None
|
|
|
|
def tab(self, tabnum):
|
|
return self._tab_directory_content()
|
|
|
|
|
|
class rename_append(Command):
|
|
""":rename_append [-FLAGS...]
|
|
|
|
Opens the console with ":rename <current file>" with the cursor positioned
|
|
before the file extension.
|
|
|
|
Flags:
|
|
-a Position before all extensions
|
|
-r Remove everything before extensions
|
|
"""
|
|
def __init__(self, *args, **kwargs):
|
|
super(rename_append, self).__init__(*args, **kwargs)
|
|
|
|
flags, _ = self.parse_flags()
|
|
self._flag_ext_all = 'a' in flags
|
|
self._flag_remove = 'r' in flags
|
|
|
|
def execute(self):
|
|
from ranger import MACRO_DELIMITER, MACRO_DELIMITER_ESC
|
|
|
|
tfile = self.fm.thisfile
|
|
relpath = tfile.relative_path.replace(MACRO_DELIMITER, MACRO_DELIMITER_ESC)
|
|
basename = tfile.basename.replace(MACRO_DELIMITER, MACRO_DELIMITER_ESC)
|
|
|
|
if basename.find('.') <= 0 or os.path.isdir(relpath):
|
|
self.fm.open_console('rename ' + relpath)
|
|
return
|
|
|
|
if self._flag_ext_all:
|
|
pos_ext = re.search(r'[^.]+', basename).end(0)
|
|
else:
|
|
pos_ext = basename.rindex('.')
|
|
pos = len(relpath) - len(basename) + pos_ext
|
|
|
|
if self._flag_remove:
|
|
relpath = relpath[:-len(basename)] + basename[pos_ext:]
|
|
pos -= pos_ext
|
|
|
|
self.fm.open_console('rename ' + relpath, position=(7 + pos))
|
|
|
|
|
|
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_str = self.rest(1)
|
|
if not mode_str:
|
|
if self.quantifier is None:
|
|
self.fm.notify("Syntax: chmod <octal number> "
|
|
"or specify a quantifier", bad=True)
|
|
return
|
|
mode_str = str(self.quantifier)
|
|
|
|
try:
|
|
mode = int(mode_str, 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 fobj in self.fm.thistab.get_selection():
|
|
try:
|
|
os.chmod(fobj.path, mode)
|
|
except OSError as ex:
|
|
self.fm.notify(ex)
|
|
|
|
# reloading directory. maybe its better to reload the selected
|
|
# files only.
|
|
self.fm.thisdir.content_outdated = True
|
|
|
|
|
|
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):
|
|
# pylint: disable=too-many-locals,too-many-statements,too-many-branches
|
|
import sys
|
|
import tempfile
|
|
from ranger.container.file import File
|
|
from ranger.ext.shell_escape import shell_escape as esc
|
|
py3 = sys.version_info[0] >= 3
|
|
|
|
# Create and edit the file list
|
|
filenames = [f.relative_path for f in self.fm.thistab.get_selection()]
|
|
with tempfile.NamedTemporaryFile(delete=False) as listfile:
|
|
listpath = listfile.name
|
|
if py3:
|
|
listfile.write("\n".join(filenames).encode(
|
|
encoding="utf-8", errors="surrogateescape"))
|
|
else:
|
|
listfile.write("\n".join(filenames))
|
|
self.fm.execute_file([File(listpath)], app='editor')
|
|
with (open(listpath, 'r', encoding="utf-8", errors="surrogateescape") if
|
|
py3 else open(listpath, 'r')) as listfile:
|
|
new_filenames = listfile.read().split("\n")
|
|
os.unlink(listpath)
|
|
if all(a == b for a, b in zip(filenames, new_filenames)):
|
|
self.fm.notify("No renaming to be done!")
|
|
return
|
|
|
|
# Generate script
|
|
with tempfile.NamedTemporaryFile() as cmdfile:
|
|
script_lines = []
|
|
script_lines.append("# This file will be executed when you close"
|
|
" the editor.")
|
|
script_lines.append("# Please double-check everything, clear the"
|
|
" file to abort.")
|
|
new_dirs = []
|
|
for old, new in zip(filenames, new_filenames):
|
|
if old != new:
|
|
basepath, _ = os.path.split(new)
|
|
if (basepath and basepath not in new_dirs
|
|
and not os.path.isdir(basepath)):
|
|
script_lines.append("mkdir -vp -- {dir}".format(
|
|
dir=esc(basepath)))
|
|
new_dirs.append(basepath)
|
|
script_lines.append("mv -vi -- {old} {new}".format(
|
|
old=esc(old), new=esc(new)))
|
|
# Make sure not to forget the ending newline
|
|
script_content = "\n".join(script_lines) + "\n"
|
|
if py3:
|
|
cmdfile.write(script_content.encode(encoding="utf-8",
|
|
errors="surrogateescape"))
|
|
else:
|
|
cmdfile.write(script_content)
|
|
cmdfile.flush()
|
|
|
|
# Open the script and let the user review it, then check if the
|
|
# script was modified by the user
|
|
self.fm.execute_file([File(cmdfile.name)], app='editor')
|
|
cmdfile.seek(0)
|
|
script_was_edited = (script_content != cmdfile.read())
|
|
|
|
# Do the renaming
|
|
self.fm.run(['/bin/sh', cmdfile.name], flags='w')
|
|
|
|
# Retag the files, but only if the script wasn't changed during review,
|
|
# because only then we know which are the source and destination files.
|
|
if not script_was_edited:
|
|
tags_changed = False
|
|
for old, new in zip(filenames, new_filenames):
|
|
if old != new:
|
|
oldpath = self.fm.thisdir.path + '/' + old
|
|
newpath = self.fm.thisdir.path + '/' + new
|
|
if oldpath in self.fm.tags:
|
|
old_tag = self.fm.tags.tags[oldpath]
|
|
self.fm.tags.remove(oldpath)
|
|
self.fm.tags.tags[newpath] = old_tag
|
|
tags_changed = True
|
|
if tags_changed:
|
|
self.fm.tags.dump()
|
|
else:
|
|
fm.notify("files have not been retagged")
|
|
|
|
|
|
class relink(Command):
|
|
""":relink <newpath>
|
|
|
|
Changes the linked path of the currently highlighted symlink to <newpath>
|
|
"""
|
|
|
|
def execute(self):
|
|
new_path = self.rest(1)
|
|
tfile = self.fm.thisfile
|
|
|
|
if not new_path:
|
|
return self.fm.notify('Syntax: relink <newpath>', bad=True)
|
|
|
|
if not tfile.is_link:
|
|
return self.fm.notify('%s is not a symlink!' % tfile.relative_path, bad=True)
|
|
|
|
if new_path == os.readlink(tfile.path):
|
|
return None
|
|
|
|
try:
|
|
os.remove(tfile.path)
|
|
os.symlink(new_path, tfile.path)
|
|
except OSError as err:
|
|
self.fm.notify(err)
|
|
|
|
self.fm.reset()
|
|
self.fm.thisdir.pointed_obj = tfile
|
|
self.fm.thisfile = tfile
|
|
|
|
return None
|
|
|
|
def tab(self, tabnum):
|
|
if not self.rest(1):
|
|
return self.line + os.readlink(self.fm.thisfile.path)
|
|
return self._tab_directory_content()
|
|
|
|
|
|
class help_(Command):
|
|
""":help
|
|
|
|
Display ranger's manual page.
|
|
"""
|
|
name = 'help'
|
|
|
|
def execute(self):
|
|
def callback(answer):
|
|
if answer == "q":
|
|
return
|
|
elif answer == "m":
|
|
self.fm.display_help()
|
|
elif answer == "c":
|
|
self.fm.dump_commands()
|
|
elif answer == "k":
|
|
self.fm.dump_keybindings()
|
|
elif answer == "s":
|
|
self.fm.dump_settings()
|
|
|
|
self.fm.ui.console.ask(
|
|
"View [m]an page, [k]ey bindings, [c]ommands or [s]ettings? (press q to abort)",
|
|
callback,
|
|
list("mqkcs")
|
|
)
|
|
|
|
|
|
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.fm.notify("Not enough arguments", bad=True)
|
|
|
|
for arg in self.args[2:]:
|
|
self.fm.ui.keymaps.copy(self.context, self.arg(1), arg)
|
|
|
|
return None
|
|
|
|
|
|
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):
|
|
""":copytmap <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.ui.keymaps.unbind(self.context, arg)
|
|
|
|
|
|
class uncmap(unmap):
|
|
""":uncmap <keys> [<keys2>, ...]
|
|
|
|
Remove the given "console" mappings
|
|
"""
|
|
context = 'console'
|
|
|
|
|
|
class cunmap(uncmap):
|
|
""":cunmap <keys> [<keys2>, ...]
|
|
|
|
Remove the given "console" mappings
|
|
|
|
DEPRECATED in favor of uncmap.
|
|
"""
|
|
|
|
def execute(self):
|
|
self.fm.notify("cunmap is deprecated in favor of uncmap!")
|
|
super(cunmap, self).execute()
|
|
|
|
|
|
class unpmap(unmap):
|
|
""":unpmap <keys> [<keys2>, ...]
|
|
|
|
Remove the given "pager" mappings
|
|
"""
|
|
context = 'pager'
|
|
|
|
|
|
class punmap(unpmap):
|
|
""":punmap <keys> [<keys2>, ...]
|
|
|
|
Remove the given "pager" mappings
|
|
|
|
DEPRECATED in favor of unpmap.
|
|
"""
|
|
|
|
def execute(self):
|
|
self.fm.notify("punmap is deprecated in favor of unpmap!")
|
|
super(punmap, self).execute()
|
|
|
|
|
|
class untmap(unmap):
|
|
""":untmap <keys> [<keys2>, ...]
|
|
|
|
Remove the given "taskview" mappings
|
|
"""
|
|
context = 'taskview'
|
|
|
|
|
|
class tunmap(untmap):
|
|
""":tunmap <keys> [<keys2>, ...]
|
|
|
|
Remove the given "taskview" mappings
|
|
|
|
DEPRECATED in favor of untmap.
|
|
"""
|
|
|
|
def execute(self):
|
|
self.fm.notify("tunmap is deprecated in favor of untmap!")
|
|
super(tunmap, self).execute()
|
|
|
|
|
|
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):
|
|
if not self.arg(1) or not self.arg(2):
|
|
self.fm.notify("Syntax: {0} <keysequence> <command>".format(self.get_name()), bad=True)
|
|
return
|
|
|
|
self.fm.ui.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 scout(Command):
|
|
""":scout [-FLAGS...] <pattern>
|
|
|
|
Swiss army knife command for searching, traveling and filtering files.
|
|
|
|
Flags:
|
|
-a Automatically open a file on unambiguous match
|
|
-e Open the selected file when pressing enter
|
|
-f Filter files that match the current search pattern
|
|
-g Interpret pattern as a glob pattern
|
|
-i Ignore the letter case of the files
|
|
-k Keep the console open when changing a directory with the command
|
|
-l Letter skipping; e.g. allow "rdme" to match the file "readme"
|
|
-m Mark the matching files after pressing enter
|
|
-M Unmark the matching files after pressing enter
|
|
-p Permanent filter: hide non-matching files after pressing enter
|
|
-r Interpret pattern as a regular expression pattern
|
|
-s Smart case; like -i unless pattern contains upper case letters
|
|
-t Apply filter and search pattern as you type
|
|
-v Inverts the match
|
|
|
|
Multiple flags can be combined. For example, ":scout -gpt" would create
|
|
a :filter-like command using globbing.
|
|
"""
|
|
# pylint: disable=bad-whitespace
|
|
AUTO_OPEN = 'a'
|
|
OPEN_ON_ENTER = 'e'
|
|
FILTER = 'f'
|
|
SM_GLOB = 'g'
|
|
IGNORE_CASE = 'i'
|
|
KEEP_OPEN = 'k'
|
|
SM_LETTERSKIP = 'l'
|
|
MARK = 'm'
|
|
UNMARK = 'M'
|
|
PERM_FILTER = 'p'
|
|
SM_REGEX = 'r'
|
|
SMART_CASE = 's'
|
|
AS_YOU_TYPE = 't'
|
|
INVERT = 'v'
|
|
# pylint: enable=bad-whitespace
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(scout, self).__init__(*args, **kwargs)
|
|
self._regex = None
|
|
self.flags, self.pattern = self.parse_flags()
|
|
|
|
def execute(self): # pylint: disable=too-many-branches
|
|
thisdir = self.fm.thisdir
|
|
flags = self.flags
|
|
pattern = self.pattern
|
|
regex = self._build_regex()
|
|
count = self._count(move=True)
|
|
|
|
self.fm.thistab.last_search = regex
|
|
self.fm.set_search_method(order="search")
|
|
|
|
if (self.MARK in flags or self.UNMARK in flags) and thisdir.files:
|
|
value = flags.find(self.MARK) > flags.find(self.UNMARK)
|
|
if self.FILTER in flags:
|
|
for fobj in thisdir.files:
|
|
thisdir.mark_item(fobj, value)
|
|
else:
|
|
for fobj in thisdir.files:
|
|
if regex.search(fobj.relative_path):
|
|
thisdir.mark_item(fobj, value)
|
|
|
|
if self.PERM_FILTER in flags:
|
|
thisdir.filter = regex if pattern else None
|
|
|
|
# clean up:
|
|
self.cancel()
|
|
|
|
if self.OPEN_ON_ENTER in flags or \
|
|
(self.AUTO_OPEN in flags and count == 1):
|
|
if pattern == '..':
|
|
self.fm.cd(pattern)
|
|
else:
|
|
self.fm.move(right=1)
|
|
if self.quickly_executed:
|
|
self.fm.block_input(0.5)
|
|
|
|
if self.KEEP_OPEN in flags and thisdir != self.fm.thisdir:
|
|
# reopen the console:
|
|
if not pattern:
|
|
self.fm.open_console(self.line)
|
|
else:
|
|
self.fm.open_console(self.line[0:-len(pattern)])
|
|
|
|
if self.quickly_executed and thisdir != self.fm.thisdir and pattern != "..":
|
|
self.fm.block_input(0.5)
|
|
|
|
def cancel(self):
|
|
self.fm.thisdir.temporary_filter = None
|
|
self.fm.thisdir.refilter()
|
|
|
|
def quick(self):
|
|
asyoutype = self.AS_YOU_TYPE in self.flags
|
|
if self.FILTER in self.flags:
|
|
self.fm.thisdir.temporary_filter = self._build_regex()
|
|
if self.PERM_FILTER in self.flags and asyoutype:
|
|
self.fm.thisdir.filter = self._build_regex()
|
|
if self.FILTER in self.flags or self.PERM_FILTER in self.flags:
|
|
self.fm.thisdir.refilter()
|
|
if self._count(move=asyoutype) == 1 and self.AUTO_OPEN in self.flags:
|
|
return True
|
|
return False
|
|
|
|
def tab(self, tabnum):
|
|
self._count(move=True, offset=tabnum)
|
|
|
|
def _build_regex(self):
|
|
if self._regex is not None:
|
|
return self._regex
|
|
|
|
frmat = "%s"
|
|
flags = self.flags
|
|
pattern = self.pattern
|
|
|
|
if pattern == ".":
|
|
return re.compile("")
|
|
|
|
# Handle carets at start and dollar signs at end separately
|
|
if pattern.startswith('^'):
|
|
pattern = pattern[1:]
|
|
frmat = "^" + frmat
|
|
if pattern.endswith('$'):
|
|
pattern = pattern[:-1]
|
|
frmat += "$"
|
|
|
|
# Apply one of the search methods
|
|
if self.SM_REGEX in flags:
|
|
regex = pattern
|
|
elif self.SM_GLOB in flags:
|
|
regex = re.escape(pattern).replace("\\*", ".*").replace("\\?", ".")
|
|
elif self.SM_LETTERSKIP in flags:
|
|
regex = ".*".join(re.escape(c) for c in pattern)
|
|
else:
|
|
regex = re.escape(pattern)
|
|
|
|
regex = frmat % regex
|
|
|
|
# Invert regular expression if necessary
|
|
if self.INVERT in flags:
|
|
regex = "^(?:(?!%s).)*$" % regex
|
|
|
|
# Compile Regular Expression
|
|
# pylint: disable=no-member
|
|
options = re.UNICODE
|
|
if self.IGNORE_CASE in flags or self.SMART_CASE in flags and \
|
|
pattern.islower():
|
|
options |= re.IGNORECASE
|
|
# pylint: enable=no-member
|
|
try:
|
|
self._regex = re.compile(regex, options)
|
|
except re.error:
|
|
self._regex = re.compile("")
|
|
return self._regex
|
|
|
|
def _count(self, move=False, offset=0):
|
|
count = 0
|
|
cwd = self.fm.thisdir
|
|
pattern = self.pattern
|
|
|
|
if not pattern or not cwd.files:
|
|
return 0
|
|
if pattern == '.':
|
|
return 0
|
|
if pattern == '..':
|
|
return 1
|
|
|
|
deq = deque(cwd.files)
|
|
deq.rotate(-cwd.pointer - offset)
|
|
i = offset
|
|
regex = self._build_regex()
|
|
for fsobj in deq:
|
|
if regex.search(fsobj.relative_path):
|
|
count += 1
|
|
if move and count == 1:
|
|
cwd.move(to=(cwd.pointer + i) % len(cwd.files))
|
|
self.fm.thisfile = cwd.pointed_obj
|
|
if count > 1:
|
|
return count
|
|
i += 1
|
|
|
|
return count == 1
|
|
|
|
|
|
class narrow(Command):
|
|
"""
|
|
:narrow
|
|
|
|
Show only the files selected right now. If no files are selected,
|
|
disable narrowing.
|
|
"""
|
|
def execute(self):
|
|
if self.fm.thisdir.marked_items:
|
|
selection = [f.basename for f in self.fm.thistab.get_selection()]
|
|
self.fm.thisdir.narrow_filter = selection
|
|
else:
|
|
self.fm.thisdir.narrow_filter = None
|
|
self.fm.thisdir.refilter()
|
|
|
|
|
|
class filter_inode_type(Command):
|
|
"""
|
|
:filter_inode_type [dfl]
|
|
|
|
Displays only the files of specified inode type. Parameters
|
|
can be combined.
|
|
|
|
d display directories
|
|
f display files
|
|
l display links
|
|
"""
|
|
|
|
def execute(self):
|
|
if not self.arg(1):
|
|
self.fm.thisdir.inode_type_filter = ""
|
|
else:
|
|
self.fm.thisdir.inode_type_filter = self.arg(1)
|
|
self.fm.thisdir.refilter()
|
|
|
|
|
|
class filter_stack(Command):
|
|
"""
|
|
:filter_stack ...
|
|
|
|
Manages the filter stack.
|
|
|
|
filter_stack add FILTER_TYPE ARGS...
|
|
filter_stack pop
|
|
filter_stack decompose
|
|
filter_stack rotate [N=1]
|
|
filter_stack clear
|
|
filter_stack show
|
|
"""
|
|
def execute(self):
|
|
from ranger.core.filter_stack import SIMPLE_FILTERS, FILTER_COMBINATORS
|
|
|
|
subcommand = self.arg(1)
|
|
|
|
if subcommand == "add":
|
|
try:
|
|
self.fm.thisdir.filter_stack.append(
|
|
SIMPLE_FILTERS[self.arg(2)](self.rest(3))
|
|
)
|
|
except KeyError:
|
|
FILTER_COMBINATORS[self.arg(2)](self.fm.thisdir.filter_stack)
|
|
elif subcommand == "pop":
|
|
self.fm.thisdir.filter_stack.pop()
|
|
elif subcommand == "decompose":
|
|
inner_filters = self.fm.thisdir.filter_stack.pop().decompose()
|
|
if inner_filters:
|
|
self.fm.thisdir.filter_stack.extend(inner_filters)
|
|
elif subcommand == "clear":
|
|
self.fm.thisdir.filter_stack = []
|
|
elif subcommand == "rotate":
|
|
rotate_by = int(self.arg(2) or self.quantifier or 1)
|
|
self.fm.thisdir.filter_stack = (
|
|
self.fm.thisdir.filter_stack[-rotate_by:]
|
|
+ self.fm.thisdir.filter_stack[:-rotate_by]
|
|
)
|
|
elif subcommand == "show":
|
|
stack = list(map(str, self.fm.thisdir.filter_stack))
|
|
pager = self.fm.ui.open_pager()
|
|
pager.set_source(["Filter stack: "] + stack)
|
|
pager.move(to=100, percentage=True)
|
|
return
|
|
else:
|
|
self.fm.notify(
|
|
"Unknown subcommand: {}".format(subcommand),
|
|
bad=True
|
|
)
|
|
return
|
|
|
|
self.fm.thisdir.refilter()
|
|
|
|
|
|
class grep(Command):
|
|
""":grep <string>
|
|
|
|
Looks for a string in all marked files or directories
|
|
"""
|
|
|
|
def execute(self):
|
|
if self.rest(1):
|
|
action = ['grep', '--line-number']
|
|
action.extend(['-e', self.rest(1), '-r'])
|
|
action.extend(f.path for f in self.fm.thistab.get_selection())
|
|
self.fm.execute_command(action, flags='p')
|
|
|
|
|
|
class flat(Command):
|
|
"""
|
|
:flat <level>
|
|
|
|
Flattens the directory view up to the specified level.
|
|
|
|
-1 fully flattened
|
|
0 remove flattened view
|
|
"""
|
|
|
|
def execute(self):
|
|
try:
|
|
level_str = self.rest(1)
|
|
level = int(level_str)
|
|
except ValueError:
|
|
level = self.quantifier
|
|
if level is None:
|
|
self.fm.notify("Syntax: flat <level>", bad=True)
|
|
return
|
|
if level < -1:
|
|
self.fm.notify("Need an integer number (-1, 0, 1, ...)", bad=True)
|
|
self.fm.thisdir.unload()
|
|
self.fm.thisdir.flat = level
|
|
self.fm.thisdir.load_content()
|
|
|
|
|
|
class reset_previews(Command):
|
|
""":reset_previews
|
|
|
|
Reset the file previews.
|
|
"""
|
|
def execute(self):
|
|
self.fm.previews = {}
|
|
self.fm.ui.need_redraw = True
|
|
|
|
|
|
# Version control commands
|
|
# --------------------------------
|
|
|
|
|
|
class stage(Command):
|
|
"""
|
|
:stage
|
|
|
|
Stage selected files for the corresponding version control system
|
|
"""
|
|
|
|
def execute(self):
|
|
from ranger.ext.vcs import VcsError
|
|
|
|
if self.fm.thisdir.vcs and self.fm.thisdir.vcs.track:
|
|
filelist = [f.path for f in self.fm.thistab.get_selection()]
|
|
try:
|
|
self.fm.thisdir.vcs.action_add(filelist)
|
|
except VcsError as ex:
|
|
self.fm.notify('Unable to stage files: {0}'.format(ex))
|
|
self.fm.ui.vcsthread.process(self.fm.thisdir)
|
|
else:
|
|
self.fm.notify('Unable to stage files: Not in repository')
|
|
|
|
|
|
class unstage(Command):
|
|
"""
|
|
:unstage
|
|
|
|
Unstage selected files for the corresponding version control system
|
|
"""
|
|
|
|
def execute(self):
|
|
from ranger.ext.vcs import VcsError
|
|
|
|
if self.fm.thisdir.vcs and self.fm.thisdir.vcs.track:
|
|
filelist = [f.path for f in self.fm.thistab.get_selection()]
|
|
try:
|
|
self.fm.thisdir.vcs.action_reset(filelist)
|
|
except VcsError as ex:
|
|
self.fm.notify('Unable to unstage files: {0}'.format(ex))
|
|
self.fm.ui.vcsthread.process(self.fm.thisdir)
|
|
else:
|
|
self.fm.notify('Unable to unstage files: Not in repository')
|
|
|
|
# Metadata commands
|
|
# --------------------------------
|
|
|
|
|
|
class prompt_metadata(Command):
|
|
"""
|
|
:prompt_metadata <key1> [<key2> [<key3> ...]]
|
|
|
|
Prompt the user to input metadata for multiple keys in a row.
|
|
"""
|
|
|
|
_command_name = "meta"
|
|
_console_chain = None
|
|
|
|
def execute(self):
|
|
prompt_metadata._console_chain = self.args[1:]
|
|
self._process_command_stack()
|
|
|
|
def _process_command_stack(self):
|
|
if prompt_metadata._console_chain:
|
|
key = prompt_metadata._console_chain.pop()
|
|
self._fill_console(key)
|
|
else:
|
|
for col in self.fm.ui.browser.columns:
|
|
col.need_redraw = True
|
|
|
|
def _fill_console(self, key):
|
|
metadata = self.fm.metadata.get_metadata(self.fm.thisfile.path)
|
|
if key in metadata and metadata[key]:
|
|
existing_value = metadata[key]
|
|
else:
|
|
existing_value = ""
|
|
text = "%s %s %s" % (self._command_name, key, existing_value)
|
|
self.fm.open_console(text, position=len(text))
|
|
|
|
|
|
class meta(prompt_metadata):
|
|
"""
|
|
:meta <key> [<value>]
|
|
|
|
Change metadata of a file. Deletes the key if value is empty.
|
|
"""
|
|
|
|
def execute(self):
|
|
key = self.arg(1)
|
|
update_dict = dict()
|
|
update_dict[key] = self.rest(2)
|
|
selection = self.fm.thistab.get_selection()
|
|
for fobj in selection:
|
|
self.fm.metadata.set_metadata(fobj.path, update_dict)
|
|
self._process_command_stack()
|
|
|
|
def tab(self, tabnum):
|
|
key = self.arg(1)
|
|
metadata = self.fm.metadata.get_metadata(self.fm.thisfile.path)
|
|
if key in metadata and metadata[key]:
|
|
return [" ".join([self.arg(0), self.arg(1), metadata[key]])]
|
|
return [self.arg(0) + " " + k for k in sorted(metadata)
|
|
if k.startswith(self.arg(1))]
|
|
|
|
|
|
class linemode(default_linemode):
|
|
"""
|
|
:linemode <mode>
|
|
|
|
Change what is displayed as a filename.
|
|
|
|
- "mode" may be any of the defined linemodes (see: ranger.core.linemode).
|
|
"normal" is mapped to "filename".
|
|
"""
|
|
|
|
def execute(self):
|
|
mode = self.arg(1)
|
|
|
|
if mode == "normal":
|
|
from ranger.core.linemode import DEFAULT_LINEMODE
|
|
mode = DEFAULT_LINEMODE
|
|
|
|
if mode not in self.fm.thisfile.linemode_dict:
|
|
self.fm.notify("Unhandled linemode: `%s'" % mode, bad=True)
|
|
return
|
|
|
|
self.fm.thisdir.set_linemode_of_children(mode)
|
|
|
|
# Ask the browsercolumns to redraw
|
|
for col in self.fm.ui.browser.columns:
|
|
col.need_redraw = True
|
|
|
|
|
|
class yank(Command):
|
|
""":yank [name|dir|path]
|
|
|
|
Copies the file's name (default), directory or path into both the primary X
|
|
selection and the clipboard.
|
|
"""
|
|
|
|
modes = {
|
|
'': 'basename',
|
|
'name_without_extension': 'basename_without_extension',
|
|
'name': 'basename',
|
|
'dir': 'dirname',
|
|
'path': 'path',
|
|
}
|
|
|
|
def execute(self):
|
|
import subprocess
|
|
|
|
def clipboards():
|
|
from ranger.ext.get_executables import get_executables
|
|
clipboard_managers = {
|
|
'xclip': [
|
|
['xclip'],
|
|
['xclip', '-selection', 'clipboard'],
|
|
],
|
|
'xsel': [
|
|
['xsel'],
|
|
['xsel', '-b'],
|
|
],
|
|
'wl-copy': [
|
|
['wl-copy'],
|
|
],
|
|
'pbcopy': [
|
|
['pbcopy'],
|
|
],
|
|
}
|
|
ordered_managers = ['pbcopy', 'wl-copy', 'xclip', 'xsel']
|
|
executables = get_executables()
|
|
for manager in ordered_managers:
|
|
if manager in executables:
|
|
return clipboard_managers[manager]
|
|
return []
|
|
|
|
clipboard_commands = clipboards()
|
|
|
|
mode = self.modes[self.arg(1)]
|
|
selection = self.get_selection_attr(mode)
|
|
|
|
new_clipboard_contents = "\n".join(selection)
|
|
for command in clipboard_commands:
|
|
process = subprocess.Popen(command, universal_newlines=True,
|
|
stdin=subprocess.PIPE)
|
|
process.communicate(input=new_clipboard_contents)
|
|
|
|
def get_selection_attr(self, attr):
|
|
return [getattr(item, attr) for item in
|
|
self.fm.thistab.get_selection()]
|
|
|
|
def tab(self, tabnum):
|
|
return (
|
|
self.start(1) + mode for mode
|
|
in sorted(self.modes.keys())
|
|
if mode
|
|
)
|
|
|
|
|
|
class paste_ext(Command):
|
|
"""
|
|
:paste_ext
|
|
|
|
Like paste but tries to rename conflicting files so that the
|
|
file extension stays intact (e.g. file_.ext).
|
|
"""
|
|
|
|
@staticmethod
|
|
def make_safe_path(dst):
|
|
if not os.path.exists(dst):
|
|
return dst
|
|
|
|
dst_name, dst_ext = os.path.splitext(dst)
|
|
|
|
if not dst_name.endswith("_"):
|
|
dst_name += "_"
|
|
if not os.path.exists(dst_name + dst_ext):
|
|
return dst_name + dst_ext
|
|
n = 0
|
|
test_dst = dst_name + str(n)
|
|
while os.path.exists(test_dst + dst_ext):
|
|
n += 1
|
|
test_dst = dst_name + str(n)
|
|
|
|
return test_dst + dst_ext
|
|
|
|
def execute(self):
|
|
return self.fm.paste(make_safe_path=paste_ext.make_safe_path)
|