commands_full.py (57039B)
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 )