Python · 62106 bytes Raw Blame History
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 = self.fm.thisdir.path
247 paths = [os.path.relpath(os.path.join(basepath, path), paths_rel)
248 for path in paths]
249 else:
250 paths_rel = ''
251 return paths, paths_rel
252
253 def tab(self, tabnum):
254 from os.path import sep
255
256 start, dest, dest_abs, ends_with_sep = self._tab_args()
257
258 paths, paths_rel = self._tab_paths(dest, dest_abs, ends_with_sep)
259 if paths is None:
260 if self.fm.settings.cd_tab_fuzzy:
261 paths, paths_rel = self._tab_fuzzy(dest, dest_abs)
262 else:
263 paths, paths_rel = self._tab_normal(dest, dest_abs)
264
265 paths.sort()
266
267 if self.fm.settings.cd_bookmarks:
268 paths[0:0] = [
269 os.path.relpath(v.path, paths_rel) if paths_rel else v.path
270 for v in self.fm.bookmarks.dct.values() for path in paths
271 if v.path.startswith(os.path.join(paths_rel, path) + sep)
272 ]
273
274 if not paths:
275 return None
276 if len(paths) == 1:
277 return start + paths[0] + sep
278 return [start + dirname + sep for dirname in paths]
279
280
281 class chain(Command):
282 """:chain <command1>; <command2>; ...
283
284 Calls multiple commands at once, separated by semicolons.
285 """
286 resolve_macros = False
287
288 def execute(self):
289 if not self.rest(1).strip():
290 self.fm.notify('Syntax: chain <command1>; <command2>; ...', bad=True)
291 return
292 for command in [s.strip() for s in self.rest(1).split(";")]:
293 self.fm.execute_console(command)
294
295
296 class shell(Command):
297 escape_macros_for_shell = True
298
299 def execute(self):
300 if self.arg(1) and self.arg(1)[0] == '-':
301 flags = self.arg(1)[1:]
302 command = self.rest(2)
303 else:
304 flags = ''
305 command = self.rest(1)
306
307 if command:
308 self.fm.execute_command(command, flags=flags)
309
310 def tab(self, tabnum):
311 from ranger.ext.get_executables import get_executables
312 if self.arg(1) and self.arg(1)[0] == '-':
313 command = self.rest(2)
314 else:
315 command = self.rest(1)
316 start = self.line[0:len(self.line) - len(command)]
317
318 try:
319 position_of_last_space = command.rindex(" ")
320 except ValueError:
321 return (start + program + ' ' for program
322 in get_executables() if program.startswith(command))
323 if position_of_last_space == len(command) - 1:
324 selection = self.fm.thistab.get_selection()
325 if len(selection) == 1:
326 return self.line + selection[0].shell_escaped_basename + ' '
327 return self.line + '%s '
328
329 before_word, start_of_word = self.line.rsplit(' ', 1)
330 return (before_word + ' ' + file.shell_escaped_basename
331 for file in self.fm.thisdir.files or []
332 if file.shell_escaped_basename.startswith(start_of_word))
333
334
335 class open_with(Command):
336
337 def execute(self):
338 app, flags, mode = self._get_app_flags_mode(self.rest(1))
339 self.fm.execute_file(
340 files=[f for f in self.fm.thistab.get_selection()],
341 app=app,
342 flags=flags,
343 mode=mode)
344
345 def tab(self, tabnum):
346 return self._tab_through_executables()
347
348 def _get_app_flags_mode(self, string): # pylint: disable=too-many-branches,too-many-statements
349 """Extracts the application, flags and mode from a string.
350
351 examples:
352 "mplayer f 1" => ("mplayer", "f", 1)
353 "atool 4" => ("atool", "", 4)
354 "p" => ("", "p", 0)
355 "" => None
356 """
357
358 app = ''
359 flags = ''
360 mode = 0
361 split = string.split()
362
363 if len(split) == 1:
364 part = split[0]
365 if self._is_app(part):
366 app = part
367 elif self._is_flags(part):
368 flags = part
369 elif self._is_mode(part):
370 mode = part
371
372 elif len(split) == 2:
373 part0 = split[0]
374 part1 = split[1]
375
376 if self._is_app(part0):
377 app = part0
378 if self._is_flags(part1):
379 flags = part1
380 elif self._is_mode(part1):
381 mode = part1
382 elif self._is_flags(part0):
383 flags = part0
384 if self._is_mode(part1):
385 mode = part1
386 elif self._is_mode(part0):
387 mode = part0
388 if self._is_flags(part1):
389 flags = part1
390
391 elif len(split) >= 3:
392 part0 = split[0]
393 part1 = split[1]
394 part2 = split[2]
395
396 if self._is_app(part0):
397 app = part0
398 if self._is_flags(part1):
399 flags = part1
400 if self._is_mode(part2):
401 mode = part2
402 elif self._is_mode(part1):
403 mode = part1
404 if self._is_flags(part2):
405 flags = part2
406 elif self._is_flags(part0):
407 flags = part0
408 if self._is_mode(part1):
409 mode = part1
410 elif self._is_mode(part0):
411 mode = part0
412 if self._is_flags(part1):
413 flags = part1
414
415 return app, flags, int(mode)
416
417 def _is_app(self, arg):
418 return not self._is_flags(arg) and not arg.isdigit()
419
420 @staticmethod
421 def _is_flags(arg):
422 from ranger.core.runner import ALLOWED_FLAGS
423 return all(x in ALLOWED_FLAGS for x in arg)
424
425 @staticmethod
426 def _is_mode(arg):
427 return all(x in '0123456789' for x in arg)
428
429
430 class set_(Command):
431 """:set <option name>=<python expression>
432
433 Gives an option a new value.
434
435 Use `:set <option>!` to toggle or cycle it, e.g. `:set flush_input!`
436 """
437 name = 'set' # don't override the builtin set class
438
439 def execute(self):
440 name = self.arg(1)
441 name, value, _, toggle = self.parse_setting_line_v2()
442 if toggle:
443 self.fm.toggle_option(name)
444 else:
445 self.fm.set_option_from_string(name, value)
446
447 def tab(self, tabnum): # pylint: disable=too-many-return-statements
448 from ranger.gui.colorscheme import get_all_colorschemes
449 name, value, name_done = self.parse_setting_line()
450 settings = self.fm.settings
451 if not name:
452 return sorted(self.firstpart + setting for setting in settings)
453 if not value and not name_done:
454 return sorted(self.firstpart + setting for setting in settings
455 if setting.startswith(name))
456 if not value:
457 value_completers = {
458 "colorscheme":
459 # Cycle through colorschemes when name, but no value is specified
460 lambda: sorted(self.firstpart + colorscheme for colorscheme
461 in get_all_colorschemes(self.fm)),
462
463 "column_ratios":
464 lambda: self.firstpart + ",".join(map(str, settings[name])),
465 }
466
467 def default_value_completer():
468 return self.firstpart + str(settings[name])
469
470 return value_completers.get(name, default_value_completer)()
471 if bool in settings.types_of(name):
472 if 'true'.startswith(value.lower()):
473 return self.firstpart + 'True'
474 if 'false'.startswith(value.lower()):
475 return self.firstpart + 'False'
476 # Tab complete colorscheme values if incomplete value is present
477 if name == "colorscheme":
478 return sorted(self.firstpart + colorscheme for colorscheme
479 in get_all_colorschemes(self.fm) if colorscheme.startswith(value))
480 return None
481
482
483 class setlocal(set_):
484 """:setlocal path=<regular expression> <option name>=<python expression>
485
486 Gives an option a new value.
487 """
488 PATH_RE_DQUOTED = re.compile(r'^setlocal\s+path="(.*?)"')
489 PATH_RE_SQUOTED = re.compile(r"^setlocal\s+path='(.*?)'")
490 PATH_RE_UNQUOTED = re.compile(r'^path=(.*?)$')
491
492 def _re_shift(self, match):
493 if not match:
494 return None
495 path = os.path.expanduser(match.group(1))
496 for _ in range(len(path.split())):
497 self.shift()
498 return path
499
500 def execute(self):
501 path = self._re_shift(self.PATH_RE_DQUOTED.match(self.line))
502 if path is None:
503 path = self._re_shift(self.PATH_RE_SQUOTED.match(self.line))
504 if path is None:
505 path = self._re_shift(self.PATH_RE_UNQUOTED.match(self.arg(1)))
506 if path is None and self.fm.thisdir:
507 path = self.fm.thisdir.path
508 if not path:
509 return
510
511 name, value, _ = self.parse_setting_line()
512 self.fm.set_option_from_string(name, value, localpath=path)
513
514
515 class setintag(set_):
516 """:setintag <tag or tags> <option name>=<option value>
517
518 Sets an option for directories that are tagged with a specific tag.
519 """
520
521 def execute(self):
522 tags = self.arg(1)
523 self.shift()
524 name, value, _ = self.parse_setting_line()
525 self.fm.set_option_from_string(name, value, tags=tags)
526
527
528 class default_linemode(Command):
529
530 def execute(self):
531 from ranger.container.fsobject import FileSystemObject
532
533 if len(self.args) < 2:
534 self.fm.notify(
535 "Usage: default_linemode [path=<regexp> | tag=<tag(s)>] <linemode>", bad=True)
536
537 # Extract options like "path=..." or "tag=..." from the command line
538 arg1 = self.arg(1)
539 method = "always"
540 argument = None
541 if arg1.startswith("path="):
542 method = "path"
543 argument = re.compile(arg1[5:])
544 self.shift()
545 elif arg1.startswith("tag="):
546 method = "tag"
547 argument = arg1[4:]
548 self.shift()
549
550 # Extract and validate the line mode from the command line
551 lmode = self.rest(1)
552 if lmode not in FileSystemObject.linemode_dict:
553 self.fm.notify(
554 "Invalid linemode: %s; should be %s" % (
555 lmode, "/".join(FileSystemObject.linemode_dict)),
556 bad=True,
557 )
558
559 # Add the prepared entry to the fm.default_linemodes
560 entry = [method, argument, lmode]
561 self.fm.default_linemodes.appendleft(entry)
562
563 # Redraw the columns
564 if self.fm.ui.browser:
565 for col in self.fm.ui.browser.columns:
566 col.need_redraw = True
567
568 def tab(self, tabnum):
569 return (self.arg(0) + " " + lmode
570 for lmode in self.fm.thisfile.linemode_dict.keys()
571 if lmode.startswith(self.arg(1)))
572
573
574 class quit(Command): # pylint: disable=redefined-builtin
575 """:quit
576
577 Closes the current tab, if there's more than one tab.
578 Otherwise quits if there are no tasks in progress.
579 """
580 def _exit_no_work(self):
581 if self.fm.loader.has_work():
582 self.fm.notify('Not quitting: Tasks in progress: Use `quit!` to force quit')
583 else:
584 self.fm.exit()
585
586 def execute(self):
587 if len(self.fm.tabs) >= 2:
588 self.fm.tab_close()
589 else:
590 self._exit_no_work()
591
592
593 class quit_bang(Command):
594 """:quit!
595
596 Closes the current tab, if there's more than one tab.
597 Otherwise force quits immediately.
598 """
599 name = 'quit!'
600 allow_abbrev = False
601
602 def execute(self):
603 if len(self.fm.tabs) >= 2:
604 self.fm.tab_close()
605 else:
606 self.fm.exit()
607
608
609 class quitall(Command):
610 """:quitall
611
612 Quits if there are no tasks in progress.
613 """
614 def _exit_no_work(self):
615 if self.fm.loader.has_work():
616 self.fm.notify('Not quitting: Tasks in progress: Use `quitall!` to force quit')
617 else:
618 self.fm.exit()
619
620 def execute(self):
621 self._exit_no_work()
622
623
624 class quitall_bang(Command):
625 """:quitall!
626
627 Force quits immediately.
628 """
629 name = 'quitall!'
630 allow_abbrev = False
631
632 def execute(self):
633 self.fm.exit()
634
635
636 class terminal(Command):
637 """:terminal
638
639 Spawns an "x-terminal-emulator" starting in the current directory.
640 """
641
642 def execute(self):
643 from ranger.ext.get_executables import get_term
644 self.fm.run(get_term(), flags='f')
645
646
647 class delete(Command):
648 """:delete
649
650 Tries to delete the selection or the files passed in arguments (if any).
651 The arguments use a shell-like escaping.
652
653 "Selection" is defined as all the "marked files" (by default, you
654 can mark files with space or v). If there are no marked files,
655 use the "current file" (where the cursor is)
656
657 When attempting to delete non-empty directories or multiple
658 marked files, it will require a confirmation.
659 """
660
661 allow_abbrev = False
662 escape_macros_for_shell = True
663
664 def execute(self):
665 import shlex
666 from functools import partial
667
668 def is_directory_with_files(path):
669 return os.path.isdir(path) and not os.path.islink(path) and len(os.listdir(path)) > 0
670
671 if self.rest(1):
672 files = shlex.split(self.rest(1))
673 many_files = (len(files) > 1 or is_directory_with_files(files[0]))
674 else:
675 cwd = self.fm.thisdir
676 tfile = self.fm.thisfile
677 if not cwd or not tfile:
678 self.fm.notify("Error: no file selected for deletion!", bad=True)
679 return
680
681 # relative_path used for a user-friendly output in the confirmation.
682 files = [f.relative_path for f in self.fm.thistab.get_selection()]
683 many_files = (cwd.marked_items or is_directory_with_files(tfile.path))
684
685 confirm = self.fm.settings.confirm_on_delete
686 if confirm != 'never' and (confirm != 'multiple' or many_files):
687 self.fm.ui.console.ask(
688 "Confirm deletion of: %s (y/N)" % ', '.join(files),
689 partial(self._question_callback, files),
690 ('n', 'N', 'y', 'Y'),
691 )
692 else:
693 # no need for a confirmation, just delete
694 self.fm.delete(files)
695
696 def tab(self, tabnum):
697 return self._tab_directory_content()
698
699 def _question_callback(self, files, answer):
700 if answer == 'y' or answer == 'Y':
701 self.fm.delete(files)
702
703
704 class trash(Command):
705 """:trash
706
707 Tries to move the selection or the files passed in arguments (if any) to
708 the trash, using rifle rules with label "trash".
709 The arguments use a shell-like escaping.
710
711 "Selection" is defined as all the "marked files" (by default, you
712 can mark files with space or v). If there are no marked files,
713 use the "current file" (where the cursor is)
714
715 When attempting to trash non-empty directories or multiple
716 marked files, it will require a confirmation.
717 """
718
719 allow_abbrev = False
720 escape_macros_for_shell = True
721
722 def execute(self):
723 import shlex
724 from functools import partial
725
726 def is_directory_with_files(path):
727 return os.path.isdir(path) and not os.path.islink(path) and len(os.listdir(path)) > 0
728
729 if self.rest(1):
730 files = shlex.split(self.rest(1))
731 many_files = (len(files) > 1 or is_directory_with_files(files[0]))
732 else:
733 cwd = self.fm.thisdir
734 tfile = self.fm.thisfile
735 if not cwd or not tfile:
736 self.fm.notify("Error: no file selected for deletion!", bad=True)
737 return
738
739 # relative_path used for a user-friendly output in the confirmation.
740 files = [f.relative_path for f in self.fm.thistab.get_selection()]
741 many_files = (cwd.marked_items or is_directory_with_files(tfile.path))
742
743 confirm = self.fm.settings.confirm_on_delete
744 if confirm != 'never' and (confirm != 'multiple' or many_files):
745 self.fm.ui.console.ask(
746 "Confirm deletion of: %s (y/N)" % ', '.join(files),
747 partial(self._question_callback, files),
748 ('n', 'N', 'y', 'Y'),
749 )
750 else:
751 # no need for a confirmation, just delete
752 self.fm.execute_file(files, label='trash')
753
754 def tab(self, tabnum):
755 return self._tab_directory_content()
756
757 def _question_callback(self, files, answer):
758 if answer == 'y' or answer == 'Y':
759 self.fm.execute_file(files, label='trash')
760
761
762 class jump_non(Command):
763 """:jump_non [-FLAGS...]
764
765 Jumps to first non-directory if highlighted file is a directory and vice versa.
766
767 Flags:
768 -r Jump in reverse order
769 -w Wrap around if reaching end of filelist
770 """
771 def __init__(self, *args, **kwargs):
772 super(jump_non, self).__init__(*args, **kwargs)
773
774 flags, _ = self.parse_flags()
775 self._flag_reverse = 'r' in flags
776 self._flag_wrap = 'w' in flags
777
778 @staticmethod
779 def _non(fobj, is_directory):
780 return fobj.is_directory if not is_directory else not fobj.is_directory
781
782 def execute(self):
783 tfile = self.fm.thisfile
784 passed = False
785 found_before = None
786 found_after = None
787 for fobj in self.fm.thisdir.files[::-1] if self._flag_reverse else self.fm.thisdir.files:
788 if fobj.path == tfile.path:
789 passed = True
790 continue
791
792 if passed:
793 if self._non(fobj, tfile.is_directory):
794 found_after = fobj.path
795 break
796 elif not found_before and self._non(fobj, tfile.is_directory):
797 found_before = fobj.path
798
799 if found_after:
800 self.fm.select_file(found_after)
801 elif self._flag_wrap and found_before:
802 self.fm.select_file(found_before)
803
804
805 class mark_tag(Command):
806 """:mark_tag [<tags>]
807
808 Mark all tags that are tagged with either of the given tags.
809 When leaving out the tag argument, all tagged files are marked.
810 """
811 do_mark = True
812
813 def execute(self):
814 cwd = self.fm.thisdir
815 tags = self.rest(1).replace(" ", "")
816 if not self.fm.tags or not cwd.files:
817 return
818 for fileobj in cwd.files:
819 try:
820 tag = self.fm.tags.tags[fileobj.realpath]
821 except KeyError:
822 continue
823 if not tags or tag in tags:
824 cwd.mark_item(fileobj, val=self.do_mark)
825 self.fm.ui.status.need_redraw = True
826 self.fm.ui.need_redraw = True
827
828
829 class console(Command):
830 """:console <command>
831
832 Open the console with the given command.
833 """
834
835 def execute(self):
836 position = None
837 if self.arg(1)[0:2] == '-p':
838 try:
839 position = int(self.arg(1)[2:])
840 except ValueError:
841 pass
842 else:
843 self.shift()
844 self.fm.open_console(self.rest(1), position=position)
845
846
847 class load_copy_buffer(Command):
848 """:load_copy_buffer
849
850 Load the copy buffer from datadir/copy_buffer
851 """
852 copy_buffer_filename = 'copy_buffer'
853
854 def execute(self):
855 import sys
856 from ranger.container.file import File
857 from os.path import exists
858 fname = self.fm.datapath(self.copy_buffer_filename)
859 unreadable = IOError if sys.version_info[0] < 3 else OSError
860 try:
861 fobj = open(fname, 'r')
862 except unreadable:
863 return self.fm.notify(
864 "Cannot open %s" % (fname or self.copy_buffer_filename), bad=True)
865
866 self.fm.copy_buffer = set(File(g)
867 for g in fobj.read().split("\n") if exists(g))
868 fobj.close()
869 self.fm.ui.redraw_main_column()
870 return None
871
872
873 class save_copy_buffer(Command):
874 """:save_copy_buffer
875
876 Save the copy buffer to datadir/copy_buffer
877 """
878 copy_buffer_filename = 'copy_buffer'
879
880 def execute(self):
881 import sys
882 fname = None
883 fname = self.fm.datapath(self.copy_buffer_filename)
884 unwritable = IOError if sys.version_info[0] < 3 else OSError
885 try:
886 fobj = open(fname, 'w')
887 except unwritable:
888 return self.fm.notify("Cannot open %s" %
889 (fname or self.copy_buffer_filename), bad=True)
890 fobj.write("\n".join(fobj.path for fobj in self.fm.copy_buffer))
891 fobj.close()
892 return None
893
894
895 class unmark_tag(mark_tag):
896 """:unmark_tag [<tags>]
897
898 Unmark all tags that are tagged with either of the given tags.
899 When leaving out the tag argument, all tagged files are unmarked.
900 """
901 do_mark = False
902
903
904 class mkdir(Command):
905 """:mkdir <dirname>
906
907 Creates a directory with the name <dirname>.
908 """
909
910 def execute(self):
911 from os.path import join, expanduser, lexists
912 from os import makedirs
913
914 dirname = join(self.fm.thisdir.path, expanduser(self.rest(1)))
915 if not lexists(dirname):
916 makedirs(dirname)
917 else:
918 self.fm.notify("file/directory exists!", bad=True)
919
920 def tab(self, tabnum):
921 return self._tab_directory_content()
922
923
924 class touch(Command):
925 """:touch <fname>
926
927 Creates a file with the name <fname>.
928 """
929
930 def execute(self):
931 from os.path import join, expanduser, lexists
932
933 fname = join(self.fm.thisdir.path, expanduser(self.rest(1)))
934 if not lexists(fname):
935 open(fname, 'a').close()
936 else:
937 self.fm.notify("file/directory exists!", bad=True)
938
939 def tab(self, tabnum):
940 return self._tab_directory_content()
941
942
943 class edit(Command):
944 """:edit <filename>
945
946 Opens the specified file in vim
947 """
948
949 def execute(self):
950 if not self.arg(1):
951 self.fm.edit_file(self.fm.thisfile.path)
952 else:
953 self.fm.edit_file(self.rest(1))
954
955 def tab(self, tabnum):
956 return self._tab_directory_content()
957
958
959 class eval_(Command):
960 """:eval [-q] <python code>
961
962 Evaluates the python code.
963 `fm' is a reference to the FM instance.
964 To display text, use the function `p'.
965
966 Examples:
967 :eval fm
968 :eval len(fm.directories)
969 :eval p("Hello World!")
970 """
971 name = 'eval'
972 resolve_macros = False
973
974 def execute(self):
975 # The import is needed so eval() can access the ranger module
976 import ranger # NOQA pylint: disable=unused-import,unused-variable
977 if self.arg(1) == '-q':
978 code = self.rest(2)
979 quiet = True
980 else:
981 code = self.rest(1)
982 quiet = False
983 global cmd, fm, p, quantifier # pylint: disable=invalid-name,global-variable-undefined
984 fm = self.fm
985 cmd = self.fm.execute_console
986 p = fm.notify
987 quantifier = self.quantifier
988 try:
989 try:
990 result = eval(code) # pylint: disable=eval-used
991 except SyntaxError:
992 exec(code) # pylint: disable=exec-used
993 else:
994 if result and not quiet:
995 p(result)
996 except Exception as err: # pylint: disable=broad-except
997 fm.notify("The error `%s` was caused by evaluating the "
998 "following code: `%s`" % (err, code), bad=True)
999
1000
1001 class rename(Command):
1002 """:rename <newname>
1003
1004 Changes the name of the currently highlighted file to <newname>
1005 """
1006
1007 def execute(self):
1008 from ranger.container.file import File
1009 from os import access
1010
1011 new_name = self.rest(1)
1012
1013 if not new_name:
1014 return self.fm.notify('Syntax: rename <newname>', bad=True)
1015
1016 if new_name == self.fm.thisfile.relative_path:
1017 return None
1018
1019 if access(new_name, os.F_OK):
1020 return self.fm.notify("Can't rename: file already exists!", bad=True)
1021
1022 if self.fm.rename(self.fm.thisfile, new_name):
1023 file_new = File(new_name)
1024 self.fm.bookmarks.update_path(self.fm.thisfile.path, file_new)
1025 self.fm.tags.update_path(self.fm.thisfile.path, file_new.path)
1026 self.fm.thisdir.pointed_obj = file_new
1027 self.fm.thisfile = file_new
1028
1029 return None
1030
1031 def tab(self, tabnum):
1032 return self._tab_directory_content()
1033
1034
1035 class rename_append(Command):
1036 """:rename_append [-FLAGS...]
1037
1038 Opens the console with ":rename <current file>" with the cursor positioned
1039 before the file extension.
1040
1041 Flags:
1042 -a Position before all extensions
1043 -r Remove everything before extensions
1044 """
1045 def __init__(self, *args, **kwargs):
1046 super(rename_append, self).__init__(*args, **kwargs)
1047
1048 flags, _ = self.parse_flags()
1049 self._flag_ext_all = 'a' in flags
1050 self._flag_remove = 'r' in flags
1051
1052 def execute(self):
1053 from ranger import MACRO_DELIMITER, MACRO_DELIMITER_ESC
1054
1055 tfile = self.fm.thisfile
1056 relpath = tfile.relative_path.replace(MACRO_DELIMITER, MACRO_DELIMITER_ESC)
1057 basename = tfile.basename.replace(MACRO_DELIMITER, MACRO_DELIMITER_ESC)
1058
1059 if basename.find('.') <= 0 or os.path.isdir(relpath):
1060 self.fm.open_console('rename ' + relpath)
1061 return
1062
1063 if self._flag_ext_all:
1064 pos_ext = re.search(r'[^.]+', basename).end(0)
1065 else:
1066 pos_ext = basename.rindex('.')
1067 pos = len(relpath) - len(basename) + pos_ext
1068
1069 if self._flag_remove:
1070 relpath = relpath[:-len(basename)] + basename[pos_ext:]
1071 pos -= pos_ext
1072
1073 self.fm.open_console('rename ' + relpath, position=(7 + pos))
1074
1075
1076 class chmod(Command):
1077 """:chmod <octal number>
1078
1079 Sets the permissions of the selection to the octal number.
1080
1081 The octal number is between 0 and 777. The digits specify the
1082 permissions for the user, the group and others.
1083
1084 A 1 permits execution, a 2 permits writing, a 4 permits reading.
1085 Add those numbers to combine them. So a 7 permits everything.
1086 """
1087
1088 def execute(self):
1089 mode_str = self.rest(1)
1090 if not mode_str:
1091 if self.quantifier is None:
1092 self.fm.notify("Syntax: chmod <octal number> "
1093 "or specify a quantifier", bad=True)
1094 return
1095 mode_str = str(self.quantifier)
1096
1097 try:
1098 mode = int(mode_str, 8)
1099 if mode < 0 or mode > 0o777:
1100 raise ValueError
1101 except ValueError:
1102 self.fm.notify("Need an octal number between 0 and 777!", bad=True)
1103 return
1104
1105 for fobj in self.fm.thistab.get_selection():
1106 try:
1107 os.chmod(fobj.path, mode)
1108 except OSError as ex:
1109 self.fm.notify(ex)
1110
1111 # reloading directory. maybe its better to reload the selected
1112 # files only.
1113 self.fm.thisdir.content_outdated = True
1114
1115
1116 class bulkrename(Command):
1117 """:bulkrename
1118
1119 This command opens a list of selected files in an external editor.
1120 After you edit and save the file, it will generate a shell script
1121 which does bulk renaming according to the changes you did in the file.
1122
1123 This shell script is opened in an editor for you to review.
1124 After you close it, it will be executed.
1125 """
1126
1127 def execute(self):
1128 # pylint: disable=too-many-locals,too-many-statements,too-many-branches
1129 import sys
1130 import tempfile
1131 from ranger.container.file import File
1132 from ranger.ext.shell_escape import shell_escape as esc
1133 py3 = sys.version_info[0] >= 3
1134
1135 # Create and edit the file list
1136 filenames = [f.relative_path for f in self.fm.thistab.get_selection()]
1137 with tempfile.NamedTemporaryFile(delete=False) as listfile:
1138 listpath = listfile.name
1139 if py3:
1140 listfile.write("\n".join(filenames).encode(
1141 encoding="utf-8", errors="surrogateescape"))
1142 else:
1143 listfile.write("\n".join(filenames))
1144 self.fm.execute_file([File(listpath)], app='editor')
1145 with (open(listpath, 'r', encoding="utf-8", errors="surrogateescape") if
1146 py3 else open(listpath, 'r')) as listfile:
1147 new_filenames = listfile.read().split("\n")
1148 os.unlink(listpath)
1149 if all(a == b for a, b in zip(filenames, new_filenames)):
1150 self.fm.notify("No renaming to be done!")
1151 return
1152
1153 # Generate script
1154 with tempfile.NamedTemporaryFile() as cmdfile:
1155 script_lines = []
1156 script_lines.append("# This file will be executed when you close"
1157 " the editor.")
1158 script_lines.append("# Please double-check everything, clear the"
1159 " file to abort.")
1160 new_dirs = []
1161 for old, new in zip(filenames, new_filenames):
1162 if old != new:
1163 basepath, _ = os.path.split(new)
1164 if (basepath and basepath not in new_dirs
1165 and not os.path.isdir(basepath)):
1166 script_lines.append("mkdir -vp -- {dir}".format(
1167 dir=esc(basepath)))
1168 new_dirs.append(basepath)
1169 script_lines.append("mv -vi -- {old} {new}".format(
1170 old=esc(old), new=esc(new)))
1171 # Make sure not to forget the ending newline
1172 script_content = "\n".join(script_lines) + "\n"
1173 if py3:
1174 cmdfile.write(script_content.encode(encoding="utf-8",
1175 errors="surrogateescape"))
1176 else:
1177 cmdfile.write(script_content)
1178 cmdfile.flush()
1179
1180 # Open the script and let the user review it, then check if the
1181 # script was modified by the user
1182 self.fm.execute_file([File(cmdfile.name)], app='editor')
1183 cmdfile.seek(0)
1184 script_was_edited = (script_content != cmdfile.read())
1185
1186 # Do the renaming
1187 self.fm.run(['/bin/sh', cmdfile.name], flags='w')
1188
1189 # Retag the files, but only if the script wasn't changed during review,
1190 # because only then we know which are the source and destination files.
1191 if not script_was_edited:
1192 tags_changed = False
1193 for old, new in zip(filenames, new_filenames):
1194 if old != new:
1195 oldpath = self.fm.thisdir.path + '/' + old
1196 newpath = self.fm.thisdir.path + '/' + new
1197 if oldpath in self.fm.tags:
1198 old_tag = self.fm.tags.tags[oldpath]
1199 self.fm.tags.remove(oldpath)
1200 self.fm.tags.tags[newpath] = old_tag
1201 tags_changed = True
1202 if tags_changed:
1203 self.fm.tags.dump()
1204 else:
1205 fm.notify("files have not been retagged")
1206
1207
1208 class relink(Command):
1209 """:relink <newpath>
1210
1211 Changes the linked path of the currently highlighted symlink to <newpath>
1212 """
1213
1214 def execute(self):
1215 new_path = self.rest(1)
1216 tfile = self.fm.thisfile
1217
1218 if not new_path:
1219 return self.fm.notify('Syntax: relink <newpath>', bad=True)
1220
1221 if not tfile.is_link:
1222 return self.fm.notify('%s is not a symlink!' % tfile.relative_path, bad=True)
1223
1224 if new_path == os.readlink(tfile.path):
1225 return None
1226
1227 try:
1228 os.remove(tfile.path)
1229 os.symlink(new_path, tfile.path)
1230 except OSError as err:
1231 self.fm.notify(err)
1232
1233 self.fm.reset()
1234 self.fm.thisdir.pointed_obj = tfile
1235 self.fm.thisfile = tfile
1236
1237 return None
1238
1239 def tab(self, tabnum):
1240 if not self.rest(1):
1241 return self.line + os.readlink(self.fm.thisfile.path)
1242 return self._tab_directory_content()
1243
1244
1245 class help_(Command):
1246 """:help
1247
1248 Display ranger's manual page.
1249 """
1250 name = 'help'
1251
1252 def execute(self):
1253 def callback(answer):
1254 if answer == "q":
1255 return
1256 elif answer == "m":
1257 self.fm.display_help()
1258 elif answer == "c":
1259 self.fm.dump_commands()
1260 elif answer == "k":
1261 self.fm.dump_keybindings()
1262 elif answer == "s":
1263 self.fm.dump_settings()
1264
1265 self.fm.ui.console.ask(
1266 "View [m]an page, [k]ey bindings, [c]ommands or [s]ettings? (press q to abort)",
1267 callback,
1268 list("mqkcs")
1269 )
1270
1271
1272 class copymap(Command):
1273 """:copymap <keys> <newkeys1> [<newkeys2>...]
1274
1275 Copies a "browser" keybinding from <keys> to <newkeys>
1276 """
1277 context = 'browser'
1278
1279 def execute(self):
1280 if not self.arg(1) or not self.arg(2):
1281 return self.fm.notify("Not enough arguments", bad=True)
1282
1283 for arg in self.args[2:]:
1284 self.fm.ui.keymaps.copy(self.context, self.arg(1), arg)
1285
1286 return None
1287
1288
1289 class copypmap(copymap):
1290 """:copypmap <keys> <newkeys1> [<newkeys2>...]
1291
1292 Copies a "pager" keybinding from <keys> to <newkeys>
1293 """
1294 context = 'pager'
1295
1296
1297 class copycmap(copymap):
1298 """:copycmap <keys> <newkeys1> [<newkeys2>...]
1299
1300 Copies a "console" keybinding from <keys> to <newkeys>
1301 """
1302 context = 'console'
1303
1304
1305 class copytmap(copymap):
1306 """:copytmap <keys> <newkeys1> [<newkeys2>...]
1307
1308 Copies a "taskview" keybinding from <keys> to <newkeys>
1309 """
1310 context = 'taskview'
1311
1312
1313 class unmap(Command):
1314 """:unmap <keys> [<keys2>, ...]
1315
1316 Remove the given "browser" mappings
1317 """
1318 context = 'browser'
1319
1320 def execute(self):
1321 for arg in self.args[1:]:
1322 self.fm.ui.keymaps.unbind(self.context, arg)
1323
1324
1325 class uncmap(unmap):
1326 """:uncmap <keys> [<keys2>, ...]
1327
1328 Remove the given "console" mappings
1329 """
1330 context = 'console'
1331
1332
1333 class cunmap(uncmap):
1334 """:cunmap <keys> [<keys2>, ...]
1335
1336 Remove the given "console" mappings
1337
1338 DEPRECATED in favor of uncmap.
1339 """
1340
1341 def execute(self):
1342 self.fm.notify("cunmap is deprecated in favor of uncmap!")
1343 super(cunmap, self).execute()
1344
1345
1346 class unpmap(unmap):
1347 """:unpmap <keys> [<keys2>, ...]
1348
1349 Remove the given "pager" mappings
1350 """
1351 context = 'pager'
1352
1353
1354 class punmap(unpmap):
1355 """:punmap <keys> [<keys2>, ...]
1356
1357 Remove the given "pager" mappings
1358
1359 DEPRECATED in favor of unpmap.
1360 """
1361
1362 def execute(self):
1363 self.fm.notify("punmap is deprecated in favor of unpmap!")
1364 super(punmap, self).execute()
1365
1366
1367 class untmap(unmap):
1368 """:untmap <keys> [<keys2>, ...]
1369
1370 Remove the given "taskview" mappings
1371 """
1372 context = 'taskview'
1373
1374
1375 class tunmap(untmap):
1376 """:tunmap <keys> [<keys2>, ...]
1377
1378 Remove the given "taskview" mappings
1379
1380 DEPRECATED in favor of untmap.
1381 """
1382
1383 def execute(self):
1384 self.fm.notify("tunmap is deprecated in favor of untmap!")
1385 super(tunmap, self).execute()
1386
1387
1388 class map_(Command):
1389 """:map <keysequence> <command>
1390
1391 Maps a command to a keysequence in the "browser" context.
1392
1393 Example:
1394 map j move down
1395 map J move down 10
1396 """
1397 name = 'map'
1398 context = 'browser'
1399 resolve_macros = False
1400
1401 def execute(self):
1402 if not self.arg(1) or not self.arg(2):
1403 self.fm.notify("Syntax: {0} <keysequence> <command>".format(self.get_name()), bad=True)
1404 return
1405
1406 self.fm.ui.keymaps.bind(self.context, self.arg(1), self.rest(2))
1407
1408
1409 class cmap(map_):
1410 """:cmap <keysequence> <command>
1411
1412 Maps a command to a keysequence in the "console" context.
1413
1414 Example:
1415 cmap <ESC> console_close
1416 cmap <C-x> console_type test
1417 """
1418 context = 'console'
1419
1420
1421 class tmap(map_):
1422 """:tmap <keysequence> <command>
1423
1424 Maps a command to a keysequence in the "taskview" context.
1425 """
1426 context = 'taskview'
1427
1428
1429 class pmap(map_):
1430 """:pmap <keysequence> <command>
1431
1432 Maps a command to a keysequence in the "pager" context.
1433 """
1434 context = 'pager'
1435
1436
1437 class scout(Command):
1438 """:scout [-FLAGS...] <pattern>
1439
1440 Swiss army knife command for searching, traveling and filtering files.
1441
1442 Flags:
1443 -a Automatically open a file on unambiguous match
1444 -e Open the selected file when pressing enter
1445 -f Filter files that match the current search pattern
1446 -g Interpret pattern as a glob pattern
1447 -i Ignore the letter case of the files
1448 -k Keep the console open when changing a directory with the command
1449 -l Letter skipping; e.g. allow "rdme" to match the file "readme"
1450 -m Mark the matching files after pressing enter
1451 -M Unmark the matching files after pressing enter
1452 -p Permanent filter: hide non-matching files after pressing enter
1453 -r Interpret pattern as a regular expression pattern
1454 -s Smart case; like -i unless pattern contains upper case letters
1455 -t Apply filter and search pattern as you type
1456 -v Inverts the match
1457
1458 Multiple flags can be combined. For example, ":scout -gpt" would create
1459 a :filter-like command using globbing.
1460 """
1461 # pylint: disable=bad-whitespace
1462 AUTO_OPEN = 'a'
1463 OPEN_ON_ENTER = 'e'
1464 FILTER = 'f'
1465 SM_GLOB = 'g'
1466 IGNORE_CASE = 'i'
1467 KEEP_OPEN = 'k'
1468 SM_LETTERSKIP = 'l'
1469 MARK = 'm'
1470 UNMARK = 'M'
1471 PERM_FILTER = 'p'
1472 SM_REGEX = 'r'
1473 SMART_CASE = 's'
1474 AS_YOU_TYPE = 't'
1475 INVERT = 'v'
1476 # pylint: enable=bad-whitespace
1477
1478 def __init__(self, *args, **kwargs):
1479 super(scout, self).__init__(*args, **kwargs)
1480 self._regex = None
1481 self.flags, self.pattern = self.parse_flags()
1482
1483 def execute(self): # pylint: disable=too-many-branches
1484 thisdir = self.fm.thisdir
1485 flags = self.flags
1486 pattern = self.pattern
1487 regex = self._build_regex()
1488 count = self._count(move=True)
1489
1490 self.fm.thistab.last_search = regex
1491 self.fm.set_search_method(order="search")
1492
1493 if (self.MARK in flags or self.UNMARK in flags) and thisdir.files:
1494 value = flags.find(self.MARK) > flags.find(self.UNMARK)
1495 if self.FILTER in flags:
1496 for fobj in thisdir.files:
1497 thisdir.mark_item(fobj, value)
1498 else:
1499 for fobj in thisdir.files:
1500 if regex.search(fobj.relative_path):
1501 thisdir.mark_item(fobj, value)
1502
1503 if self.PERM_FILTER in flags:
1504 thisdir.filter = regex if pattern else None
1505
1506 # clean up:
1507 self.cancel()
1508
1509 if self.OPEN_ON_ENTER in flags or \
1510 (self.AUTO_OPEN in flags and count == 1):
1511 if pattern == '..':
1512 self.fm.cd(pattern)
1513 else:
1514 self.fm.move(right=1)
1515 if self.quickly_executed:
1516 self.fm.block_input(0.5)
1517
1518 if self.KEEP_OPEN in flags and thisdir != self.fm.thisdir:
1519 # reopen the console:
1520 if not pattern:
1521 self.fm.open_console(self.line)
1522 else:
1523 self.fm.open_console(self.line[0:-len(pattern)])
1524
1525 if self.quickly_executed and thisdir != self.fm.thisdir and pattern != "..":
1526 self.fm.block_input(0.5)
1527
1528 def cancel(self):
1529 self.fm.thisdir.temporary_filter = None
1530 self.fm.thisdir.refilter()
1531
1532 def quick(self):
1533 asyoutype = self.AS_YOU_TYPE in self.flags
1534 if self.FILTER in self.flags:
1535 self.fm.thisdir.temporary_filter = self._build_regex()
1536 if self.PERM_FILTER in self.flags and asyoutype:
1537 self.fm.thisdir.filter = self._build_regex()
1538 if self.FILTER in self.flags or self.PERM_FILTER in self.flags:
1539 self.fm.thisdir.refilter()
1540 if self._count(move=asyoutype) == 1 and self.AUTO_OPEN in self.flags:
1541 return True
1542 return False
1543
1544 def tab(self, tabnum):
1545 self._count(move=True, offset=tabnum)
1546
1547 def _build_regex(self):
1548 if self._regex is not None:
1549 return self._regex
1550
1551 frmat = "%s"
1552 flags = self.flags
1553 pattern = self.pattern
1554
1555 if pattern == ".":
1556 return re.compile("")
1557
1558 # Handle carets at start and dollar signs at end separately
1559 if pattern.startswith('^'):
1560 pattern = pattern[1:]
1561 frmat = "^" + frmat
1562 if pattern.endswith('$'):
1563 pattern = pattern[:-1]
1564 frmat += "$"
1565
1566 # Apply one of the search methods
1567 if self.SM_REGEX in flags:
1568 regex = pattern
1569 elif self.SM_GLOB in flags:
1570 regex = re.escape(pattern).replace("\\*", ".*").replace("\\?", ".")
1571 elif self.SM_LETTERSKIP in flags:
1572 regex = ".*".join(re.escape(c) for c in pattern)
1573 else:
1574 regex = re.escape(pattern)
1575
1576 regex = frmat % regex
1577
1578 # Invert regular expression if necessary
1579 if self.INVERT in flags:
1580 regex = "^(?:(?!%s).)*$" % regex
1581
1582 # Compile Regular Expression
1583 # pylint: disable=no-member
1584 options = re.UNICODE
1585 if self.IGNORE_CASE in flags or self.SMART_CASE in flags and \
1586 pattern.islower():
1587 options |= re.IGNORECASE
1588 # pylint: enable=no-member
1589 try:
1590 self._regex = re.compile(regex, options)
1591 except re.error:
1592 self._regex = re.compile("")
1593 return self._regex
1594
1595 def _count(self, move=False, offset=0):
1596 count = 0
1597 cwd = self.fm.thisdir
1598 pattern = self.pattern
1599
1600 if not pattern or not cwd.files:
1601 return 0
1602 if pattern == '.':
1603 return 0
1604 if pattern == '..':
1605 return 1
1606
1607 deq = deque(cwd.files)
1608 deq.rotate(-cwd.pointer - offset)
1609 i = offset
1610 regex = self._build_regex()
1611 for fsobj in deq:
1612 if regex.search(fsobj.relative_path):
1613 count += 1
1614 if move and count == 1:
1615 cwd.move(to=(cwd.pointer + i) % len(cwd.files))
1616 self.fm.thisfile = cwd.pointed_obj
1617 if count > 1:
1618 return count
1619 i += 1
1620
1621 return count == 1
1622
1623
1624 class narrow(Command):
1625 """
1626 :narrow
1627
1628 Show only the files selected right now. If no files are selected,
1629 disable narrowing.
1630 """
1631 def execute(self):
1632 if self.fm.thisdir.marked_items:
1633 selection = [f.basename for f in self.fm.thistab.get_selection()]
1634 self.fm.thisdir.narrow_filter = selection
1635 else:
1636 self.fm.thisdir.narrow_filter = None
1637 self.fm.thisdir.refilter()
1638
1639
1640 class filter_inode_type(Command):
1641 """
1642 :filter_inode_type [dfl]
1643
1644 Displays only the files of specified inode type. Parameters
1645 can be combined.
1646
1647 d display directories
1648 f display files
1649 l display links
1650 """
1651
1652 def execute(self):
1653 if not self.arg(1):
1654 self.fm.thisdir.inode_type_filter = ""
1655 else:
1656 self.fm.thisdir.inode_type_filter = self.arg(1)
1657 self.fm.thisdir.refilter()
1658
1659
1660 class filter_stack(Command):
1661 """
1662 :filter_stack ...
1663
1664 Manages the filter stack.
1665
1666 filter_stack add FILTER_TYPE ARGS...
1667 filter_stack pop
1668 filter_stack decompose
1669 filter_stack rotate [N=1]
1670 filter_stack clear
1671 filter_stack show
1672 """
1673 def execute(self):
1674 from ranger.core.filter_stack import SIMPLE_FILTERS, FILTER_COMBINATORS
1675
1676 subcommand = self.arg(1)
1677
1678 if subcommand == "add":
1679 try:
1680 self.fm.thisdir.filter_stack.append(
1681 SIMPLE_FILTERS[self.arg(2)](self.rest(3))
1682 )
1683 except KeyError:
1684 FILTER_COMBINATORS[self.arg(2)](self.fm.thisdir.filter_stack)
1685 elif subcommand == "pop":
1686 self.fm.thisdir.filter_stack.pop()
1687 elif subcommand == "decompose":
1688 inner_filters = self.fm.thisdir.filter_stack.pop().decompose()
1689 if inner_filters:
1690 self.fm.thisdir.filter_stack.extend(inner_filters)
1691 elif subcommand == "clear":
1692 self.fm.thisdir.filter_stack = []
1693 elif subcommand == "rotate":
1694 rotate_by = int(self.arg(2) or self.quantifier or 1)
1695 self.fm.thisdir.filter_stack = (
1696 self.fm.thisdir.filter_stack[-rotate_by:]
1697 + self.fm.thisdir.filter_stack[:-rotate_by]
1698 )
1699 elif subcommand == "show":
1700 stack = list(map(str, self.fm.thisdir.filter_stack))
1701 pager = self.fm.ui.open_pager()
1702 pager.set_source(["Filter stack: "] + stack)
1703 pager.move(to=100, percentage=True)
1704 return
1705 else:
1706 self.fm.notify(
1707 "Unknown subcommand: {}".format(subcommand),
1708 bad=True
1709 )
1710 return
1711
1712 self.fm.thisdir.refilter()
1713
1714
1715 class grep(Command):
1716 """:grep <string>
1717
1718 Looks for a string in all marked files or directories
1719 """
1720
1721 def execute(self):
1722 if self.rest(1):
1723 action = ['grep', '--line-number']
1724 action.extend(['-e', self.rest(1), '-r'])
1725 action.extend(f.path for f in self.fm.thistab.get_selection())
1726 self.fm.execute_command(action, flags='p')
1727
1728
1729 class flat(Command):
1730 """
1731 :flat <level>
1732
1733 Flattens the directory view up to the specified level.
1734
1735 -1 fully flattened
1736 0 remove flattened view
1737 """
1738
1739 def execute(self):
1740 try:
1741 level_str = self.rest(1)
1742 level = int(level_str)
1743 except ValueError:
1744 level = self.quantifier
1745 if level is None:
1746 self.fm.notify("Syntax: flat <level>", bad=True)
1747 return
1748 if level < -1:
1749 self.fm.notify("Need an integer number (-1, 0, 1, ...)", bad=True)
1750 self.fm.thisdir.unload()
1751 self.fm.thisdir.flat = level
1752 self.fm.thisdir.load_content()
1753
1754
1755 class reset_previews(Command):
1756 """:reset_previews
1757
1758 Reset the file previews.
1759 """
1760 def execute(self):
1761 self.fm.previews = {}
1762 self.fm.ui.need_redraw = True
1763
1764
1765 # Version control commands
1766 # --------------------------------
1767
1768
1769 class stage(Command):
1770 """
1771 :stage
1772
1773 Stage selected files for the corresponding version control system
1774 """
1775
1776 def execute(self):
1777 from ranger.ext.vcs import VcsError
1778
1779 if self.fm.thisdir.vcs and self.fm.thisdir.vcs.track:
1780 filelist = [f.path for f in self.fm.thistab.get_selection()]
1781 try:
1782 self.fm.thisdir.vcs.action_add(filelist)
1783 except VcsError as ex:
1784 self.fm.notify('Unable to stage files: {0}'.format(ex))
1785 self.fm.ui.vcsthread.process(self.fm.thisdir)
1786 else:
1787 self.fm.notify('Unable to stage files: Not in repository')
1788
1789
1790 class unstage(Command):
1791 """
1792 :unstage
1793
1794 Unstage selected files for the corresponding version control system
1795 """
1796
1797 def execute(self):
1798 from ranger.ext.vcs import VcsError
1799
1800 if self.fm.thisdir.vcs and self.fm.thisdir.vcs.track:
1801 filelist = [f.path for f in self.fm.thistab.get_selection()]
1802 try:
1803 self.fm.thisdir.vcs.action_reset(filelist)
1804 except VcsError as ex:
1805 self.fm.notify('Unable to unstage files: {0}'.format(ex))
1806 self.fm.ui.vcsthread.process(self.fm.thisdir)
1807 else:
1808 self.fm.notify('Unable to unstage files: Not in repository')
1809
1810 # Metadata commands
1811 # --------------------------------
1812
1813
1814 class prompt_metadata(Command):
1815 """
1816 :prompt_metadata <key1> [<key2> [<key3> ...]]
1817
1818 Prompt the user to input metadata for multiple keys in a row.
1819 """
1820
1821 _command_name = "meta"
1822 _console_chain = None
1823
1824 def execute(self):
1825 prompt_metadata._console_chain = self.args[1:]
1826 self._process_command_stack()
1827
1828 def _process_command_stack(self):
1829 if prompt_metadata._console_chain:
1830 key = prompt_metadata._console_chain.pop()
1831 self._fill_console(key)
1832 else:
1833 for col in self.fm.ui.browser.columns:
1834 col.need_redraw = True
1835
1836 def _fill_console(self, key):
1837 metadata = self.fm.metadata.get_metadata(self.fm.thisfile.path)
1838 if key in metadata and metadata[key]:
1839 existing_value = metadata[key]
1840 else:
1841 existing_value = ""
1842 text = "%s %s %s" % (self._command_name, key, existing_value)
1843 self.fm.open_console(text, position=len(text))
1844
1845
1846 class meta(prompt_metadata):
1847 """
1848 :meta <key> [<value>]
1849
1850 Change metadata of a file. Deletes the key if value is empty.
1851 """
1852
1853 def execute(self):
1854 key = self.arg(1)
1855 update_dict = dict()
1856 update_dict[key] = self.rest(2)
1857 selection = self.fm.thistab.get_selection()
1858 for fobj in selection:
1859 self.fm.metadata.set_metadata(fobj.path, update_dict)
1860 self._process_command_stack()
1861
1862 def tab(self, tabnum):
1863 key = self.arg(1)
1864 metadata = self.fm.metadata.get_metadata(self.fm.thisfile.path)
1865 if key in metadata and metadata[key]:
1866 return [" ".join([self.arg(0), self.arg(1), metadata[key]])]
1867 return [self.arg(0) + " " + k for k in sorted(metadata)
1868 if k.startswith(self.arg(1))]
1869
1870
1871 class linemode(default_linemode):
1872 """
1873 :linemode <mode>
1874
1875 Change what is displayed as a filename.
1876
1877 - "mode" may be any of the defined linemodes (see: ranger.core.linemode).
1878 "normal" is mapped to "filename".
1879 """
1880
1881 def execute(self):
1882 mode = self.arg(1)
1883
1884 if mode == "normal":
1885 from ranger.core.linemode import DEFAULT_LINEMODE
1886 mode = DEFAULT_LINEMODE
1887
1888 if mode not in self.fm.thisfile.linemode_dict:
1889 self.fm.notify("Unhandled linemode: `%s'" % mode, bad=True)
1890 return
1891
1892 self.fm.thisdir.set_linemode_of_children(mode)
1893
1894 # Ask the browsercolumns to redraw
1895 for col in self.fm.ui.browser.columns:
1896 col.need_redraw = True
1897
1898
1899 class yank(Command):
1900 """:yank [name|dir|path]
1901
1902 Copies the file's name (default), directory or path into both the primary X
1903 selection and the clipboard.
1904 """
1905
1906 modes = {
1907 '': 'basename',
1908 'name_without_extension': 'basename_without_extension',
1909 'name': 'basename',
1910 'dir': 'dirname',
1911 'path': 'path',
1912 }
1913
1914 def execute(self):
1915 import subprocess
1916
1917 def clipboards():
1918 from ranger.ext.get_executables import get_executables
1919 clipboard_managers = {
1920 'xclip': [
1921 ['xclip'],
1922 ['xclip', '-selection', 'clipboard'],
1923 ],
1924 'xsel': [
1925 ['xsel'],
1926 ['xsel', '-b'],
1927 ],
1928 'wl-copy': [
1929 ['wl-copy'],
1930 ],
1931 'pbcopy': [
1932 ['pbcopy'],
1933 ],
1934 }
1935 ordered_managers = ['pbcopy', 'wl-copy', 'xclip', 'xsel']
1936 executables = get_executables()
1937 for manager in ordered_managers:
1938 if manager in executables:
1939 return clipboard_managers[manager]
1940 return []
1941
1942 clipboard_commands = clipboards()
1943
1944 mode = self.modes[self.arg(1)]
1945 selection = self.get_selection_attr(mode)
1946
1947 new_clipboard_contents = "\n".join(selection)
1948 for command in clipboard_commands:
1949 process = subprocess.Popen(command, universal_newlines=True,
1950 stdin=subprocess.PIPE)
1951 process.communicate(input=new_clipboard_contents)
1952
1953 def get_selection_attr(self, attr):
1954 return [getattr(item, attr) for item in
1955 self.fm.thistab.get_selection()]
1956
1957 def tab(self, tabnum):
1958 return (
1959 self.start(1) + mode for mode
1960 in sorted(self.modes.keys())
1961 if mode
1962 )
1963
1964
1965 class paste_ext(Command):
1966 """
1967 :paste_ext
1968
1969 Like paste but tries to rename conflicting files so that the
1970 file extension stays intact (e.g. file_.ext).
1971 """
1972
1973 @staticmethod
1974 def make_safe_path(dst):
1975 if not os.path.exists(dst):
1976 return dst
1977
1978 dst_name, dst_ext = os.path.splitext(dst)
1979
1980 if not dst_name.endswith("_"):
1981 dst_name += "_"
1982 if not os.path.exists(dst_name + dst_ext):
1983 return dst_name + dst_ext
1984 n = 0
1985 test_dst = dst_name + str(n)
1986 while os.path.exists(test_dst + dst_ext):
1987 n += 1
1988 test_dst = dst_name + str(n)
1989
1990 return test_dst + dst_ext
1991
1992 def execute(self):
1993 return self.fm.paste(make_safe_path=paste_ext.make_safe_path)