Mercurial > dotfiles.old
comparison config/ranger/commands.py @ 245:ca66a7fd5cc9
Add ranger configs
author | zegervdv <zegervdv@me.com> |
---|---|
date | Wed, 18 Feb 2015 22:41:39 +0100 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
244:50379acb4815 | 245:ca66a7fd5cc9 |
---|---|
1 # -*- coding: utf-8 -*- | |
2 # Copyright (C) 2009-2013 Roman Zimbelmann <[email protected]> | |
3 # This configuration file is licensed under the same terms as ranger. | |
4 # =================================================================== | |
5 # This file contains ranger's commands. | |
6 # It's all in python; lines beginning with # are comments. | |
7 # | |
8 # Note that additional commands are automatically generated from the methods | |
9 # of the class ranger.core.actions.Actions. | |
10 # | |
11 # You can customize commands in the file ~/.config/ranger/commands.py. | |
12 # It has the same syntax as this file. In fact, you can just copy this | |
13 # file there with `ranger --copy-config=commands' and make your modifications. | |
14 # But make sure you update your configs when you update ranger. | |
15 # | |
16 # =================================================================== | |
17 # Every class defined here which is a subclass of `Command' will be used as a | |
18 # command in ranger. Several methods are defined to interface with ranger: | |
19 # execute(): called when the command is executed. | |
20 # cancel(): called when closing the console. | |
21 # tab(): called when <TAB> is pressed. | |
22 # quick(): called after each keypress. | |
23 # | |
24 # The return values for tab() can be either: | |
25 # None: There is no tab completion | |
26 # A string: Change the console to this string | |
27 # A list/tuple/generator: cycle through every item in it | |
28 # | |
29 # The return value for quick() can be: | |
30 # False: Nothing happens | |
31 # True: Execute the command afterwards | |
32 # | |
33 # The return value for execute() and cancel() doesn't matter. | |
34 # | |
35 # =================================================================== | |
36 # Commands have certain attributes and methods that facilitate parsing of | |
37 # the arguments: | |
38 # | |
39 # self.line: The whole line that was written in the console. | |
40 # self.args: A list of all (space-separated) arguments to the command. | |
41 # self.quantifier: If this command was mapped to the key "X" and | |
42 # the user pressed 6X, self.quantifier will be 6. | |
43 # self.arg(n): The n-th argument, or an empty string if it doesn't exist. | |
44 # self.rest(n): The n-th argument plus everything that followed. For example, | |
45 # If the command was "search foo bar a b c", rest(2) will be "bar a b c" | |
46 # self.start(n): The n-th argument and anything before it. For example, | |
47 # If the command was "search foo bar a b c", rest(2) will be "bar a b c" | |
48 # | |
49 # =================================================================== | |
50 # And this is a little reference for common ranger functions and objects: | |
51 # | |
52 # self.fm: A reference to the "fm" object which contains most information | |
53 # about ranger. | |
54 # self.fm.notify(string): Print the given string on the screen. | |
55 # self.fm.notify(string, bad=True): Print the given string in RED. | |
56 # self.fm.reload_cwd(): Reload the current working directory. | |
57 # self.fm.thisdir: The current working directory. (A File object.) | |
58 # self.fm.thisfile: The current file. (A File object too.) | |
59 # self.fm.thistab.get_selection(): A list of all selected files. | |
60 # self.fm.execute_console(string): Execute the string as a ranger command. | |
61 # self.fm.open_console(string): Open the console with the given string | |
62 # already typed in for you. | |
63 # self.fm.move(direction): Moves the cursor in the given direction, which | |
64 # can be something like down=3, up=5, right=1, left=1, to=6, ... | |
65 # | |
66 # File objects (for example self.fm.thisfile) have these useful attributes and | |
67 # methods: | |
68 # | |
69 # cf.path: The path to the file. | |
70 # cf.basename: The base name only. | |
71 # cf.load_content(): Force a loading of the directories content (which | |
72 # obviously works with directories only) | |
73 # cf.is_directory: True/False depending on whether it's a directory. | |
74 # | |
75 # For advanced commands it is unavoidable to dive a bit into the source code | |
76 # of ranger. | |
77 # =================================================================== | |
78 | |
79 import os | |
80 from ranger.api.commands import * | |
81 from ranger.core.loader import CommandLoader | |
82 | |
83 class alias(Command): | |
84 """:alias <newcommand> <oldcommand> | |
85 | |
86 Copies the oldcommand as newcommand. | |
87 """ | |
88 | |
89 context = 'browser' | |
90 resolve_macros = False | |
91 | |
92 def execute(self): | |
93 if not self.arg(1) or not self.arg(2): | |
94 self.fm.notify('Syntax: alias <newcommand> <oldcommand>', bad=True) | |
95 else: | |
96 self.fm.commands.alias(self.arg(1), self.rest(2)) | |
97 | |
98 class cd(Command): | |
99 """:cd [-r] <dirname> | |
100 | |
101 The cd command changes the directory. | |
102 The command 'cd -' is equivalent to typing ``. | |
103 Using the option "-r" will get you to the real path. | |
104 """ | |
105 | |
106 def execute(self): | |
107 import os.path | |
108 if self.arg(1) == '-r': | |
109 self.shift() | |
110 destination = os.path.realpath(self.rest(1)) | |
111 if os.path.isfile(destination): | |
112 destination = os.path.dirname(destination) | |
113 else: | |
114 destination = self.rest(1) | |
115 | |
116 if not destination: | |
117 destination = '~' | |
118 | |
119 if destination == '-': | |
120 self.fm.enter_bookmark('`') | |
121 else: | |
122 self.fm.cd(destination) | |
123 | |
124 def tab(self): | |
125 import os | |
126 from os.path import dirname, basename, expanduser, join | |
127 | |
128 cwd = self.fm.thisdir.path | |
129 rel_dest = self.rest(1) | |
130 | |
131 bookmarks = [v.path for v in self.fm.bookmarks.dct.values() | |
132 if rel_dest in v.path ] | |
133 | |
134 # expand the tilde into the user directory | |
135 if rel_dest.startswith('~'): | |
136 rel_dest = expanduser(rel_dest) | |
137 | |
138 # define some shortcuts | |
139 abs_dest = join(cwd, rel_dest) | |
140 abs_dirname = dirname(abs_dest) | |
141 rel_basename = basename(rel_dest) | |
142 rel_dirname = dirname(rel_dest) | |
143 | |
144 try: | |
145 # are we at the end of a directory? | |
146 if rel_dest.endswith('/') or rel_dest == '': | |
147 _, dirnames, _ = next(os.walk(abs_dest)) | |
148 | |
149 # are we in the middle of the filename? | |
150 else: | |
151 _, dirnames, _ = next(os.walk(abs_dirname)) | |
152 dirnames = [dn for dn in dirnames \ | |
153 if dn.startswith(rel_basename)] | |
154 except (OSError, StopIteration): | |
155 # os.walk found nothing | |
156 pass | |
157 else: | |
158 dirnames.sort() | |
159 dirnames = bookmarks + dirnames | |
160 | |
161 # no results, return None | |
162 if len(dirnames) == 0: | |
163 return | |
164 | |
165 # one result. since it must be a directory, append a slash. | |
166 if len(dirnames) == 1: | |
167 return self.start(1) + join(rel_dirname, dirnames[0]) + '/' | |
168 | |
169 # more than one result. append no slash, so the user can | |
170 # manually type in the slash to advance into that directory | |
171 return (self.start(1) + join(rel_dirname, dirname) for dirname in dirnames) | |
172 | |
173 | |
174 class chain(Command): | |
175 """:chain <command1>; <command2>; ... | |
176 | |
177 Calls multiple commands at once, separated by semicolons. | |
178 """ | |
179 def execute(self): | |
180 for command in self.rest(1).split(";"): | |
181 self.fm.execute_console(command) | |
182 | |
183 | |
184 class shell(Command): | |
185 escape_macros_for_shell = True | |
186 | |
187 def execute(self): | |
188 if self.arg(1) and self.arg(1)[0] == '-': | |
189 flags = self.arg(1)[1:] | |
190 command = self.rest(2) | |
191 else: | |
192 flags = '' | |
193 command = self.rest(1) | |
194 | |
195 if not command and 'p' in flags: | |
196 command = 'cat %f' | |
197 if command: | |
198 if '%' in command: | |
199 command = self.fm.substitute_macros(command, escape=True) | |
200 self.fm.execute_command(command, flags=flags) | |
201 | |
202 def tab(self): | |
203 from ranger.ext.get_executables import get_executables | |
204 if self.arg(1) and self.arg(1)[0] == '-': | |
205 command = self.rest(2) | |
206 else: | |
207 command = self.rest(1) | |
208 start = self.line[0:len(self.line) - len(command)] | |
209 | |
210 try: | |
211 position_of_last_space = command.rindex(" ") | |
212 except ValueError: | |
213 return (start + program + ' ' for program \ | |
214 in get_executables() if program.startswith(command)) | |
215 if position_of_last_space == len(command) - 1: | |
216 selection = self.fm.thistab.get_selection() | |
217 if len(selection) == 1: | |
218 return self.line + selection[0].shell_escaped_basename + ' ' | |
219 else: | |
220 return self.line + '%s ' | |
221 else: | |
222 before_word, start_of_word = self.line.rsplit(' ', 1) | |
223 return (before_word + ' ' + file.shell_escaped_basename \ | |
224 for file in self.fm.thisdir.files \ | |
225 if file.shell_escaped_basename.startswith(start_of_word)) | |
226 | |
227 class open_with(Command): | |
228 def execute(self): | |
229 app, flags, mode = self._get_app_flags_mode(self.rest(1)) | |
230 self.fm.execute_file( | |
231 files = [f for f in self.fm.thistab.get_selection()], | |
232 app = app, | |
233 flags = flags, | |
234 mode = mode) | |
235 | |
236 def tab(self): | |
237 return self._tab_through_executables() | |
238 | |
239 def _get_app_flags_mode(self, string): | |
240 """Extracts the application, flags and mode from a string. | |
241 | |
242 examples: | |
243 "mplayer f 1" => ("mplayer", "f", 1) | |
244 "aunpack 4" => ("aunpack", "", 4) | |
245 "p" => ("", "p", 0) | |
246 "" => None | |
247 """ | |
248 | |
249 app = '' | |
250 flags = '' | |
251 mode = 0 | |
252 split = string.split() | |
253 | |
254 if len(split) == 0: | |
255 pass | |
256 | |
257 elif len(split) == 1: | |
258 part = split[0] | |
259 if self._is_app(part): | |
260 app = part | |
261 elif self._is_flags(part): | |
262 flags = part | |
263 elif self._is_mode(part): | |
264 mode = part | |
265 | |
266 elif len(split) == 2: | |
267 part0 = split[0] | |
268 part1 = split[1] | |
269 | |
270 if self._is_app(part0): | |
271 app = part0 | |
272 if self._is_flags(part1): | |
273 flags = part1 | |
274 elif self._is_mode(part1): | |
275 mode = part1 | |
276 elif self._is_flags(part0): | |
277 flags = part0 | |
278 if self._is_mode(part1): | |
279 mode = part1 | |
280 elif self._is_mode(part0): | |
281 mode = part0 | |
282 if self._is_flags(part1): | |
283 flags = part1 | |
284 | |
285 elif len(split) >= 3: | |
286 part0 = split[0] | |
287 part1 = split[1] | |
288 part2 = split[2] | |
289 | |
290 if self._is_app(part0): | |
291 app = part0 | |
292 if self._is_flags(part1): | |
293 flags = part1 | |
294 if self._is_mode(part2): | |
295 mode = part2 | |
296 elif self._is_mode(part1): | |
297 mode = part1 | |
298 if self._is_flags(part2): | |
299 flags = part2 | |
300 elif self._is_flags(part0): | |
301 flags = part0 | |
302 if self._is_mode(part1): | |
303 mode = part1 | |
304 elif self._is_mode(part0): | |
305 mode = part0 | |
306 if self._is_flags(part1): | |
307 flags = part1 | |
308 | |
309 return app, flags, int(mode) | |
310 | |
311 def _is_app(self, arg): | |
312 return not self._is_flags(arg) and not arg.isdigit() | |
313 | |
314 def _is_flags(self, arg): | |
315 from ranger.core.runner import ALLOWED_FLAGS | |
316 return all(x in ALLOWED_FLAGS for x in arg) | |
317 | |
318 def _is_mode(self, arg): | |
319 return all(x in '0123456789' for x in arg) | |
320 | |
321 | |
322 class set_(Command): | |
323 """:set <option name>=<python expression> | |
324 | |
325 Gives an option a new value. | |
326 """ | |
327 name = 'set' # don't override the builtin set class | |
328 def execute(self): | |
329 name = self.arg(1) | |
330 name, value, _ = self.parse_setting_line() | |
331 self.fm.set_option_from_string(name, value) | |
332 | |
333 def tab(self): | |
334 name, value, name_done = self.parse_setting_line() | |
335 settings = self.fm.settings | |
336 if not name: | |
337 return sorted(self.firstpart + setting for setting in settings) | |
338 if not value and not name_done: | |
339 return (self.firstpart + setting for setting in settings \ | |
340 if setting.startswith(name)) | |
341 if not value: | |
342 return self.firstpart + str(settings[name]) | |
343 if bool in settings.types_of(name): | |
344 if 'true'.startswith(value.lower()): | |
345 return self.firstpart + 'True' | |
346 if 'false'.startswith(value.lower()): | |
347 return self.firstpart + 'False' | |
348 | |
349 | |
350 class setlocal(set_): | |
351 """:setlocal path=<python string> <option name>=<python expression> | |
352 | |
353 Gives an option a new value. | |
354 """ | |
355 PATH_RE = re.compile(r'^\s*path="?(.*?)"?\s*$') | |
356 def execute(self): | |
357 import os.path | |
358 match = self.PATH_RE.match(self.arg(1)) | |
359 if match: | |
360 path = os.path.normpath(os.path.expanduser(match.group(1))) | |
361 self.shift() | |
362 elif self.fm.thisdir: | |
363 path = self.fm.thisdir.path | |
364 else: | |
365 path = None | |
366 | |
367 if path: | |
368 name = self.arg(1) | |
369 name, value, _ = self.parse_setting_line() | |
370 self.fm.set_option_from_string(name, value, localpath=path) | |
371 | |
372 | |
373 class setintag(setlocal): | |
374 """:setintag <tag or tags> <option name>=<option value> | |
375 | |
376 Sets an option for directories that are tagged with a specific tag. | |
377 """ | |
378 def execute(self): | |
379 tags = self.arg(1) | |
380 self.shift() | |
381 name, value, _ = self.parse_setting_line() | |
382 self.fm.set_option_from_string(name, value, tags=tags) | |
383 | |
384 | |
385 class quit(Command): | |
386 """:quit | |
387 | |
388 Closes the current tab. If there is only one tab, quit the program. | |
389 """ | |
390 | |
391 def execute(self): | |
392 if len(self.fm.tabs) <= 1: | |
393 self.fm.exit() | |
394 self.fm.tab_close() | |
395 | |
396 | |
397 class quitall(Command): | |
398 """:quitall | |
399 | |
400 Quits the program immediately. | |
401 """ | |
402 | |
403 def execute(self): | |
404 self.fm.exit() | |
405 | |
406 | |
407 class quit_bang(quitall): | |
408 """:quit! | |
409 | |
410 Quits the program immediately. | |
411 """ | |
412 name = 'quit!' | |
413 allow_abbrev = False | |
414 | |
415 | |
416 class terminal(Command): | |
417 """:terminal | |
418 | |
419 Spawns an "x-terminal-emulator" starting in the current directory. | |
420 """ | |
421 def execute(self): | |
422 import os | |
423 from ranger.ext.get_executables import get_executables | |
424 command = os.environ.get('TERMCMD', os.environ.get('TERM')) | |
425 if command not in get_executables(): | |
426 command = 'x-terminal-emulator' | |
427 if command not in get_executables(): | |
428 command = 'xterm' | |
429 self.fm.run(command, flags='f') | |
430 | |
431 | |
432 class delete(Command): | |
433 """:delete | |
434 | |
435 Tries to delete the selection. | |
436 | |
437 "Selection" is defined as all the "marked files" (by default, you | |
438 can mark files with space or v). If there are no marked files, | |
439 use the "current file" (where the cursor is) | |
440 | |
441 When attempting to delete non-empty directories or multiple | |
442 marked files, it will require a confirmation. | |
443 """ | |
444 | |
445 allow_abbrev = False | |
446 | |
447 def execute(self): | |
448 import os | |
449 if self.rest(1): | |
450 self.fm.notify("Error: delete takes no arguments! It deletes " | |
451 "the selected file(s).", bad=True) | |
452 return | |
453 | |
454 cwd = self.fm.thisdir | |
455 cf = self.fm.thisfile | |
456 if not cwd or not cf: | |
457 self.fm.notify("Error: no file selected for deletion!", bad=True) | |
458 return | |
459 | |
460 confirm = self.fm.settings.confirm_on_delete | |
461 many_files = (cwd.marked_items or (cf.is_directory and not cf.is_link \ | |
462 and len(os.listdir(cf.path)) > 0)) | |
463 | |
464 if confirm != 'never' and (confirm != 'multiple' or many_files): | |
465 self.fm.ui.console.ask("Confirm deletion of: %s (y/N)" % | |
466 ', '.join(f.basename for f in self.fm.thistab.get_selection()), | |
467 self._question_callback, ('n', 'N', 'y', 'Y')) | |
468 else: | |
469 # no need for a confirmation, just delete | |
470 self.fm.delete() | |
471 | |
472 def _question_callback(self, answer): | |
473 if answer == 'y' or answer == 'Y': | |
474 self.fm.delete() | |
475 | |
476 | |
477 class mark_tag(Command): | |
478 """:mark_tag [<tags>] | |
479 | |
480 Mark all tags that are tagged with either of the given tags. | |
481 When leaving out the tag argument, all tagged files are marked. | |
482 """ | |
483 do_mark = True | |
484 | |
485 def execute(self): | |
486 cwd = self.fm.thisdir | |
487 tags = self.rest(1).replace(" ","") | |
488 if not self.fm.tags: | |
489 return | |
490 for fileobj in cwd.files: | |
491 try: | |
492 tag = self.fm.tags.tags[fileobj.realpath] | |
493 except KeyError: | |
494 continue | |
495 if not tags or tag in tags: | |
496 cwd.mark_item(fileobj, val=self.do_mark) | |
497 self.fm.ui.status.need_redraw = True | |
498 self.fm.ui.need_redraw = True | |
499 | |
500 | |
501 class console(Command): | |
502 """:console <command> | |
503 | |
504 Open the console with the given command. | |
505 """ | |
506 def execute(self): | |
507 position = None | |
508 if self.arg(1)[0:2] == '-p': | |
509 try: | |
510 position = int(self.arg(1)[2:]) | |
511 self.shift() | |
512 except: | |
513 pass | |
514 self.fm.open_console(self.rest(1), position=position) | |
515 | |
516 | |
517 class load_copy_buffer(Command): | |
518 """:load_copy_buffer | |
519 | |
520 Load the copy buffer from confdir/copy_buffer | |
521 """ | |
522 copy_buffer_filename = 'copy_buffer' | |
523 def execute(self): | |
524 from ranger.container.file import File | |
525 from os.path import exists | |
526 try: | |
527 fname = self.fm.confpath(self.copy_buffer_filename) | |
528 f = open(fname, 'r') | |
529 except: | |
530 return self.fm.notify("Cannot open %s" % \ | |
531 (fname or self.copy_buffer_filename), bad=True) | |
532 self.fm.copy_buffer = set(File(g) \ | |
533 for g in f.read().split("\n") if exists(g)) | |
534 f.close() | |
535 self.fm.ui.redraw_main_column() | |
536 | |
537 | |
538 class save_copy_buffer(Command): | |
539 """:save_copy_buffer | |
540 | |
541 Save the copy buffer to confdir/copy_buffer | |
542 """ | |
543 copy_buffer_filename = 'copy_buffer' | |
544 def execute(self): | |
545 fname = None | |
546 try: | |
547 fname = self.fm.confpath(self.copy_buffer_filename) | |
548 f = open(fname, 'w') | |
549 except: | |
550 return self.fm.notify("Cannot open %s" % \ | |
551 (fname or self.copy_buffer_filename), bad=True) | |
552 f.write("\n".join(f.path for f in self.fm.copy_buffer)) | |
553 f.close() | |
554 | |
555 | |
556 class unmark_tag(mark_tag): | |
557 """:unmark_tag [<tags>] | |
558 | |
559 Unmark all tags that are tagged with either of the given tags. | |
560 When leaving out the tag argument, all tagged files are unmarked. | |
561 """ | |
562 do_mark = False | |
563 | |
564 | |
565 class mkdir(Command): | |
566 """:mkdir <dirname> | |
567 | |
568 Creates a directory with the name <dirname>. | |
569 """ | |
570 | |
571 def execute(self): | |
572 from os.path import join, expanduser, lexists | |
573 from os import mkdir | |
574 | |
575 dirname = join(self.fm.thisdir.path, expanduser(self.rest(1))) | |
576 if not lexists(dirname): | |
577 mkdir(dirname) | |
578 else: | |
579 self.fm.notify("file/directory exists!", bad=True) | |
580 | |
581 def tab(self): | |
582 return self._tab_directory_content() | |
583 | |
584 | |
585 class touch(Command): | |
586 """:touch <fname> | |
587 | |
588 Creates a file with the name <fname>. | |
589 """ | |
590 | |
591 def execute(self): | |
592 from os.path import join, expanduser, lexists | |
593 | |
594 fname = join(self.fm.thisdir.path, expanduser(self.rest(1))) | |
595 if not lexists(fname): | |
596 open(fname, 'a').close() | |
597 else: | |
598 self.fm.notify("file/directory exists!", bad=True) | |
599 | |
600 def tab(self): | |
601 return self._tab_directory_content() | |
602 | |
603 | |
604 class edit(Command): | |
605 """:edit <filename> | |
606 | |
607 Opens the specified file in vim | |
608 """ | |
609 | |
610 def execute(self): | |
611 if not self.arg(1): | |
612 self.fm.edit_file(self.fm.thisfile.path) | |
613 else: | |
614 self.fm.edit_file(self.rest(1)) | |
615 | |
616 def tab(self): | |
617 return self._tab_directory_content() | |
618 | |
619 | |
620 class eval_(Command): | |
621 """:eval [-q] <python code> | |
622 | |
623 Evaluates the python code. | |
624 `fm' is a reference to the FM instance. | |
625 To display text, use the function `p'. | |
626 | |
627 Examples: | |
628 :eval fm | |
629 :eval len(fm.directories) | |
630 :eval p("Hello World!") | |
631 """ | |
632 name = 'eval' | |
633 resolve_macros = False | |
634 | |
635 def execute(self): | |
636 if self.arg(1) == '-q': | |
637 code = self.rest(2) | |
638 quiet = True | |
639 else: | |
640 code = self.rest(1) | |
641 quiet = False | |
642 import ranger | |
643 global cmd, fm, p, quantifier | |
644 fm = self.fm | |
645 cmd = self.fm.execute_console | |
646 p = fm.notify | |
647 quantifier = self.quantifier | |
648 try: | |
649 try: | |
650 result = eval(code) | |
651 except SyntaxError: | |
652 exec(code) | |
653 else: | |
654 if result and not quiet: | |
655 p(result) | |
656 except Exception as err: | |
657 p(err) | |
658 | |
659 | |
660 class rename(Command): | |
661 """:rename <newname> | |
662 | |
663 Changes the name of the currently highlighted file to <newname> | |
664 """ | |
665 | |
666 def execute(self): | |
667 from ranger.container.file import File | |
668 from os import access | |
669 | |
670 new_name = self.rest(1) | |
671 | |
672 if not new_name: | |
673 return self.fm.notify('Syntax: rename <newname>', bad=True) | |
674 | |
675 if new_name == self.fm.thisfile.basename: | |
676 return | |
677 | |
678 if access(new_name, os.F_OK): | |
679 return self.fm.notify("Can't rename: file already exists!", bad=True) | |
680 | |
681 self.fm.rename(self.fm.thisfile, new_name) | |
682 f = File(new_name) | |
683 self.fm.thisdir.pointed_obj = f | |
684 self.fm.thisfile = f | |
685 | |
686 def tab(self): | |
687 return self._tab_directory_content() | |
688 | |
689 | |
690 class chmod(Command): | |
691 """:chmod <octal number> | |
692 | |
693 Sets the permissions of the selection to the octal number. | |
694 | |
695 The octal number is between 0 and 777. The digits specify the | |
696 permissions for the user, the group and others. | |
697 | |
698 A 1 permits execution, a 2 permits writing, a 4 permits reading. | |
699 Add those numbers to combine them. So a 7 permits everything. | |
700 """ | |
701 | |
702 def execute(self): | |
703 mode = self.rest(1) | |
704 if not mode: | |
705 mode = str(self.quantifier) | |
706 | |
707 try: | |
708 mode = int(mode, 8) | |
709 if mode < 0 or mode > 0o777: | |
710 raise ValueError | |
711 except ValueError: | |
712 self.fm.notify("Need an octal number between 0 and 777!", bad=True) | |
713 return | |
714 | |
715 for file in self.fm.thistab.get_selection(): | |
716 try: | |
717 os.chmod(file.path, mode) | |
718 except Exception as ex: | |
719 self.fm.notify(ex) | |
720 | |
721 try: | |
722 # reloading directory. maybe its better to reload the selected | |
723 # files only. | |
724 self.fm.thisdir.load_content() | |
725 except: | |
726 pass | |
727 | |
728 | |
729 class bulkrename(Command): | |
730 """:bulkrename | |
731 | |
732 This command opens a list of selected files in an external editor. | |
733 After you edit and save the file, it will generate a shell script | |
734 which does bulk renaming according to the changes you did in the file. | |
735 | |
736 This shell script is opened in an editor for you to review. | |
737 After you close it, it will be executed. | |
738 """ | |
739 def execute(self): | |
740 import sys | |
741 import tempfile | |
742 from ranger.container.file import File | |
743 from ranger.ext.shell_escape import shell_escape as esc | |
744 py3 = sys.version > "3" | |
745 | |
746 # Create and edit the file list | |
747 filenames = [f.basename for f in self.fm.thistab.get_selection()] | |
748 listfile = tempfile.NamedTemporaryFile() | |
749 | |
750 if py3: | |
751 listfile.write("\n".join(filenames).encode("utf-8")) | |
752 else: | |
753 listfile.write("\n".join(filenames)) | |
754 listfile.flush() | |
755 self.fm.execute_file([File(listfile.name)], app='editor') | |
756 listfile.seek(0) | |
757 if py3: | |
758 new_filenames = listfile.read().decode("utf-8").split("\n") | |
759 else: | |
760 new_filenames = listfile.read().split("\n") | |
761 listfile.close() | |
762 if all(a == b for a, b in zip(filenames, new_filenames)): | |
763 self.fm.notify("No renaming to be done!") | |
764 return | |
765 | |
766 # Generate and execute script | |
767 cmdfile = tempfile.NamedTemporaryFile() | |
768 cmdfile.write(b"# This file will be executed when you close the editor.\n") | |
769 cmdfile.write(b"# Please double-check everything, clear the file to abort.\n") | |
770 if py3: | |
771 cmdfile.write("\n".join("mv -vi -- " + esc(old) + " " + esc(new) \ | |
772 for old, new in zip(filenames, new_filenames) \ | |
773 if old != new).encode("utf-8")) | |
774 else: | |
775 cmdfile.write("\n".join("mv -vi -- " + esc(old) + " " + esc(new) \ | |
776 for old, new in zip(filenames, new_filenames) if old != new)) | |
777 cmdfile.flush() | |
778 self.fm.execute_file([File(cmdfile.name)], app='editor') | |
779 self.fm.run(['/bin/sh', cmdfile.name], flags='w') | |
780 cmdfile.close() | |
781 | |
782 | |
783 class relink(Command): | |
784 """:relink <newpath> | |
785 | |
786 Changes the linked path of the currently highlighted symlink to <newpath> | |
787 """ | |
788 | |
789 def execute(self): | |
790 from ranger.container.file import File | |
791 | |
792 new_path = self.rest(1) | |
793 cf = self.fm.thisfile | |
794 | |
795 if not new_path: | |
796 return self.fm.notify('Syntax: relink <newpath>', bad=True) | |
797 | |
798 if not cf.is_link: | |
799 return self.fm.notify('%s is not a symlink!' % cf.basename, bad=True) | |
800 | |
801 if new_path == os.readlink(cf.path): | |
802 return | |
803 | |
804 try: | |
805 os.remove(cf.path) | |
806 os.symlink(new_path, cf.path) | |
807 except OSError as err: | |
808 self.fm.notify(err) | |
809 | |
810 self.fm.reset() | |
811 self.fm.thisdir.pointed_obj = cf | |
812 self.fm.thisfile = cf | |
813 | |
814 def tab(self): | |
815 if not self.rest(1): | |
816 return self.line+os.readlink(self.fm.thisfile.path) | |
817 else: | |
818 return self._tab_directory_content() | |
819 | |
820 | |
821 class help_(Command): | |
822 """:help | |
823 | |
824 Display ranger's manual page. | |
825 """ | |
826 name = 'help' | |
827 def execute(self): | |
828 if self.quantifier == 1: | |
829 self.fm.dump_keybindings() | |
830 elif self.quantifier == 2: | |
831 self.fm.dump_commands() | |
832 elif self.quantifier == 3: | |
833 self.fm.dump_settings() | |
834 else: | |
835 self.fm.display_help() | |
836 | |
837 | |
838 class copymap(Command): | |
839 """:copymap <keys> <newkeys1> [<newkeys2>...] | |
840 | |
841 Copies a "browser" keybinding from <keys> to <newkeys> | |
842 """ | |
843 context = 'browser' | |
844 | |
845 def execute(self): | |
846 if not self.arg(1) or not self.arg(2): | |
847 return self.fm.notify("Not enough arguments", bad=True) | |
848 | |
849 for arg in self.args[2:]: | |
850 self.fm.ui.keymaps.copy(self.context, self.arg(1), arg) | |
851 | |
852 | |
853 class copypmap(copymap): | |
854 """:copypmap <keys> <newkeys1> [<newkeys2>...] | |
855 | |
856 Copies a "pager" keybinding from <keys> to <newkeys> | |
857 """ | |
858 context = 'pager' | |
859 | |
860 | |
861 class copycmap(copymap): | |
862 """:copycmap <keys> <newkeys1> [<newkeys2>...] | |
863 | |
864 Copies a "console" keybinding from <keys> to <newkeys> | |
865 """ | |
866 context = 'console' | |
867 | |
868 | |
869 class copytmap(copymap): | |
870 """:copycmap <keys> <newkeys1> [<newkeys2>...] | |
871 | |
872 Copies a "taskview" keybinding from <keys> to <newkeys> | |
873 """ | |
874 context = 'taskview' | |
875 | |
876 | |
877 class unmap(Command): | |
878 """:unmap <keys> [<keys2>, ...] | |
879 | |
880 Remove the given "browser" mappings | |
881 """ | |
882 context = 'browser' | |
883 | |
884 def execute(self): | |
885 for arg in self.args[1:]: | |
886 self.fm.ui.keymaps.unbind(self.context, arg) | |
887 | |
888 | |
889 class cunmap(unmap): | |
890 """:cunmap <keys> [<keys2>, ...] | |
891 | |
892 Remove the given "console" mappings | |
893 """ | |
894 context = 'browser' | |
895 | |
896 | |
897 class punmap(unmap): | |
898 """:punmap <keys> [<keys2>, ...] | |
899 | |
900 Remove the given "pager" mappings | |
901 """ | |
902 context = 'pager' | |
903 | |
904 | |
905 class tunmap(unmap): | |
906 """:tunmap <keys> [<keys2>, ...] | |
907 | |
908 Remove the given "taskview" mappings | |
909 """ | |
910 context = 'taskview' | |
911 | |
912 | |
913 class map_(Command): | |
914 """:map <keysequence> <command> | |
915 | |
916 Maps a command to a keysequence in the "browser" context. | |
917 | |
918 Example: | |
919 map j move down | |
920 map J move down 10 | |
921 """ | |
922 name = 'map' | |
923 context = 'browser' | |
924 resolve_macros = False | |
925 | |
926 def execute(self): | |
927 self.fm.ui.keymaps.bind(self.context, self.arg(1), self.rest(2)) | |
928 | |
929 | |
930 class cmap(map_): | |
931 """:cmap <keysequence> <command> | |
932 | |
933 Maps a command to a keysequence in the "console" context. | |
934 | |
935 Example: | |
936 cmap <ESC> console_close | |
937 cmap <C-x> console_type test | |
938 """ | |
939 context = 'console' | |
940 | |
941 | |
942 class tmap(map_): | |
943 """:tmap <keysequence> <command> | |
944 | |
945 Maps a command to a keysequence in the "taskview" context. | |
946 """ | |
947 context = 'taskview' | |
948 | |
949 | |
950 class pmap(map_): | |
951 """:pmap <keysequence> <command> | |
952 | |
953 Maps a command to a keysequence in the "pager" context. | |
954 """ | |
955 context = 'pager' | |
956 | |
957 | |
958 class scout(Command): | |
959 """:scout [-FLAGS] <pattern> | |
960 | |
961 Swiss army knife command for searching, traveling and filtering files. | |
962 The command takes various flags as arguments which can be used to | |
963 influence its behaviour: | |
964 | |
965 -a = automatically open a file on unambiguous match | |
966 -e = open the selected file when pressing enter | |
967 -f = filter files that match the current search pattern | |
968 -g = interpret pattern as a glob pattern | |
969 -i = ignore the letter case of the files | |
970 -k = keep the console open when changing a directory with the command | |
971 -l = letter skipping; e.g. allow "rdme" to match the file "readme" | |
972 -m = mark the matching files after pressing enter | |
973 -M = unmark the matching files after pressing enter | |
974 -p = permanent filter: hide non-matching files after pressing enter | |
975 -s = smart case; like -i unless pattern contains upper case letters | |
976 -t = apply filter and search pattern as you type | |
977 -v = inverts the match | |
978 | |
979 Multiple flags can be combined. For example, ":scout -gpt" would create | |
980 a :filter-like command using globbing. | |
981 """ | |
982 AUTO_OPEN = 'a' | |
983 OPEN_ON_ENTER = 'e' | |
984 FILTER = 'f' | |
985 SM_GLOB = 'g' | |
986 IGNORE_CASE = 'i' | |
987 KEEP_OPEN = 'k' | |
988 SM_LETTERSKIP = 'l' | |
989 MARK = 'm' | |
990 UNMARK = 'M' | |
991 PERM_FILTER = 'p' | |
992 SM_REGEX = 'r' | |
993 SMART_CASE = 's' | |
994 AS_YOU_TYPE = 't' | |
995 INVERT = 'v' | |
996 | |
997 def __init__(self, *args, **kws): | |
998 Command.__init__(self, *args, **kws) | |
999 self._regex = None | |
1000 self.flags, self.pattern = self.parse_flags() | |
1001 | |
1002 def execute(self): | |
1003 thisdir = self.fm.thisdir | |
1004 flags = self.flags | |
1005 pattern = self.pattern | |
1006 regex = self._build_regex() | |
1007 count = self._count(move=True) | |
1008 | |
1009 self.fm.thistab.last_search = regex | |
1010 self.fm.set_search_method(order="search") | |
1011 | |
1012 if self.MARK in flags or self.UNMARK in flags: | |
1013 value = flags.find(self.MARK) > flags.find(self.UNMARK) | |
1014 if self.FILTER in flags: | |
1015 for f in thisdir.files: | |
1016 thisdir.mark_item(f, value) | |
1017 else: | |
1018 for f in thisdir.files: | |
1019 if regex.search(f.basename): | |
1020 thisdir.mark_item(f, value) | |
1021 | |
1022 if self.PERM_FILTER in flags: | |
1023 thisdir.filter = regex if pattern else None | |
1024 | |
1025 # clean up: | |
1026 self.cancel() | |
1027 | |
1028 if self.OPEN_ON_ENTER in flags or \ | |
1029 self.AUTO_OPEN in flags and count == 1: | |
1030 if os.path.exists(pattern): | |
1031 self.fm.cd(pattern) | |
1032 else: | |
1033 self.fm.move(right=1) | |
1034 | |
1035 if self.KEEP_OPEN in flags and thisdir != self.fm.thisdir: | |
1036 # reopen the console: | |
1037 self.fm.open_console(self.line[0:-len(pattern)]) | |
1038 | |
1039 if thisdir != self.fm.thisdir and pattern != "..": | |
1040 self.fm.block_input(0.5) | |
1041 | |
1042 def cancel(self): | |
1043 self.fm.thisdir.temporary_filter = None | |
1044 self.fm.thisdir.refilter() | |
1045 | |
1046 def quick(self): | |
1047 asyoutype = self.AS_YOU_TYPE in self.flags | |
1048 if self.FILTER in self.flags: | |
1049 self.fm.thisdir.temporary_filter = self._build_regex() | |
1050 if self.PERM_FILTER in self.flags and asyoutype: | |
1051 self.fm.thisdir.filter = self._build_regex() | |
1052 if self.FILTER in self.flags or self.PERM_FILTER in self.flags: | |
1053 self.fm.thisdir.refilter() | |
1054 if self._count(move=asyoutype) == 1 and self.AUTO_OPEN in self.flags: | |
1055 return True | |
1056 return False | |
1057 | |
1058 def tab(self): | |
1059 self._count(move=True, offset=1) | |
1060 | |
1061 def _build_regex(self): | |
1062 if self._regex is not None: | |
1063 return self._regex | |
1064 | |
1065 frmat = "%s" | |
1066 flags = self.flags | |
1067 pattern = self.pattern | |
1068 | |
1069 if pattern == ".": | |
1070 return re.compile("") | |
1071 | |
1072 # Handle carets at start and dollar signs at end separately | |
1073 if pattern.startswith('^'): | |
1074 pattern = pattern[1:] | |
1075 frmat = "^" + frmat | |
1076 if pattern.endswith('$'): | |
1077 pattern = pattern[:-1] | |
1078 frmat += "$" | |
1079 | |
1080 # Apply one of the search methods | |
1081 if self.SM_REGEX in flags: | |
1082 regex = pattern | |
1083 elif self.SM_GLOB in flags: | |
1084 regex = re.escape(pattern).replace("\\*", ".*").replace("\\?", ".") | |
1085 elif self.SM_LETTERSKIP in flags: | |
1086 regex = ".*".join(re.escape(c) for c in pattern) | |
1087 else: | |
1088 regex = re.escape(pattern) | |
1089 | |
1090 regex = frmat % regex | |
1091 | |
1092 # Invert regular expression if necessary | |
1093 if self.INVERT in flags: | |
1094 regex = "^(?:(?!%s).)*$" % regex | |
1095 | |
1096 # Compile Regular Expression | |
1097 options = re.LOCALE | re.UNICODE | |
1098 if self.IGNORE_CASE in flags or self.SMART_CASE in flags and \ | |
1099 pattern.islower(): | |
1100 options |= re.IGNORECASE | |
1101 try: | |
1102 self._regex = re.compile(regex, options) | |
1103 except: | |
1104 self._regex = re.compile("") | |
1105 return self._regex | |
1106 | |
1107 def _count(self, move=False, offset=0): | |
1108 count = 0 | |
1109 cwd = self.fm.thisdir | |
1110 pattern = self.pattern | |
1111 | |
1112 if not pattern: | |
1113 return 0 | |
1114 if pattern == '.': | |
1115 return 0 | |
1116 if pattern == '..': | |
1117 return 1 | |
1118 | |
1119 deq = deque(cwd.files) | |
1120 deq.rotate(-cwd.pointer - offset) | |
1121 i = offset | |
1122 regex = self._build_regex() | |
1123 for fsobj in deq: | |
1124 if regex.search(fsobj.basename): | |
1125 count += 1 | |
1126 if move and count == 1: | |
1127 cwd.move(to=(cwd.pointer + i) % len(cwd.files)) | |
1128 self.fm.thisfile = cwd.pointed_obj | |
1129 if count > 1: | |
1130 return count | |
1131 i += 1 | |
1132 | |
1133 return count == 1 | |
1134 | |
1135 | |
1136 class grep(Command): | |
1137 """:grep <string> | |
1138 | |
1139 Looks for a string in all marked files or directories | |
1140 """ | |
1141 | |
1142 def execute(self): | |
1143 if self.rest(1): | |
1144 action = ['grep', '--line-number'] | |
1145 action.extend(['-e', self.rest(1), '-r']) | |
1146 action.extend(f.path for f in self.fm.thistab.get_selection()) | |
1147 self.fm.execute_command(action, flags='p') | |
1148 | |
1149 | |
1150 # Version control commands | |
1151 # -------------------------------- | |
1152 class stage(Command): | |
1153 """ | |
1154 :stage | |
1155 | |
1156 Stage selected files for the corresponding version control system | |
1157 """ | |
1158 def execute(self): | |
1159 from ranger.ext.vcs import VcsError | |
1160 | |
1161 filelist = [f.path for f in self.fm.thistab.get_selection()] | |
1162 self.fm.thisdir.vcs_outdated = True | |
1163 # for f in self.fm.thistab.get_selection(): | |
1164 # f.vcs_outdated = True | |
1165 | |
1166 try: | |
1167 self.fm.thisdir.vcs.add(filelist) | |
1168 except VcsError: | |
1169 self.fm.notify("Could not stage files.") | |
1170 | |
1171 self.fm.reload_cwd() | |
1172 | |
1173 | |
1174 class unstage(Command): | |
1175 """ | |
1176 :unstage | |
1177 | |
1178 Unstage selected files for the corresponding version control system | |
1179 """ | |
1180 def execute(self): | |
1181 from ranger.ext.vcs import VcsError | |
1182 | |
1183 filelist = [f.path for f in self.fm.thistab.get_selection()] | |
1184 self.fm.thisdir.vcs_outdated = True | |
1185 # for f in self.fm.thistab.get_selection(): | |
1186 # f.vcs_outdated = True | |
1187 | |
1188 try: | |
1189 self.fm.thisdir.vcs.reset(filelist) | |
1190 except VcsError: | |
1191 self.fm.notify("Could not unstage files.") | |
1192 | |
1193 self.fm.reload_cwd() | |
1194 | |
1195 | |
1196 class diff(Command): | |
1197 """ | |
1198 :diff | |
1199 | |
1200 Displays a diff of selected files against last last commited version | |
1201 """ | |
1202 def execute(self): | |
1203 from ranger.ext.vcs import VcsError | |
1204 import tempfile | |
1205 | |
1206 L = self.fm.thistab.get_selection() | |
1207 if len(L) == 0: return | |
1208 | |
1209 filelist = [f.path for f in L] | |
1210 vcs = L[0].vcs | |
1211 | |
1212 diff = vcs.get_raw_diff(filelist=filelist) | |
1213 if len(diff.strip()) > 0: | |
1214 tmp = tempfile.NamedTemporaryFile() | |
1215 tmp.write(diff.encode('utf-8')) | |
1216 tmp.flush() | |
1217 | |
1218 pager = os.environ.get('PAGER', ranger.DEFAULT_PAGER) | |
1219 self.fm.run([pager, tmp.name]) | |
1220 else: | |
1221 raise Exception("diff is empty") | |
1222 | |
1223 | |
1224 class log(Command): | |
1225 """ | |
1226 :log | |
1227 | |
1228 Displays the log of the current repo or files | |
1229 """ | |
1230 def execute(self): | |
1231 from ranger.ext.vcs import VcsError | |
1232 import tempfile | |
1233 | |
1234 L = self.fm.thistab.get_selection() | |
1235 if len(L) == 0: return | |
1236 | |
1237 filelist = [f.path for f in L] | |
1238 vcs = L[0].vcs | |
1239 | |
1240 log = vcs.get_raw_log(filelist=filelist) | |
1241 tmp = tempfile.NamedTemporaryFile() | |
1242 tmp.write(log.encode('utf-8')) | |
1243 tmp.flush() | |
1244 | |
1245 pager = os.environ.get('PAGER', ranger.DEFAULT_PAGER) | |
1246 self.fm.run([pager, tmp.name]) | |
1247 | |
1248 class extracthere(Command): | |
1249 def execute(self): | |
1250 """ Extract copied files to current directory """ | |
1251 copied_files = tuple(self.fm.env.copy) | |
1252 | |
1253 if not copied_files: | |
1254 return | |
1255 | |
1256 def refresh(_): | |
1257 cwd = self.fm.env.get_directory(original_path) | |
1258 cwd.load_content() | |
1259 | |
1260 one_file = copied_files[0] | |
1261 cwd = self.fm.env.cwd | |
1262 original_path = cwd.path | |
1263 au_flags = ['-X', cwd.path] | |
1264 au_flags += self.line.split()[1:] | |
1265 au_flags += ['-e'] | |
1266 | |
1267 self.fm.env.copy.clear() | |
1268 self.fm.env.cut = False | |
1269 if len(copied_files) == 1: | |
1270 descr = "extracting: " + os.path.basename(one_file.path) | |
1271 else: | |
1272 descr = "extracting files from: " + os.path.basename(one_file.dirname) | |
1273 obj = CommandLoader(args=['aunpack'] + au_flags \ | |
1274 + [f.path for f in copied_files], descr=descr) | |
1275 | |
1276 obj.signal_bind('after', refresh) | |
1277 self.fm.loader.add(obj) | |
1278 | |
1279 class compress(Command): | |
1280 def execute(self): | |
1281 """ Compress marked files to current directory """ | |
1282 cwd = self.fm.env.cwd | |
1283 marked_files = cwd.get_selection() | |
1284 | |
1285 if not marked_files: | |
1286 return | |
1287 | |
1288 def refresh(_): | |
1289 cwd = self.fm.env.get_directory(original_path) | |
1290 cwd.load_content() | |
1291 | |
1292 original_path = cwd.path | |
1293 parts = self.line.split() | |
1294 au_flags = parts[1:] | |
1295 | |
1296 descr = "compressing files in: " + os.path.basename(parts[1]) | |
1297 obj = CommandLoader(args=['apack'] + au_flags + \ | |
1298 [os.path.relpath(f.path, cwd.path) for f in marked_files], descr=descr) | |
1299 | |
1300 obj.signal_bind('after', refresh) | |
1301 self.fm.loader.add(obj) | |
1302 | |
1303 def tab(self): | |
1304 """ Complete with current folder name """ | |
1305 | |
1306 extension = ['.zip', '.tar.gz', '.rar', '.7z'] | |
1307 return ['compress ' + os.path.basename(self.fm.env.cwd.path) + ext for ext in extension] |