This is the first push to this repo with my dotfiles
[dotfilesold/.git] / .config / ranger / commands_full.py
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
92 from __future__ import (absolute_import, division, print_function)
93
94 from collections import deque
95 import os
96 import re
97
98 from ranger.api.commands import Command
99
100
101 class 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
118 class 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
128 class 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
280 class 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
294 class 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
333 class 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
428 class 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
481 class 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
513 class 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
526 class 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
572 class 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
591 class 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
607 class 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
622 class 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
634 class 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
645 class 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
702 class 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
745 class 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
769 class 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
787 class 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
813 class 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
835 class 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
844 class 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
864 class 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
883 class 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
899 class 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
941 class 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
975 class 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
1016 class 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
1055 class 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
1135 class 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
1172 class 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
1199 class 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
1216 class copypmap(copymap):
1217     """:copypmap <keys> <newkeys1> [<newkeys2>...]
1218
1219     Copies a "pager" keybinding from <keys> to <newkeys>
1220     """
1221     context = 'pager'
1222
1223
1224 class copycmap(copymap):
1225     """:copycmap <keys> <newkeys1> [<newkeys2>...]
1226
1227     Copies a "console" keybinding from <keys> to <newkeys>
1228     """
1229     context = 'console'
1230
1231
1232 class copytmap(copymap):
1233     """:copycmap <keys> <newkeys1> [<newkeys2>...]
1234
1235     Copies a "taskview" keybinding from <keys> to <newkeys>
1236     """
1237     context = 'taskview'
1238
1239
1240 class 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
1252 class cunmap(unmap):
1253     """:cunmap <keys> [<keys2>, ...]
1254
1255     Remove the given "console" mappings
1256     """
1257     context = 'browser'
1258
1259
1260 class punmap(unmap):
1261     """:punmap <keys> [<keys2>, ...]
1262
1263     Remove the given "pager" mappings
1264     """
1265     context = 'pager'
1266
1267
1268 class tunmap(unmap):
1269     """:tunmap <keys> [<keys2>, ...]
1270
1271     Remove the given "taskview" mappings
1272     """
1273     context = 'taskview'
1274
1275
1276 class 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
1297 class 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
1309 class tmap(map_):
1310     """:tmap <keysequence> <command>
1311
1312     Maps a command to a keysequence in the "taskview" context.
1313     """
1314     context = 'taskview'
1315
1316
1317 class pmap(map_):
1318     """:pmap <keysequence> <command>
1319
1320     Maps a command to a keysequence in the "pager" context.
1321     """
1322     context = 'pager'
1323
1324
1325 class 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
1512 class 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
1528 class 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
1548 class 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
1603 class 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
1617 class 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
1646 class 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
1667 class 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
1691 class 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
1723 class 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
1748 class 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
1776 class 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         )