-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcommands.py
364 lines (309 loc) · 13.4 KB
/
commands.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
# 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)]