-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
3,762 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,364 @@ | ||
# This is a sample commands.py. You can add your own commands here. | ||
# | ||
# Please refer to commands_full.py for all the default commands and a complete | ||
# documentation. Do NOT add them all here, or you may end up with defunct | ||
# commands when upgrading ranger. | ||
|
||
# A simple command for demonstration purposes follows. | ||
# ----------------------------------------------------------------------------- | ||
|
||
from __future__ import (absolute_import, division, print_function) | ||
|
||
# You can import any python module as needed. | ||
import os | ||
|
||
# You always need to import ranger.api.commands here to get the Command class: | ||
from ranger.api.commands import Command | ||
|
||
|
||
# Any class that is a subclass of "Command" will be integrated into ranger as a | ||
# command. Try typing ":my_edit<ENTER>" in ranger! | ||
class my_edit(Command): | ||
# The so-called doc-string of the class will be visible in the built-in | ||
# help that is accessible by typing "?c" inside ranger. | ||
""":my_edit <filename> | ||
A sample command for demonstration purposes that opens a file in an editor. | ||
""" | ||
|
||
# The execute method is called when you run this command in ranger. | ||
def execute(self): | ||
# self.arg(1) is the first (space-separated) argument to the function. | ||
# This way you can write ":my_edit somefilename<ENTER>". | ||
if self.arg(1): | ||
# self.rest(1) contains self.arg(1) and everything that follows | ||
target_filename = self.rest(1) | ||
else: | ||
# self.fm is a ranger.core.filemanager.FileManager object and gives | ||
# you access to internals of ranger. | ||
# self.fm.thisfile is a ranger.container.file.File object and is a | ||
# reference to the currently selected file. | ||
target_filename = self.fm.thisfile.path | ||
|
||
# This is a generic function to print text in ranger. | ||
self.fm.notify("Let's edit the file " + target_filename + "!") | ||
|
||
# Using bad=True in fm.notify allows you to print error messages: | ||
if not os.path.exists(target_filename): | ||
self.fm.notify("The given file does not exist!", bad=True) | ||
return | ||
|
||
# This executes a function from ranger.core.acitons, a module with a | ||
# variety of subroutines that can help you construct commands. | ||
# Check out the source, or run "pydoc ranger.core.actions" for a list. | ||
self.fm.edit_file(target_filename) | ||
|
||
# The tab method is called when you press tab, and should return a list of | ||
# suggestions that the user will tab through. | ||
# tabnum is 1 for <TAB> and -1 for <S-TAB> by default | ||
def tab(self, tabnum): | ||
# This is a generic tab-completion function that iterates through the | ||
# content of the current directory. | ||
return self._tab_directory_content() | ||
|
||
class mkcd(Command): | ||
""" | ||
:mkcd <dirname> | ||
Creates a directory with the name <dirname> and enters it. | ||
""" | ||
|
||
def execute(self): | ||
from os.path import join, expanduser, lexists | ||
from os import makedirs | ||
import re | ||
|
||
dirname = join(self.fm.thisdir.path, expanduser(self.rest(1))) | ||
if not lexists(dirname): | ||
makedirs(dirname) | ||
|
||
match = re.search('^/|^~[^/]*/', dirname) | ||
if match: | ||
self.fm.cd(match.group(0)) | ||
dirname = dirname[match.end(0):] | ||
|
||
for m in re.finditer('[^/]+', dirname): | ||
s = m.group(0) | ||
if s == '..' or (s.startswith('.') and not self.fm.settings['show_hidden']): | ||
self.fm.cd(s) | ||
else: | ||
## We force ranger to load content before calling `scout`. | ||
self.fm.thisdir.load_content(schedule=False) | ||
self.fm.execute_console('scout -ae ^{}$'.format(s)) | ||
else: | ||
self.fm.notify("file/directory exists!", bad=True) | ||
|
||
class fzf_select(Command): | ||
""" | ||
:fzf_select | ||
Find a file using fzf. | ||
With a prefix argument to select only directories. | ||
See: https://github.com/junegunn/fzf | ||
""" | ||
|
||
def execute(self): | ||
import subprocess | ||
import os | ||
from ranger.ext.get_executables import get_executables | ||
|
||
if 'fzf' not in get_executables(): | ||
self.fm.notify('Could not find fzf in the PATH.', bad=True) | ||
return | ||
|
||
fd = None | ||
if 'fdfind' in get_executables(): | ||
fd = 'fdfind' | ||
elif 'fd' in get_executables(): | ||
fd = 'fd' | ||
|
||
if fd is not None: | ||
hidden = ('--hidden' if self.fm.settings.show_hidden else '') | ||
exclude = "--no-ignore-vcs --exclude '.git' --exclude '*.py[co]' --exclude '__pycache__'" | ||
only_directories = ('--type directory' if self.quantifier else '') | ||
fzf_default_command = '{} --follow {} {} {} --color=always'.format( | ||
fd, hidden, exclude, only_directories | ||
) | ||
else: | ||
hidden = ('-false' if self.fm.settings.show_hidden else r"-path '*/\.*' -prune") | ||
exclude = r"\( -name '\.git' -o -iname '\.*py[co]' -o -fstype 'dev' -o -fstype 'proc' \) -prune" | ||
only_directories = ('-type d' if self.quantifier else '') | ||
fzf_default_command = 'find -L . -mindepth 1 {} -o {} -o {} -print | cut -b3-'.format( | ||
hidden, exclude, only_directories | ||
) | ||
|
||
env = os.environ.copy() | ||
env['FZF_DEFAULT_COMMAND'] = fzf_default_command | ||
env['FZF_DEFAULT_OPTS'] = '--height=40% --layout=reverse --ansi --preview="{}"'.format(''' | ||
( | ||
batcat --color=always {} || | ||
bat --color=always {} || | ||
cat {} || | ||
tree -ahpCL 3 -I '.git' -I '*.py[co]' -I '__pycache__' {} | ||
) 2>/dev/null | head -n 100 | ||
''') | ||
|
||
fzf = self.fm.execute_command('fzf --no-multi', env=env, | ||
universal_newlines=True, stdout=subprocess.PIPE) | ||
stdout, _ = fzf.communicate() | ||
if fzf.returncode == 0: | ||
selected = os.path.abspath(stdout.strip()) | ||
if os.path.isdir(selected): | ||
self.fm.cd(selected) | ||
else: | ||
self.fm.select_file(selected) | ||
|
||
from collections import deque | ||
|
||
class fd_search(Command): | ||
""" | ||
:fd_search [-d<depth>] <query> | ||
Executes "fd -d<depth> <query>" in the current directory and focuses the | ||
first match. <depth> defaults to 1, i.e. only the contents of the current | ||
directory. | ||
See https://github.com/sharkdp/fd | ||
""" | ||
|
||
SEARCH_RESULTS = deque() | ||
|
||
def execute(self): | ||
import re | ||
import subprocess | ||
from ranger.ext.get_executables import get_executables | ||
|
||
self.SEARCH_RESULTS.clear() | ||
|
||
if 'fdfind' in get_executables(): | ||
fd = 'fdfind' | ||
elif 'fd' in get_executables(): | ||
fd = 'fd' | ||
else: | ||
self.fm.notify("Couldn't find fd in the PATH.", bad=True) | ||
return | ||
|
||
if self.arg(1): | ||
if self.arg(1)[:2] == '-d': | ||
depth = self.arg(1) | ||
target = self.rest(2) | ||
else: | ||
depth = '-d1' | ||
target = self.rest(1) | ||
else: | ||
self.fm.notify(":fd_search needs a query.", bad=True) | ||
return | ||
|
||
hidden = ('--hidden' if self.fm.settings.show_hidden else '') | ||
exclude = "--no-ignore-vcs --exclude '.git' --exclude '*.py[co]' --exclude '__pycache__'" | ||
command = '{} --follow {} {} {} --print0 {}'.format( | ||
fd, depth, hidden, exclude, target | ||
) | ||
fd = self.fm.execute_command(command, universal_newlines=True, stdout=subprocess.PIPE) | ||
stdout, _ = fd.communicate() | ||
|
||
if fd.returncode == 0: | ||
results = filter(None, stdout.split('\0')) | ||
if not self.fm.settings.show_hidden and self.fm.settings.hidden_filter: | ||
hidden_filter = re.compile(self.fm.settings.hidden_filter) | ||
results = filter(lambda res: not hidden_filter.search(os.path.basename(res)), results) | ||
results = map(lambda res: os.path.abspath(os.path.join(self.fm.thisdir.path, res)), results) | ||
self.SEARCH_RESULTS.extend(sorted(results, key=str.lower)) | ||
if len(self.SEARCH_RESULTS) > 0: | ||
self.fm.notify('Found {} result{}.'.format(len(self.SEARCH_RESULTS), | ||
('s' if len(self.SEARCH_RESULTS) > 1 else ''))) | ||
self.fm.select_file(self.SEARCH_RESULTS[0]) | ||
else: | ||
self.fm.notify('No results found.') | ||
|
||
class fd_next(Command): | ||
""" | ||
:fd_next | ||
Selects the next match from the last :fd_search. | ||
""" | ||
|
||
def execute(self): | ||
if len(fd_search.SEARCH_RESULTS) > 1: | ||
fd_search.SEARCH_RESULTS.rotate(-1) # rotate left | ||
self.fm.select_file(fd_search.SEARCH_RESULTS[0]) | ||
elif len(fd_search.SEARCH_RESULTS) == 1: | ||
self.fm.select_file(fd_search.SEARCH_RESULTS[0]) | ||
|
||
class fd_prev(Command): | ||
""" | ||
:fd_prev | ||
Selects the next match from the last :fd_search. | ||
""" | ||
|
||
def execute(self): | ||
if len(fd_search.SEARCH_RESULTS) > 1: | ||
fd_search.SEARCH_RESULTS.rotate(1) # rotate right | ||
self.fm.select_file(fd_search.SEARCH_RESULTS[0]) | ||
elif len(fd_search.SEARCH_RESULTS) == 1: | ||
self.fm.select_file(fd_search.SEARCH_RESULTS[0]) | ||
|
||
import re | ||
|
||
class ag(Command): | ||
""":ag 'regex' | ||
Looks for a string in all marked paths or current dir | ||
""" | ||
editor = os.getenv('EDITOR') or 'vim' | ||
acmd = 'ag --smart-case --group --color --hidden' # --search-zip | ||
qarg = re.compile(r"""^(".*"|'.*')$""") | ||
patterns = [] | ||
# THINK:USE: set_clipboard on each direct ':ag' search? So I could find in vim easily | ||
|
||
def _sel(self): | ||
d = self.fm.thisdir | ||
if d.marked_items: | ||
return [f.relative_path for f in d.marked_items] | ||
# WARN: permanently hidden files like .* are searched anyways | ||
# << BUG: files skipped in .agignore are grep'ed being added on cmdline | ||
if d.temporary_filter and d.files_all and (len(d.files_all) != len(d.files)): | ||
return [f.relative_path for f in d.files] | ||
return [] | ||
|
||
def _arg(self, i=1): | ||
if self.rest(i): | ||
ag.patterns.append(self.rest(i)) | ||
return ag.patterns[-1] if ag.patterns else '' | ||
|
||
def _quot(self, patt): | ||
return patt if ag.qarg.match(patt) else shell_quote(patt) | ||
|
||
def _bare(self, patt): | ||
return patt[1:-1] if ag.qarg.match(patt) else patt | ||
|
||
def _aug_vim(self, iarg, comm='Ag'): | ||
if self.arg(iarg) == '-Q': | ||
self.shift() | ||
comm = 'sil AgSet def.e.literal 1|' + comm | ||
# patt = self._quot(self._arg(iarg)) | ||
patt = self._arg(iarg) # No need to quote in new ag.vim | ||
# FIXME:(add support) 'AgPaths' + self._sel() | ||
cmd = ' '.join([comm, patt]) | ||
cmdl = [ag.editor, '-c', cmd, '-c', 'only'] | ||
return (cmdl, '') | ||
|
||
def _aug_sh(self, iarg, flags=[]): | ||
cmdl = ag.acmd.split() + flags | ||
if iarg == 1: | ||
import shlex | ||
cmdl += shlex.split(self.rest(iarg)) | ||
else: | ||
# NOTE: only allowed switches | ||
opt = self.arg(iarg) | ||
while opt in ['-Q', '-w']: | ||
self.shift() | ||
cmdl.append(opt) | ||
opt = self.arg(iarg) | ||
# TODO: save -Q/-w into ag.patterns =NEED rewrite plugin to join _aug*() | ||
patt = self._bare(self._arg(iarg)) # THINK? use shlex.split() also/instead | ||
cmdl.append(patt) | ||
if '-g' not in flags: | ||
cmdl += self._sel() | ||
return (cmdl, '-p') | ||
|
||
def _choose(self): | ||
if self.arg(1) == '-v': | ||
return self._aug_vim(2, 'Ag') | ||
elif self.arg(1) == '-g': | ||
return self._aug_vim(2, 'sil AgView grp|Ag') | ||
elif self.arg(1) == '-l': | ||
return self._aug_sh(2, ['--files-with-matches', '--count']) | ||
elif self.arg(1) == '-p': # paths | ||
return self._aug_sh(2, ['-g']) | ||
elif self.arg(1) == '-f': | ||
return self._aug_sh(2) | ||
elif self.arg(1) == '-r': | ||
return self._aug_sh(2, ['--files-with-matches']) | ||
else: | ||
return self._aug_sh(1) | ||
|
||
def _catch(self, cmd): | ||
from subprocess import check_output, CalledProcessError | ||
try: | ||
out = check_output(cmd) | ||
except CalledProcessError: | ||
return None | ||
else: | ||
return out[:-1].decode('utf-8').splitlines() | ||
|
||
# DEV | ||
# NOTE: regex becomes very big for big dirs | ||
# BAD: flat ignores 'filter' for nested dirs | ||
def _filter(self, lst, thisdir): | ||
# filter /^rel_dir/ on lst | ||
# get leftmost path elements | ||
# make regex '^' + '|'.join(re.escape(nm)) + '$' | ||
thisdir.temporary_filter = re.compile(file_with_matches) | ||
thisdir.refilter() | ||
|
||
for f in thisdir.files_all: | ||
if f.is_directory: | ||
# DEV: each time filter-out one level of files from lst | ||
self._filter(lst, f) | ||
|
||
def execute(self): | ||
cmd, flags = self._choose() | ||
# self.fm.notify(cmd) | ||
# TODO:ENH: cmd may be [..] -- no need to shell_escape | ||
if self.arg(1) != '-r': | ||
self.fm.execute_command(cmd, flags=flags) | ||
else: | ||
self._filter(self._catch(cmd)) | ||
|
||
def tab(self): | ||
# BAD:(:ag <prev_patt>) when input alias ':agv' and then <Tab> | ||
# <= EXPL: aliases expanded before parsing cmdline | ||
cmd = self.arg(0) | ||
flg = self.arg(1) | ||
if flg[0] == '-' and flg[1] in 'flvgprw': | ||
cmd += ' ' + flg | ||
return ['{} {}'.format(cmd, p) for p in reversed(ag.patterns)] | ||
|
Oops, something went wrong.