Skip to content

Commit

Permalink
Add ranger
Browse files Browse the repository at this point in the history
  • Loading branch information
mlch911 committed Dec 10, 2021
1 parent 47388b6 commit 6b7755a
Show file tree
Hide file tree
Showing 10 changed files with 3,762 additions and 0 deletions.
364 changes: 364 additions & 0 deletions ranger/commands.py
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)]

Loading

0 comments on commit 6b7755a

Please sign in to comment.