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