summaryrefslogtreecommitdiffstats
path: root/.config/ranger/commands_full.py
diff options
context:
space:
mode:
authorYigit Sever2019-03-17 23:09:49 +0300
committerYigit Sever2019-03-17 23:18:28 +0300
commitea211500227aa58f5e495777743c5d391cbc3110 (patch)
treeafa2b455f9fea40b45dbf8a742c0d0d498024edb /.config/ranger/commands_full.py
downloaddotfiles-ea211500227aa58f5e495777743c5d391cbc3110.tar.gz
dotfiles-ea211500227aa58f5e495777743c5d391cbc3110.tar.bz2
dotfiles-ea211500227aa58f5e495777743c5d391cbc3110.zip
Initial commit
Diffstat (limited to '.config/ranger/commands_full.py')
-rw-r--r--.config/ranger/commands_full.py1836
1 files changed, 1836 insertions, 0 deletions
diff --git a/.config/ranger/commands_full.py b/.config/ranger/commands_full.py
new file mode 100644
index 0000000..d177203
--- /dev/null
+++ b/.config/ranger/commands_full.py
@@ -0,0 +1,1836 @@
1# -*- coding: utf-8 -*-
2# This file is part of ranger, the console file manager.
3# This configuration file is licensed under the same terms as ranger.
4# ===================================================================
5#
6# NOTE: If you copied this file to /etc/ranger/commands_full.py or
7# ~/.config/ranger/commands_full.py, then it will NOT be loaded by ranger,
8# and only serve as a reference.
9#
10# ===================================================================
11# This file contains ranger's commands.
12# It's all in python; lines beginning with # are comments.
13#
14# Note that additional commands are automatically generated from the methods
15# of the class ranger.core.actions.Actions.
16#
17# You can customize commands in the files /etc/ranger/commands.py (system-wide)
18# and ~/.config/ranger/commands.py (per user).
19# They have the same syntax as this file. In fact, you can just copy this
20# file to ~/.config/ranger/commands_full.py with
21# `ranger --copy-config=commands_full' and make your modifications, don't
22# forget to rename it to commands.py. You can also use
23# `ranger --copy-config=commands' to copy a short sample commands.py that
24# has everything you need to get started.
25# But make sure you update your configs when you update ranger.
26#
27# ===================================================================
28# Every class defined here which is a subclass of `Command' will be used as a
29# command in ranger. Several methods are defined to interface with ranger:
30# execute(): called when the command is executed.
31# cancel(): called when closing the console.
32# tab(tabnum): called when <TAB> is pressed.
33# quick(): called after each keypress.
34#
35# tab() argument tabnum is 1 for <TAB> and -1 for <S-TAB> by default
36#
37# The return values for tab() can be either:
38# None: There is no tab completion
39# A string: Change the console to this string
40# A list/tuple/generator: cycle through every item in it
41#
42# The return value for quick() can be:
43# False: Nothing happens
44# True: Execute the command afterwards
45#
46# The return value for execute() and cancel() doesn't matter.
47#
48# ===================================================================
49# Commands have certain attributes and methods that facilitate parsing of
50# the arguments:
51#
52# self.line: The whole line that was written in the console.
53# self.args: A list of all (space-separated) arguments to the command.
54# self.quantifier: If this command was mapped to the key "X" and
55# the user pressed 6X, self.quantifier will be 6.
56# self.arg(n): The n-th argument, or an empty string if it doesn't exist.
57# self.rest(n): The n-th argument plus everything that followed. For example,
58# if the command was "search foo bar a b c", rest(2) will be "bar a b c"
59# self.start(n): Anything before the n-th argument. For example, if the
60# command was "search foo bar a b c", start(2) will be "search foo"
61#
62# ===================================================================
63# And this is a little reference for common ranger functions and objects:
64#
65# self.fm: A reference to the "fm" object which contains most information
66# about ranger.
67# self.fm.notify(string): Print the given string on the screen.
68# self.fm.notify(string, bad=True): Print the given string in RED.
69# self.fm.reload_cwd(): Reload the current working directory.
70# self.fm.thisdir: The current working directory. (A File object.)
71# self.fm.thisfile: The current file. (A File object too.)
72# self.fm.thistab.get_selection(): A list of all selected files.
73# self.fm.execute_console(string): Execute the string as a ranger command.
74# self.fm.open_console(string): Open the console with the given string
75# already typed in for you.
76# self.fm.move(direction): Moves the cursor in the given direction, which
77# can be something like down=3, up=5, right=1, left=1, to=6, ...
78#
79# File objects (for example self.fm.thisfile) have these useful attributes and
80# methods:
81#
82# tfile.path: The path to the file.
83# tfile.basename: The base name only.
84# tfile.load_content(): Force a loading of the directories content (which
85# obviously works with directories only)
86# tfile.is_directory: True/False depending on whether it's a directory.
87#
88# For advanced commands it is unavoidable to dive a bit into the source code
89# of ranger.
90# ===================================================================
91
92from __future__ import (absolute_import, division, print_function)
93
94from collections import deque
95import os
96import re
97
98from ranger.api.commands import Command
99
100
101class alias(Command):
102 """:alias <newcommand> <oldcommand>
103
104 Copies the oldcommand as newcommand.
105 """
106
107 context = 'browser'
108 resolve_macros = False
109
110 def execute(self):
111 if not self.arg(1) or not self.arg(2):
112 self.fm.notify('Syntax: alias <newcommand> <oldcommand>', bad=True)
113 return
114
115 self.fm.commands.alias(self.arg(1), self.rest(2))
116
117
118class echo(Command):
119 """:echo <text>
120
121 Display the text in the statusbar.
122 """
123
124 def execute(self):
125 self.fm.notify(self.rest(1))
126
127
128class cd(Command):
129 """:cd [-r] <path>
130
131 The cd command changes the directory.
132 If the path is a file, selects that file.
133 The command 'cd -' is equivalent to typing ``.
134 Using the option "-r" will get you to the real path.
135 """
136
137 def execute(self):
138 if self.arg(1) == '-r':
139 self.shift()
140 destination = os.path.realpath(self.rest(1))
141 if os.path.isfile(destination):
142 self.fm.select_file(destination)
143 return
144 else:
145 destination = self.rest(1)
146
147 if not destination:
148 destination = '~'
149
150 if destination == '-':
151 self.fm.enter_bookmark('`')
152 else:
153 self.fm.cd(destination)
154
155 def _tab_args(self):
156 # dest must be rest because path could contain spaces
157 if self.arg(1) == '-r':
158 start = self.start(2)
159 dest = self.rest(2)
160 else:
161 start = self.start(1)
162 dest = self.rest(1)
163
164 if dest:
165 head, tail = os.path.split(os.path.expanduser(dest))
166 if head:
167 dest_exp = os.path.join(os.path.normpath(head), tail)
168 else:
169 dest_exp = tail
170 else:
171 dest_exp = ''
172 return (start, dest_exp, os.path.join(self.fm.thisdir.path, dest_exp),
173 dest.endswith(os.path.sep))
174
175 @staticmethod
176 def _tab_paths(dest, dest_abs, ends_with_sep):
177 if not dest:
178 try:
179 return next(os.walk(dest_abs))[1], dest_abs
180 except (OSError, StopIteration):
181 return [], ''
182
183 if ends_with_sep:
184 try:
185 return [os.path.join(dest, path) for path in next(os.walk(dest_abs))[1]], ''
186 except (OSError, StopIteration):
187 return [], ''
188
189 return None, None
190
191 def _tab_match(self, path_user, path_file):
192 if self.fm.settings.cd_tab_case == 'insensitive':
193 path_user = path_user.lower()
194 path_file = path_file.lower()
195 elif self.fm.settings.cd_tab_case == 'smart' and path_user.islower():
196 path_file = path_file.lower()
197 return path_file.startswith(path_user)
198
199 def _tab_normal(self, dest, dest_abs):
200 dest_dir = os.path.dirname(dest)
201 dest_base = os.path.basename(dest)
202
203 try:
204 dirnames = next(os.walk(os.path.dirname(dest_abs)))[1]
205 except (OSError, StopIteration):
206 return [], ''
207
208 return [os.path.join(dest_dir, d) for d in dirnames if self._tab_match(dest_base, d)], ''
209
210 def _tab_fuzzy_match(self, basepath, tokens):
211 """ Find directories matching tokens recursively """
212 if not tokens:
213 tokens = ['']
214 paths = [basepath]
215 while True:
216 token = tokens.pop()
217 matches = []
218 for path in paths:
219 try:
220 directories = next(os.walk(path))[1]
221 except (OSError, StopIteration):
222 continue
223 matches += [os.path.join(path, d) for d in directories
224 if self._tab_match(token, d)]
225 if not tokens or not matches:
226 return matches
227 paths = matches
228
229 return None
230
231 def _tab_fuzzy(self, dest, dest_abs):
232 tokens = []
233 basepath = dest_abs
234 while True:
235 basepath_old = basepath
236 basepath, token = os.path.split(basepath)
237 if basepath == basepath_old:
238 break
239 if os.path.isdir(basepath_old) and not token.startswith('.'):
240 basepath = basepath_old
241 break
242 tokens.append(token)
243
244 paths = self._tab_fuzzy_match(basepath, tokens)
245 if not os.path.isabs(dest):
246 paths_rel = basepath
247 paths = [os.path.relpath(path, paths_rel) for path in paths]
248 else:
249 paths_rel = ''
250 return paths, paths_rel
251
252 def tab(self, tabnum):
253 from os.path import sep
254
255 start, dest, dest_abs, ends_with_sep = self._tab_args()
256
257 paths, paths_rel = self._tab_paths(dest, dest_abs, ends_with_sep)
258 if paths is None:
259 if self.fm.settings.cd_tab_fuzzy:
260 paths, paths_rel = self._tab_fuzzy(dest, dest_abs)
261 else:
262 paths, paths_rel = self._tab_normal(dest, dest_abs)
263
264 paths.sort()
265
266 if self.fm.settings.cd_bookmarks:
267 paths[0:0] = [
268 os.path.relpath(v.path, paths_rel) if paths_rel else v.path
269 for v in self.fm.bookmarks.dct.values() for path in paths
270 if v.path.startswith(os.path.join(paths_rel, path) + sep)
271 ]
272
273 if not paths:
274 return None
275 if len(paths) == 1:
276 return start + paths[0] + sep
277 return [start + dirname for dirname in paths]
278
279
280class chain(Command):
281 """:chain <command1>; <command2>; ...
282
283 Calls multiple commands at once, separated by semicolons.
284 """
285
286 def execute(self):
287 if not self.rest(1).strip():
288 self.fm.notify('Syntax: chain <command1>; <command2>; ...', bad=True)
289 return
290 for command in [s.strip() for s in self.rest(1).split(";")]:
291 self.fm.execute_console(command)
292
293
294class shell(Command):
295 escape_macros_for_shell = True
296
297 def execute(self):
298 if self.arg(1) and self.arg(1)[0] == '-':
299 flags = self.arg(1)[1:]
300 command = self.rest(2)
301 else:
302 flags = ''
303 command = self.rest(1)
304
305 if command:
306 self.fm.execute_command(command, flags=flags)
307
308 def tab(self, tabnum):
309 from ranger.ext.get_executables import get_executables
310 if self.arg(1) and self.arg(1)[0] == '-':
311 command = self.rest(2)
312 else:
313 command = self.rest(1)
314 start = self.line[0:len(self.line) - len(command)]
315
316 try:
317 position_of_last_space = command.rindex(" ")
318 except ValueError:
319 return (start + program + ' ' for program
320 in get_executables() if program.startswith(command))
321 if position_of_last_space == len(command) - 1:
322 selection = self.fm.thistab.get_selection()
323 if len(selection) == 1:
324 return self.line + selection[0].shell_escaped_basename + ' '
325 return self.line + '%s '
326
327 before_word, start_of_word = self.line.rsplit(' ', 1)
328 return (before_word + ' ' + file.shell_escaped_basename
329 for file in self.fm.thisdir.files or []
330 if file.shell_escaped_basename.startswith(start_of_word))
331
332
333class open_with(Command):
334
335 def execute(self):
336 app, flags, mode = self._get_app_flags_mode(self.rest(1))
337 self.fm.execute_file(
338 files=[f for f in self.fm.thistab.get_selection()],
339 app=app,
340 flags=flags,
341 mode=mode)
342
343 def tab(self, tabnum):
344 return self._tab_through_executables()
345
346 def _get_app_flags_mode(self, string): # pylint: disable=too-many-branches,too-many-statements
347 """Extracts the application, flags and mode from a string.
348
349 examples:
350 "mplayer f 1" => ("mplayer", "f", 1)
351 "atool 4" => ("atool", "", 4)
352 "p" => ("", "p", 0)
353 "" => None
354 """
355
356 app = ''
357 flags = ''
358 mode = 0
359 split = string.split()
360
361 if len(split) == 1:
362 part = split[0]
363 if self._is_app(part):
364 app = part
365 elif self._is_flags(part):
366 flags = part
367 elif self._is_mode(part):
368 mode = part
369
370 elif len(split) == 2:
371 part0 = split[0]
372 part1 = split[1]
373
374 if self._is_app(part0):
375 app = part0
376 if self._is_flags(part1):
377 flags = part1
378 elif self._is_mode(part1):
379 mode = part1
380 elif self._is_flags(part0):
381 flags = part0
382 if self._is_mode(part1):
383 mode = part1
384 elif self._is_mode(part0):
385 mode = part0
386 if self._is_flags(part1):
387 flags = part1
388
389 elif len(split) >= 3:
390 part0 = split[0]
391 part1 = split[1]
392 part2 = split[2]
393
394 if self._is_app(part0):
395 app = part0
396 if self._is_flags(part1):
397 flags = part1
398 if self._is_mode(part2):
399 mode = part2
400 elif self._is_mode(part1):
401 mode = part1
402 if self._is_flags(part2):
403 flags = part2
404 elif self._is_flags(part0):
405 flags = part0
406 if self._is_mode(part1):
407 mode = part1
408 elif self._is_mode(part0):
409 mode = part0
410 if self._is_flags(part1):
411 flags = part1
412
413 return app, flags, int(mode)
414
415 def _is_app(self, arg):
416 return not self._is_flags(arg) and not arg.isdigit()
417
418 @staticmethod
419 def _is_flags(arg):
420 from ranger.core.runner import ALLOWED_FLAGS
421 return all(x in ALLOWED_FLAGS for x in arg)
422
423 @staticmethod
424 def _is_mode(arg):
425 return all(x in '0123456789' for x in arg)
426
427
428class set_(Command):
429 """:set <option name>=<python expression>
430
431 Gives an option a new value.
432
433 Use `:set <option>!` to toggle or cycle it, e.g. `:set flush_input!`
434 """
435 name = 'set' # don't override the builtin set class
436
437 def execute(self):
438 name = self.arg(1)
439 name, value, _, toggle = self.parse_setting_line_v2()
440 if toggle:
441 self.fm.toggle_option(name)
442 else:
443 self.fm.set_option_from_string(name, value)
444
445 def tab(self, tabnum): # pylint: disable=too-many-return-statements
446 from ranger.gui.colorscheme import get_all_colorschemes
447 name, value, name_done = self.parse_setting_line()
448 settings = self.fm.settings
449 if not name:
450 return sorted(self.firstpart + setting for setting in settings)
451 if not value and not name_done:
452 return sorted(self.firstpart + setting for setting in settings
453 if setting.startswith(name))
454 if not value:
455 value_completers = {
456 "colorscheme":
457 # Cycle through colorschemes when name, but no value is specified
458 lambda: sorted(self.firstpart + colorscheme for colorscheme
459 in get_all_colorschemes(self.fm)),
460
461 "column_ratios":
462 lambda: self.firstpart + ",".join(map(str, settings[name])),
463 }
464
465 def default_value_completer():
466 return self.firstpart + str(settings[name])
467
468 return value_completers.get(name, default_value_completer)()
469 if bool in settings.types_of(name):
470 if 'true'.startswith(value.lower()):
471 return self.firstpart + 'True'
472 if 'false'.startswith(value.lower()):
473 return self.firstpart + 'False'
474 # Tab complete colorscheme values if incomplete value is present
475 if name == "colorscheme":
476 return sorted(self.firstpart + colorscheme for colorscheme
477 in get_all_colorschemes(self.fm) if colorscheme.startswith(value))
478 return None
479
480
481class setlocal(set_):
482 """:setlocal path=<regular expression> <option name>=<python expression>
483
484 Gives an option a new value.
485 """
486 PATH_RE_DQUOTED = re.compile(r'^setlocal\s+path="(.*?)"')
487 PATH_RE_SQUOTED = re.compile(r"^setlocal\s+path='(.*?)'")
488 PATH_RE_UNQUOTED = re.compile(r'^path=(.*?)$')
489
490 def _re_shift(self, match):
491 if not match:
492 return None
493 path = os.path.expanduser(match.group(1))
494 for _ in range(len(path.split())):
495 self.shift()
496 return path
497
498 def execute(self):
499 path = self._re_shift(self.PATH_RE_DQUOTED.match(self.line))
500 if path is None:
501 path = self._re_shift(self.PATH_RE_SQUOTED.match(self.line))
502 if path is None:
503 path = self._re_shift(self.PATH_RE_UNQUOTED.match(self.arg(1)))
504 if path is None and self.fm.thisdir:
505 path = self.fm.thisdir.path
506 if not path:
507 return
508
509 name, value, _ = self.parse_setting_line()
510 self.fm.set_option_from_string(name, value, localpath=path)
511
512
513class setintag(set_):
514 """:setintag <tag or tags> <option name>=<option value>
515
516 Sets an option for directories that are tagged with a specific tag.
517 """
518
519 def execute(self):
520 tags = self.arg(1)
521 self.shift()
522 name, value, _ = self.parse_setting_line()
523 self.fm.set_option_from_string(name, value, tags=tags)
524
525
526class default_linemode(Command):
527
528 def execute(self):
529 from ranger.container.fsobject import FileSystemObject
530
531 if len(self.args) < 2:
532 self.fm.notify(
533 "Usage: default_linemode [path=<regexp> | tag=<tag(s)>] <linemode>", bad=True)
534
535 # Extract options like "path=..." or "tag=..." from the command line
536 arg1 = self.arg(1)
537 method = "always"
538 argument = None
539 if arg1.startswith("path="):
540 method = "path"
541 argument = re.compile(arg1[5:])
542 self.shift()
543 elif arg1.startswith("tag="):
544 method = "tag"
545 argument = arg1[4:]
546 self.shift()
547
548 # Extract and validate the line mode from the command line
549 lmode = self.rest(1)
550 if lmode not in FileSystemObject.linemode_dict:
551 self.fm.notify(
552 "Invalid linemode: %s; should be %s" % (
553 lmode, "/".join(FileSystemObject.linemode_dict)),
554 bad=True,
555 )
556
557 # Add the prepared entry to the fm.default_linemodes
558 entry = [method, argument, lmode]
559 self.fm.default_linemodes.appendleft(entry)
560
561 # Redraw the columns
562 if self.fm.ui.browser:
563 for col in self.fm.ui.browser.columns:
564 col.need_redraw = True
565
566 def tab(self, tabnum):
567 return (self.arg(0) + " " + lmode
568 for lmode in self.fm.thisfile.linemode_dict.keys()
569 if lmode.startswith(self.arg(1)))
570
571
572class quit(Command): # pylint: disable=redefined-builtin
573 """:quit
574
575 Closes the current tab, if there's only one tab.
576 Otherwise quits if there are no tasks in progress.
577 """
578 def _exit_no_work(self):
579 if self.fm.loader.has_work():
580 self.fm.notify('Not quitting: Tasks in progress: Use `quit!` to force quit')
581 else:
582 self.fm.exit()
583
584 def execute(self):
585 if len(self.fm.tabs) >= 2:
586 self.fm.tab_close()
587 else:
588 self._exit_no_work()
589
590
591class quit_bang(Command):
592 """:quit!
593
594 Closes the current tab, if there's only one tab.
595 Otherwise force quits immediately.
596 """
597 name = 'quit!'
598 allow_abbrev = False
599
600 def execute(self):
601 if len(self.fm.tabs) >= 2:
602 self.fm.tab_close()
603 else:
604 self.fm.exit()
605
606
607class quitall(Command):
608 """:quitall
609
610 Quits if there are no tasks in progress.
611 """
612 def _exit_no_work(self):
613 if self.fm.loader.has_work():
614 self.fm.notify('Not quitting: Tasks in progress: Use `quitall!` to force quit')
615 else:
616 self.fm.exit()
617
618 def execute(self):
619 self._exit_no_work()
620
621
622class quitall_bang(Command):
623 """:quitall!
624
625 Force quits immediately.
626 """
627 name = 'quitall!'
628 allow_abbrev = False
629
630 def execute(self):
631 self.fm.exit()
632
633
634class terminal(Command):
635 """:terminal
636
637 Spawns an "x-terminal-emulator" starting in the current directory.
638 """
639
640 def execute(self):
641 from ranger.ext.get_executables import get_term
642 self.fm.run(get_term(), flags='f')
643
644
645class delete(Command):
646 """:delete
647
648 Tries to delete the selection or the files passed in arguments (if any).
649 The arguments use a shell-like escaping.
650
651 "Selection" is defined as all the "marked files" (by default, you
652 can mark files with space or v). If there are no marked files,
653 use the "current file" (where the cursor is)
654
655 When attempting to delete non-empty directories or multiple
656 marked files, it will require a confirmation.
657 """
658
659 allow_abbrev = False
660 escape_macros_for_shell = True
661
662 def execute(self):
663 import shlex
664 from functools import partial
665
666 def is_directory_with_files(path):
667 return os.path.isdir(path) and not os.path.islink(path) and len(os.listdir(path)) > 0
668
669 if self.rest(1):
670 files = shlex.split(self.rest(1))
671 many_files = (len(files) > 1 or is_directory_with_files(files[0]))
672 else:
673 cwd = self.fm.thisdir
674 tfile = self.fm.thisfile
675 if not cwd or not tfile:
676 self.fm.notify("Error: no file selected for deletion!", bad=True)
677 return
678
679 # relative_path used for a user-friendly output in the confirmation.
680 files = [f.relative_path for f in self.fm.thistab.get_selection()]
681 many_files = (cwd.marked_items or is_directory_with_files(tfile.path))
682
683 confirm = self.fm.settings.confirm_on_delete
684 if confirm != 'never' and (confirm != 'multiple' or many_files):
685 self.fm.ui.console.ask(
686 "Confirm deletion of: %s (y/N)" % ', '.join(files),
687 partial(self._question_callback, files),
688 ('n', 'N', 'y', 'Y'),
689 )
690 else:
691 # no need for a confirmation, just delete
692 self.fm.delete(files)
693
694 def tab(self, tabnum):
695 return self._tab_directory_content()
696
697 def _question_callback(self, files, answer):
698 if answer == 'y' or answer == 'Y':
699 self.fm.delete(files)
700
701
702class jump_non(Command):
703 """:jump_non [-FLAGS...]
704
705 Jumps to first non-directory if highlighted file is a directory and vice versa.
706
707 Flags:
708 -r Jump in reverse order
709 -w Wrap around if reaching end of filelist
710 """
711 def __init__(self, *args, **kwargs):
712 super(jump_non, self).__init__(*args, **kwargs)
713
714 flags, _ = self.parse_flags()
715 self._flag_reverse = 'r' in flags
716 self._flag_wrap = 'w' in flags
717
718 @staticmethod
719 def _non(fobj, is_directory):
720 return fobj.is_directory if not is_directory else not fobj.is_directory
721
722 def execute(self):
723 tfile = self.fm.thisfile
724 passed = False
725 found_before = None
726 found_after = None
727 for fobj in self.fm.thisdir.files[::-1] if self._flag_reverse else self.fm.thisdir.files:
728 if fobj.path == tfile.path:
729 passed = True
730 continue
731
732 if passed:
733 if self._non(fobj, tfile.is_directory):
734 found_after = fobj.path
735 break
736 elif not found_before and self._non(fobj, tfile.is_directory):
737 found_before = fobj.path
738
739 if found_after:
740 self.fm.select_file(found_after)
741 elif self._flag_wrap and found_before:
742 self.fm.select_file(found_before)
743
744
745class mark_tag(Command):
746 """:mark_tag [<tags>]
747
748 Mark all tags that are tagged with either of the given tags.
749 When leaving out the tag argument, all tagged files are marked.
750 """
751 do_mark = True
752
753 def execute(self):
754 cwd = self.fm.thisdir
755 tags = self.rest(1).replace(" ", "")
756 if not self.fm.tags or not cwd.files:
757 return
758 for fileobj in cwd.files:
759 try:
760 tag = self.fm.tags.tags[fileobj.realpath]
761 except KeyError:
762 continue
763 if not tags or tag in tags:
764 cwd.mark_item(fileobj, val=self.do_mark)
765 self.fm.ui.status.need_redraw = True
766 self.fm.ui.need_redraw = True
767
768
769class console(Command):
770 """:console <command>
771
772 Open the console with the given command.
773 """
774
775 def execute(self):
776 position = None
777 if self.arg(1)[0:2] == '-p':
778 try:
779 position = int(self.arg(1)[2:])
780 except ValueError:
781 pass
782 else:
783 self.shift()
784 self.fm.open_console(self.rest(1), position=position)
785
786
787class load_copy_buffer(Command):
788 """:load_copy_buffer
789
790 Load the copy buffer from datadir/copy_buffer
791 """
792 copy_buffer_filename = 'copy_buffer'
793
794 def execute(self):
795 import sys
796 from ranger.container.file import File
797 from os.path import exists
798 fname = self.fm.datapath(self.copy_buffer_filename)
799 unreadable = IOError if sys.version_info[0] < 3 else OSError
800 try:
801 fobj = open(fname, 'r')
802 except unreadable:
803 return self.fm.notify(
804 "Cannot open %s" % (fname or self.copy_buffer_filename), bad=True)
805
806 self.fm.copy_buffer = set(File(g)
807 for g in fobj.read().split("\n") if exists(g))
808 fobj.close()
809 self.fm.ui.redraw_main_column()
810 return None
811
812
813class save_copy_buffer(Command):
814 """:save_copy_buffer
815
816 Save the copy buffer to datadir/copy_buffer
817 """
818 copy_buffer_filename = 'copy_buffer'
819
820 def execute(self):
821 import sys
822 fname = None
823 fname = self.fm.datapath(self.copy_buffer_filename)
824 unwritable = IOError if sys.version_info[0] < 3 else OSError
825 try:
826 fobj = open(fname, 'w')
827 except unwritable:
828 return self.fm.notify("Cannot open %s" %
829 (fname or self.copy_buffer_filename), bad=True)
830 fobj.write("\n".join(fobj.path for fobj in self.fm.copy_buffer))
831 fobj.close()
832 return None
833
834
835class unmark_tag(mark_tag):
836 """:unmark_tag [<tags>]
837
838 Unmark all tags that are tagged with either of the given tags.
839 When leaving out the tag argument, all tagged files are unmarked.
840 """
841 do_mark = False
842
843
844class mkdir(Command):
845 """:mkdir <dirname>
846
847 Creates a directory with the name <dirname>.
848 """
849
850 def execute(self):
851 from os.path import join, expanduser, lexists
852 from os import makedirs
853
854 dirname = join(self.fm.thisdir.path, expanduser(self.rest(1)))
855 if not lexists(dirname):
856 makedirs(dirname)
857 else:
858 self.fm.notify("file/directory exists!", bad=True)
859
860 def tab(self, tabnum):
861 return self._tab_directory_content()
862
863
864class touch(Command):
865 """:touch <fname>
866
867 Creates a file with the name <fname>.
868 """
869
870 def execute(self):
871 from os.path import join, expanduser, lexists
872
873 fname = join(self.fm.thisdir.path, expanduser(self.rest(1)))
874 if not lexists(fname):
875 open(fname, 'a').close()
876 else:
877 self.fm.notify("file/directory exists!", bad=True)
878
879 def tab(self, tabnum):
880 return self._tab_directory_content()
881
882
883class edit(Command):
884 """:edit <filename>
885
886 Opens the specified file in vim
887 """
888
889 def execute(self):
890 if not self.arg(1):
891 self.fm.edit_file(self.fm.thisfile.path)
892 else:
893 self.fm.edit_file(self.rest(1))
894
895 def tab(self, tabnum):
896 return self._tab_directory_content()
897
898
899class eval_(Command):
900 """:eval [-q] <python code>
901
902 Evaluates the python code.
903 `fm' is a reference to the FM instance.
904 To display text, use the function `p'.
905
906 Examples:
907 :eval fm
908 :eval len(fm.directories)
909 :eval p("Hello World!")
910 """
911 name = 'eval'
912 resolve_macros = False
913
914 def execute(self):
915 # The import is needed so eval() can access the ranger module
916 import ranger # NOQA pylint: disable=unused-import,unused-variable
917 if self.arg(1) == '-q':
918 code = self.rest(2)
919 quiet = True
920 else:
921 code = self.rest(1)
922 quiet = False
923 global cmd, fm, p, quantifier # pylint: disable=invalid-name,global-variable-undefined
924 fm = self.fm
925 cmd = self.fm.execute_console
926 p = fm.notify
927 quantifier = self.quantifier
928 try:
929 try:
930 result = eval(code) # pylint: disable=eval-used
931 except SyntaxError:
932 exec(code) # pylint: disable=exec-used
933 else:
934 if result and not quiet:
935 p(result)
936 except Exception as err: # pylint: disable=broad-except
937 fm.notify("The error `%s` was caused by evaluating the "
938 "following code: `%s`" % (err, code), bad=True)
939
940
941class rename(Command):
942 """:rename <newname>
943
944 Changes the name of the currently highlighted file to <newname>
945 """
946
947 def execute(self):
948 from ranger.container.file import File
949 from os import access
950
951 new_name = self.rest(1)
952
953 if not new_name:
954 return self.fm.notify('Syntax: rename <newname>', bad=True)
955
956 if new_name == self.fm.thisfile.relative_path:
957 return None
958
959 if access(new_name, os.F_OK):
960 return self.fm.notify("Can't rename: file already exists!", bad=True)
961
962 if self.fm.rename(self.fm.thisfile, new_name):
963 file_new = File(new_name)
964 self.fm.bookmarks.update_path(self.fm.thisfile.path, file_new)
965 self.fm.tags.update_path(self.fm.thisfile.path, file_new.path)
966 self.fm.thisdir.pointed_obj = file_new
967 self.fm.thisfile = file_new
968
969 return None
970
971 def tab(self, tabnum):
972 return self._tab_directory_content()
973
974
975class rename_append(Command):
976 """:rename_append [-FLAGS...]
977
978 Opens the console with ":rename <current file>" with the cursor positioned
979 before the file extension.
980
981 Flags:
982 -a Position before all extensions
983 -r Remove everything before extensions
984 """
985 def __init__(self, *args, **kwargs):
986 super(rename_append, self).__init__(*args, **kwargs)
987
988 flags, _ = self.parse_flags()
989 self._flag_ext_all = 'a' in flags
990 self._flag_remove = 'r' in flags
991
992 def execute(self):
993 from ranger import MACRO_DELIMITER, MACRO_DELIMITER_ESC
994
995 tfile = self.fm.thisfile
996 relpath = tfile.relative_path.replace(MACRO_DELIMITER, MACRO_DELIMITER_ESC)
997 basename = tfile.basename.replace(MACRO_DELIMITER, MACRO_DELIMITER_ESC)
998
999 if basename.find('.') <= 0:
1000 self.fm.open_console('rename ' + relpath)
1001 return
1002
1003 if self._flag_ext_all:
1004 pos_ext = re.search(r'[^.]+', basename).end(0)
1005 else:
1006 pos_ext = basename.rindex('.')
1007 pos = len(relpath) - len(basename) + pos_ext
1008
1009 if self._flag_remove:
1010 relpath = relpath[:-len(basename)] + basename[pos_ext:]
1011 pos -= pos_ext
1012
1013 self.fm.open_console('rename ' + relpath, position=(7 + pos))
1014
1015
1016class chmod(Command):
1017 """:chmod <octal number>
1018
1019 Sets the permissions of the selection to the octal number.
1020
1021 The octal number is between 0 and 777. The digits specify the
1022 permissions for the user, the group and others.
1023
1024 A 1 permits execution, a 2 permits writing, a 4 permits reading.
1025 Add those numbers to combine them. So a 7 permits everything.
1026 """
1027
1028 def execute(self):
1029 mode_str = self.rest(1)
1030 if not mode_str:
1031 if not self.quantifier:
1032 self.fm.notify("Syntax: chmod <octal number>", bad=True)
1033 return
1034 mode_str = str(self.quantifier)
1035
1036 try:
1037 mode = int(mode_str, 8)
1038 if mode < 0 or mode > 0o777:
1039 raise ValueError
1040 except ValueError:
1041 self.fm.notify("Need an octal number between 0 and 777!", bad=True)
1042 return
1043
1044 for fobj in self.fm.thistab.get_selection():
1045 try:
1046 os.chmod(fobj.path, mode)
1047 except OSError as ex:
1048 self.fm.notify(ex)
1049
1050 # reloading directory. maybe its better to reload the selected
1051 # files only.
1052 self.fm.thisdir.content_outdated = True
1053
1054
1055class bulkrename(Command):
1056 """:bulkrename
1057
1058 This command opens a list of selected files in an external editor.
1059 After you edit and save the file, it will generate a shell script
1060 which does bulk renaming according to the changes you did in the file.
1061
1062 This shell script is opened in an editor for you to review.
1063 After you close it, it will be executed.
1064 """
1065
1066 def execute(self): # pylint: disable=too-many-locals,too-many-statements
1067 import sys
1068 import tempfile
1069 from ranger.container.file import File
1070 from ranger.ext.shell_escape import shell_escape as esc
1071 py3 = sys.version_info[0] >= 3
1072
1073 # Create and edit the file list
1074 filenames = [f.relative_path for f in self.fm.thistab.get_selection()]
1075 listfile = tempfile.NamedTemporaryFile(delete=False)
1076 listpath = listfile.name
1077
1078 if py3:
1079 listfile.write("\n".join(filenames).encode("utf-8"))
1080 else:
1081 listfile.write("\n".join(filenames))
1082 listfile.close()
1083 self.fm.execute_file([File(listpath)], app='editor')
1084 listfile = open(listpath, 'r')
1085 new_filenames = listfile.read().split("\n")
1086 listfile.close()
1087 os.unlink(listpath)
1088 if all(a == b for a, b in zip(filenames, new_filenames)):
1089 self.fm.notify("No renaming to be done!")
1090 return
1091
1092 # Generate script
1093 cmdfile = tempfile.NamedTemporaryFile()
1094 script_lines = []
1095 script_lines.append("# This file will be executed when you close the editor.\n")
1096 script_lines.append("# Please double-check everything, clear the file to abort.\n")
1097 script_lines.extend("mv -vi -- %s %s\n" % (esc(old), esc(new))
1098 for old, new in zip(filenames, new_filenames) if old != new)
1099 script_content = "".join(script_lines)
1100 if py3:
1101 cmdfile.write(script_content.encode("utf-8"))
1102 else:
1103 cmdfile.write(script_content)
1104 cmdfile.flush()
1105
1106 # Open the script and let the user review it, then check if the script
1107 # was modified by the user
1108 self.fm.execute_file([File(cmdfile.name)], app='editor')
1109 cmdfile.seek(0)
1110 script_was_edited = (script_content != cmdfile.read())
1111
1112 # Do the renaming
1113 self.fm.run(['/bin/sh', cmdfile.name], flags='w')
1114 cmdfile.close()
1115
1116 # Retag the files, but only if the script wasn't changed during review,
1117 # because only then we know which are the source and destination files.
1118 if not script_was_edited:
1119 tags_changed = False
1120 for old, new in zip(filenames, new_filenames):
1121 if old != new:
1122 oldpath = self.fm.thisdir.path + '/' + old
1123 newpath = self.fm.thisdir.path + '/' + new
1124 if oldpath in self.fm.tags:
1125 old_tag = self.fm.tags.tags[oldpath]
1126 self.fm.tags.remove(oldpath)
1127 self.fm.tags.tags[newpath] = old_tag
1128 tags_changed = True
1129 if tags_changed:
1130 self.fm.tags.dump()
1131 else:
1132 fm.notify("files have not been retagged")
1133
1134
1135class relink(Command):
1136 """:relink <newpath>
1137
1138 Changes the linked path of the currently highlighted symlink to <newpath>
1139 """
1140
1141 def execute(self):
1142 new_path = self.rest(1)
1143 tfile = self.fm.thisfile
1144
1145 if not new_path:
1146 return self.fm.notify('Syntax: relink <newpath>', bad=True)
1147
1148 if not tfile.is_link:
1149 return self.fm.notify('%s is not a symlink!' % tfile.relative_path, bad=True)
1150
1151 if new_path == os.readlink(tfile.path):
1152 return None
1153
1154 try:
1155 os.remove(tfile.path)
1156 os.symlink(new_path, tfile.path)
1157 except OSError as err:
1158 self.fm.notify(err)
1159
1160 self.fm.reset()
1161 self.fm.thisdir.pointed_obj = tfile
1162 self.fm.thisfile = tfile
1163
1164 return None
1165
1166 def tab(self, tabnum):
1167 if not self.rest(1):
1168 return self.line + os.readlink(self.fm.thisfile.path)
1169 return self._tab_directory_content()
1170
1171
1172class help_(Command):
1173 """:help
1174
1175 Display ranger's manual page.
1176 """
1177 name = 'help'
1178
1179 def execute(self):
1180 def callback(answer):
1181 if answer == "q":
1182 return
1183 elif answer == "m":
1184 self.fm.display_help()
1185 elif answer == "c":
1186 self.fm.dump_commands()
1187 elif answer == "k":
1188 self.fm.dump_keybindings()
1189 elif answer == "s":
1190 self.fm.dump_settings()
1191
1192 self.fm.ui.console.ask(
1193 "View [m]an page, [k]ey bindings, [c]ommands or [s]ettings? (press q to abort)",
1194 callback,
1195 list("mqkcs")
1196 )
1197
1198
1199class copymap(Command):
1200 """:copymap <keys> <newkeys1> [<newkeys2>...]
1201
1202 Copies a "browser" keybinding from <keys> to <newkeys>
1203 """
1204 context = 'browser'
1205
1206 def execute(self):
1207 if not self.arg(1) or not self.arg(2):
1208 return self.fm.notify("Not enough arguments", bad=True)
1209
1210 for arg in self.args[2:]:
1211 self.fm.ui.keymaps.copy(self.context, self.arg(1), arg)
1212
1213 return None
1214
1215
1216class copypmap(copymap):
1217 """:copypmap <keys> <newkeys1> [<newkeys2>...]
1218
1219 Copies a "pager" keybinding from <keys> to <newkeys>
1220 """
1221 context = 'pager'
1222
1223
1224class copycmap(copymap):
1225 """:copycmap <keys> <newkeys1> [<newkeys2>...]
1226
1227 Copies a "console" keybinding from <keys> to <newkeys>
1228 """
1229 context = 'console'
1230
1231
1232class copytmap(copymap):
1233 """:copycmap <keys> <newkeys1> [<newkeys2>...]
1234
1235 Copies a "taskview" keybinding from <keys> to <newkeys>
1236 """
1237 context = 'taskview'
1238
1239
1240class unmap(Command):
1241 """:unmap <keys> [<keys2>, ...]
1242
1243 Remove the given "browser" mappings
1244 """
1245 context = 'browser'
1246
1247 def execute(self):
1248 for arg in self.args[1:]:
1249 self.fm.ui.keymaps.unbind(self.context, arg)
1250
1251
1252class cunmap(unmap):
1253 """:cunmap <keys> [<keys2>, ...]
1254
1255 Remove the given "console" mappings
1256 """
1257 context = 'browser'
1258
1259
1260class punmap(unmap):
1261 """:punmap <keys> [<keys2>, ...]
1262
1263 Remove the given "pager" mappings
1264 """
1265 context = 'pager'
1266
1267
1268class tunmap(unmap):
1269 """:tunmap <keys> [<keys2>, ...]
1270
1271 Remove the given "taskview" mappings
1272 """
1273 context = 'taskview'
1274
1275
1276class map_(Command):
1277 """:map <keysequence> <command>
1278
1279 Maps a command to a keysequence in the "browser" context.
1280
1281 Example:
1282 map j move down
1283 map J move down 10
1284 """
1285 name = 'map'
1286 context = 'browser'
1287 resolve_macros = False
1288
1289 def execute(self):
1290 if not self.arg(1) or not self.arg(2):
1291 self.fm.notify("Syntax: {0} <keysequence> <command>".format(self.get_name()), bad=True)
1292 return
1293
1294 self.fm.ui.keymaps.bind(self.context, self.arg(1), self.rest(2))
1295
1296
1297class cmap(map_):
1298 """:cmap <keysequence> <command>
1299
1300 Maps a command to a keysequence in the "console" context.
1301
1302 Example:
1303 cmap <ESC> console_close
1304 cmap <C-x> console_type test
1305 """
1306 context = 'console'
1307
1308
1309class tmap(map_):
1310 """:tmap <keysequence> <command>
1311
1312 Maps a command to a keysequence in the "taskview" context.
1313 """
1314 context = 'taskview'
1315
1316
1317class pmap(map_):
1318 """:pmap <keysequence> <command>
1319
1320 Maps a command to a keysequence in the "pager" context.
1321 """
1322 context = 'pager'
1323
1324
1325class scout(Command):
1326 """:scout [-FLAGS...] <pattern>
1327
1328 Swiss army knife command for searching, traveling and filtering files.
1329
1330 Flags:
1331 -a Automatically open a file on unambiguous match
1332 -e Open the selected file when pressing enter
1333 -f Filter files that match the current search pattern
1334 -g Interpret pattern as a glob pattern
1335 -i Ignore the letter case of the files
1336 -k Keep the console open when changing a directory with the command
1337 -l Letter skipping; e.g. allow "rdme" to match the file "readme"
1338 -m Mark the matching files after pressing enter
1339 -M Unmark the matching files after pressing enter
1340 -p Permanent filter: hide non-matching files after pressing enter
1341 -r Interpret pattern as a regular expression pattern
1342 -s Smart case; like -i unless pattern contains upper case letters
1343 -t Apply filter and search pattern as you type
1344 -v Inverts the match
1345
1346 Multiple flags can be combined. For example, ":scout -gpt" would create
1347 a :filter-like command using globbing.
1348 """
1349 # pylint: disable=bad-whitespace
1350 AUTO_OPEN = 'a'
1351 OPEN_ON_ENTER = 'e'
1352 FILTER = 'f'
1353 SM_GLOB = 'g'
1354 IGNORE_CASE = 'i'
1355 KEEP_OPEN = 'k'
1356 SM_LETTERSKIP = 'l'
1357 MARK = 'm'
1358 UNMARK = 'M'
1359 PERM_FILTER = 'p'
1360 SM_REGEX = 'r'
1361 SMART_CASE = 's'
1362 AS_YOU_TYPE = 't'
1363 INVERT = 'v'
1364 # pylint: enable=bad-whitespace
1365
1366 def __init__(self, *args, **kwargs):
1367 super(scout, self).__init__(*args, **kwargs)
1368 self._regex = None
1369 self.flags, self.pattern = self.parse_flags()
1370
1371 def execute(self): # pylint: disable=too-many-branches
1372 thisdir = self.fm.thisdir
1373 flags = self.flags
1374 pattern = self.pattern
1375 regex = self._build_regex()
1376 count = self._count(move=True)
1377
1378 self.fm.thistab.last_search = regex
1379 self.fm.set_search_method(order="search")
1380
1381 if (self.MARK in flags or self.UNMARK in flags) and thisdir.files:
1382 value = flags.find(self.MARK) > flags.find(self.UNMARK)
1383 if self.FILTER in flags:
1384 for fobj in thisdir.files:
1385 thisdir.mark_item(fobj, value)
1386 else:
1387 for fobj in thisdir.files:
1388 if regex.search(fobj.relative_path):
1389 thisdir.mark_item(fobj, value)
1390
1391 if self.PERM_FILTER in flags:
1392 thisdir.filter = regex if pattern else None
1393
1394 # clean up:
1395 self.cancel()
1396
1397 if self.OPEN_ON_ENTER in flags or \
1398 (self.AUTO_OPEN in flags and count == 1):
1399 if pattern == '..':
1400 self.fm.cd(pattern)
1401 else:
1402 self.fm.move(right=1)
1403 if self.quickly_executed:
1404 self.fm.block_input(0.5)
1405
1406 if self.KEEP_OPEN in flags and thisdir != self.fm.thisdir:
1407 # reopen the console:
1408 if not pattern:
1409 self.fm.open_console(self.line)
1410 else:
1411 self.fm.open_console(self.line[0:-len(pattern)])
1412
1413 if self.quickly_executed and thisdir != self.fm.thisdir and pattern != "..":
1414 self.fm.block_input(0.5)
1415
1416 def cancel(self):
1417 self.fm.thisdir.temporary_filter = None
1418 self.fm.thisdir.refilter()
1419
1420 def quick(self):
1421 asyoutype = self.AS_YOU_TYPE in self.flags
1422 if self.FILTER in self.flags:
1423 self.fm.thisdir.temporary_filter = self._build_regex()
1424 if self.PERM_FILTER in self.flags and asyoutype:
1425 self.fm.thisdir.filter = self._build_regex()
1426 if self.FILTER in self.flags or self.PERM_FILTER in self.flags:
1427 self.fm.thisdir.refilter()
1428 if self._count(move=asyoutype) == 1 and self.AUTO_OPEN in self.flags:
1429 return True
1430 return False
1431
1432 def tab(self, tabnum):
1433 self._count(move=True, offset=tabnum)
1434
1435 def _build_regex(self):
1436 if self._regex is not None:
1437 return self._regex
1438
1439 frmat = "%s"
1440 flags = self.flags
1441 pattern = self.pattern
1442
1443 if pattern == ".":
1444 return re.compile("")
1445
1446 # Handle carets at start and dollar signs at end separately
1447 if pattern.startswith('^'):
1448 pattern = pattern[1:]
1449 frmat = "^" + frmat
1450 if pattern.endswith('$'):
1451 pattern = pattern[:-1]
1452 frmat += "$"
1453
1454 # Apply one of the search methods
1455 if self.SM_REGEX in flags:
1456 regex = pattern
1457 elif self.SM_GLOB in flags:
1458 regex = re.escape(pattern).replace("\\*", ".*").replace("\\?", ".")
1459 elif self.SM_LETTERSKIP in flags:
1460 regex = ".*".join(re.escape(c) for c in pattern)
1461 else:
1462 regex = re.escape(pattern)
1463
1464 regex = frmat % regex
1465
1466 # Invert regular expression if necessary
1467 if self.INVERT in flags:
1468 regex = "^(?:(?!%s).)*$" % regex
1469
1470 # Compile Regular Expression
1471 # pylint: disable=no-member
1472 options = re.UNICODE
1473 if self.IGNORE_CASE in flags or self.SMART_CASE in flags and \
1474 pattern.islower():
1475 options |= re.IGNORECASE
1476 # pylint: enable=no-member
1477 try:
1478 self._regex = re.compile(regex, options)
1479 except re.error:
1480 self._regex = re.compile("")
1481 return self._regex
1482
1483 def _count(self, move=False, offset=0):
1484 count = 0
1485 cwd = self.fm.thisdir
1486 pattern = self.pattern
1487
1488 if not pattern or not cwd.files:
1489 return 0
1490 if pattern == '.':
1491 return 0
1492 if pattern == '..':
1493 return 1
1494
1495 deq = deque(cwd.files)
1496 deq.rotate(-cwd.pointer - offset)
1497 i = offset
1498 regex = self._build_regex()
1499 for fsobj in deq:
1500 if regex.search(fsobj.relative_path):
1501 count += 1
1502 if move and count == 1:
1503 cwd.move(to=(cwd.pointer + i) % len(cwd.files))
1504 self.fm.thisfile = cwd.pointed_obj
1505 if count > 1:
1506 return count
1507 i += 1
1508
1509 return count == 1
1510
1511
1512class narrow(Command):
1513 """
1514 :narrow
1515
1516 Show only the files selected right now. If no files are selected,
1517 disable narrowing.
1518 """
1519 def execute(self):
1520 if self.fm.thisdir.marked_items:
1521 selection = [f.basename for f in self.fm.thistab.get_selection()]
1522 self.fm.thisdir.narrow_filter = selection
1523 else:
1524 self.fm.thisdir.narrow_filter = None
1525 self.fm.thisdir.refilter()
1526
1527
1528class filter_inode_type(Command):
1529 """
1530 :filter_inode_type [dfl]
1531
1532 Displays only the files of specified inode type. Parameters
1533 can be combined.
1534
1535 d display directories
1536 f display files
1537 l display links
1538 """
1539
1540 def execute(self):
1541 if not self.arg(1):
1542 self.fm.thisdir.inode_type_filter = ""
1543 else:
1544 self.fm.thisdir.inode_type_filter = self.arg(1)
1545 self.fm.thisdir.refilter()
1546
1547
1548class filter_stack(Command):
1549 """
1550 :filter_stack ...
1551
1552 Manages the filter stack.
1553
1554 filter_stack add FILTER_TYPE ARGS...
1555 filter_stack pop
1556 filter_stack decompose
1557 filter_stack rotate [N=1]
1558 filter_stack clear
1559 filter_stack show
1560 """
1561 def execute(self):
1562 from ranger.core.filter_stack import SIMPLE_FILTERS, FILTER_COMBINATORS
1563
1564 subcommand = self.arg(1)
1565
1566 if subcommand == "add":
1567 try:
1568 self.fm.thisdir.filter_stack.append(
1569 SIMPLE_FILTERS[self.arg(2)](self.rest(3))
1570 )
1571 except KeyError:
1572 FILTER_COMBINATORS[self.arg(2)](self.fm.thisdir.filter_stack)
1573 elif subcommand == "pop":
1574 self.fm.thisdir.filter_stack.pop()
1575 elif subcommand == "decompose":
1576 inner_filters = self.fm.thisdir.filter_stack.pop().decompose()
1577 if inner_filters:
1578 self.fm.thisdir.filter_stack.extend(inner_filters)
1579 elif subcommand == "clear":
1580 self.fm.thisdir.filter_stack = []
1581 elif subcommand == "rotate":
1582 rotate_by = int(self.arg(2) or 1)
1583 self.fm.thisdir.filter_stack = (
1584 self.fm.thisdir.filter_stack[-rotate_by:]
1585 + self.fm.thisdir.filter_stack[:-rotate_by]
1586 )
1587 elif subcommand == "show":
1588 stack = list(map(str, self.fm.thisdir.filter_stack))
1589 pager = self.fm.ui.open_pager()
1590 pager.set_source(["Filter stack: "] + stack)
1591 pager.move(to=100, percentage=True)
1592 return
1593 else:
1594 self.fm.notify(
1595 "Unknown subcommand: {}".format(subcommand),
1596 bad=True
1597 )
1598 return
1599
1600 self.fm.thisdir.refilter()
1601
1602
1603class grep(Command):
1604 """:grep <string>
1605
1606 Looks for a string in all marked files or directories
1607 """
1608
1609 def execute(self):
1610 if self.rest(1):
1611 action = ['grep', '--line-number']
1612 action.extend(['-e', self.rest(1), '-r'])
1613 action.extend(f.path for f in self.fm.thistab.get_selection())
1614 self.fm.execute_command(action, flags='p')
1615
1616
1617class flat(Command):
1618 """
1619 :flat <level>
1620
1621 Flattens the directory view up to the specified level.
1622
1623 -1 fully flattened
1624 0 remove flattened view
1625 """
1626
1627 def execute(self):
1628 try:
1629 level_str = self.rest(1)
1630 level = int(level_str)
1631 except ValueError:
1632 level = self.quantifier
1633 if level is None:
1634 self.fm.notify("Syntax: flat <level>", bad=True)
1635 return
1636 if level < -1:
1637 self.fm.notify("Need an integer number (-1, 0, 1, ...)", bad=True)
1638 self.fm.thisdir.unload()
1639 self.fm.thisdir.flat = level
1640 self.fm.thisdir.load_content()
1641
1642# Version control commands
1643# --------------------------------
1644
1645
1646class stage(Command):
1647 """
1648 :stage
1649
1650 Stage selected files for the corresponding version control system
1651 """
1652
1653 def execute(self):
1654 from ranger.ext.vcs import VcsError
1655
1656 if self.fm.thisdir.vcs and self.fm.thisdir.vcs.track:
1657 filelist = [f.path for f in self.fm.thistab.get_selection()]
1658 try:
1659 self.fm.thisdir.vcs.action_add(filelist)
1660 except VcsError as ex:
1661 self.fm.notify('Unable to stage files: {0}'.format(ex))
1662 self.fm.ui.vcsthread.process(self.fm.thisdir)
1663 else:
1664 self.fm.notify('Unable to stage files: Not in repository')
1665
1666
1667class unstage(Command):
1668 """
1669 :unstage
1670
1671 Unstage selected files for the corresponding version control system
1672 """
1673
1674 def execute(self):
1675 from ranger.ext.vcs import VcsError
1676
1677 if self.fm.thisdir.vcs and self.fm.thisdir.vcs.track:
1678 filelist = [f.path for f in self.fm.thistab.get_selection()]
1679 try:
1680 self.fm.thisdir.vcs.action_reset(filelist)
1681 except VcsError as ex:
1682 self.fm.notify('Unable to unstage files: {0}'.format(ex))
1683 self.fm.ui.vcsthread.process(self.fm.thisdir)
1684 else:
1685 self.fm.notify('Unable to unstage files: Not in repository')
1686
1687# Metadata commands
1688# --------------------------------
1689
1690
1691class prompt_metadata(Command):
1692 """
1693 :prompt_metadata <key1> [<key2> [<key3> ...]]
1694
1695 Prompt the user to input metadata for multiple keys in a row.
1696 """
1697
1698 _command_name = "meta"
1699 _console_chain = None
1700
1701 def execute(self):
1702 prompt_metadata._console_chain = self.args[1:]
1703 self._process_command_stack()
1704
1705 def _process_command_stack(self):
1706 if prompt_metadata._console_chain:
1707 key = prompt_metadata._console_chain.pop()
1708 self._fill_console(key)
1709 else:
1710 for col in self.fm.ui.browser.columns:
1711 col.need_redraw = True
1712
1713 def _fill_console(self, key):
1714 metadata = self.fm.metadata.get_metadata(self.fm.thisfile.path)
1715 if key in metadata and metadata[key]:
1716 existing_value = metadata[key]
1717 else:
1718 existing_value = ""
1719 text = "%s %s %s" % (self._command_name, key, existing_value)
1720 self.fm.open_console(text, position=len(text))
1721
1722
1723class meta(prompt_metadata):
1724 """
1725 :meta <key> [<value>]
1726
1727 Change metadata of a file. Deletes the key if value is empty.
1728 """
1729
1730 def execute(self):
1731 key = self.arg(1)
1732 update_dict = dict()
1733 update_dict[key] = self.rest(2)
1734 selection = self.fm.thistab.get_selection()
1735 for fobj in selection:
1736 self.fm.metadata.set_metadata(fobj.path, update_dict)
1737 self._process_command_stack()
1738
1739 def tab(self, tabnum):
1740 key = self.arg(1)
1741 metadata = self.fm.metadata.get_metadata(self.fm.thisfile.path)
1742 if key in metadata and metadata[key]:
1743 return [" ".join([self.arg(0), self.arg(1), metadata[key]])]
1744 return [self.arg(0) + " " + k for k in sorted(metadata)
1745 if k.startswith(self.arg(1))]
1746
1747
1748class linemode(default_linemode):
1749 """
1750 :linemode <mode>
1751
1752 Change what is displayed as a filename.
1753
1754 - "mode" may be any of the defined linemodes (see: ranger.core.linemode).
1755 "normal" is mapped to "filename".
1756 """
1757
1758 def execute(self):
1759 mode = self.arg(1)
1760
1761 if mode == "normal":
1762 from ranger.core.linemode import DEFAULT_LINEMODE
1763 mode = DEFAULT_LINEMODE
1764
1765 if mode not in self.fm.thisfile.linemode_dict:
1766 self.fm.notify("Unhandled linemode: `%s'" % mode, bad=True)
1767 return
1768
1769 self.fm.thisdir.set_linemode_of_children(mode)
1770
1771 # Ask the browsercolumns to redraw
1772 for col in self.fm.ui.browser.columns:
1773 col.need_redraw = True
1774
1775
1776class yank(Command):
1777 """:yank [name|dir|path]
1778
1779 Copies the file's name (default), directory or path into both the primary X
1780 selection and the clipboard.
1781 """
1782
1783 modes = {
1784 '': 'basename',
1785 'name_without_extension': 'basename_without_extension',
1786 'name': 'basename',
1787 'dir': 'dirname',
1788 'path': 'path',
1789 }
1790
1791 def execute(self):
1792 import subprocess
1793
1794 def clipboards():
1795 from ranger.ext.get_executables import get_executables
1796 clipboard_managers = {
1797 'xclip': [
1798 ['xclip'],
1799 ['xclip', '-selection', 'clipboard'],
1800 ],
1801 'xsel': [
1802 ['xsel'],
1803 ['xsel', '-b'],
1804 ],
1805 'pbcopy': [
1806 ['pbcopy'],
1807 ],
1808 }
1809 ordered_managers = ['pbcopy', 'xclip', 'xsel']
1810 executables = get_executables()
1811 for manager in ordered_managers:
1812 if manager in executables:
1813 return clipboard_managers[manager]
1814 return []
1815
1816 clipboard_commands = clipboards()
1817
1818 mode = self.modes[self.arg(1)]
1819 selection = self.get_selection_attr(mode)
1820
1821 new_clipboard_contents = "\n".join(selection)
1822 for command in clipboard_commands:
1823 process = subprocess.Popen(command, universal_newlines=True,
1824 stdin=subprocess.PIPE)
1825 process.communicate(input=new_clipboard_contents)
1826
1827 def get_selection_attr(self, attr):
1828 return [getattr(item, attr) for item in
1829 self.fm.thistab.get_selection()]
1830
1831 def tab(self, tabnum):
1832 return (
1833 self.start(1) + mode for mode
1834 in sorted(self.modes.keys())
1835 if mode
1836 )