core.actions: Safer _get_macros()
[ranger.git] / ranger / core / actions.py
blob0961c3bdc1560032b5b024f4cf740bced7623631
1 # Copyright (C) 2009, 2010, 2011 Roman Zimbelmann <romanz@lavabit.com>
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation, either version 3 of the License, or
6 # (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 import codecs
17 import os
18 import re
19 import shutil
20 import string
21 import tempfile
22 from os.path import join, isdir, realpath
23 from os import link, symlink, getcwd
24 from inspect import cleandoc
26 import ranger
27 from ranger.ext.direction import Direction
28 from ranger.ext.relative_symlink import relative_symlink
29 from ranger.ext.keybinding_parser import key_to_string, construct_keybinding
30 from ranger.ext.shell_escape import shell_quote
31 from ranger.core.shared import FileManagerAware, EnvironmentAware, \
32 SettingsAware
33 from ranger.fsobject import File
34 from ranger.core.loader import CommandLoader
36 MACRO_FAIL = "<\x01\x01MACRO_HAS_NO_VALUE\x01\01>"
38 class _MacroTemplate(string.Template):
39 """A template for substituting macros in commands"""
40 delimiter = ranger.MACRO_DELIMITER
42 class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
43 search_method = 'ctime'
45 # --------------------------
46 # -- Basic Commands
47 # --------------------------
49 def exit(self):
50 """Exit the program"""
51 raise SystemExit()
53 def reset(self):
54 """Reset the filemanager, clearing the directory buffer"""
55 old_path = self.env.cwd.path
56 self.previews = {}
57 self.env.garbage_collect(-1, self.tabs)
58 self.enter_dir(old_path)
60 def reload_cwd(self):
61 try:
62 cwd = self.env.cwd
63 except:
64 pass
65 cwd.unload()
66 cwd.load_content()
68 def notify(self, text, duration=4, bad=False):
69 if isinstance(text, Exception):
70 if ranger.arg.debug:
71 raise
72 bad = True
73 text = str(text)
74 self.log.appendleft(text)
75 if self.ui and self.ui.is_on:
76 self.ui.status.notify(" ".join(text.split("\n")),
77 duration=duration, bad=bad)
78 else:
79 print(text)
81 def abort(self):
82 try:
83 item = self.loader.queue[0]
84 except:
85 self.notify("Type Q or :quit<Enter> to exit ranger")
86 else:
87 self.notify("Aborting: " + item.get_description())
88 self.loader.remove(index=0)
90 def redraw_window(self):
91 """Redraw the window"""
92 self.ui.redraw_window()
94 def open_console(self, string='', prompt=None, position=None):
95 """Open the console if the current UI supports that"""
96 self.ui.open_console(string, prompt=prompt, position=position)
98 def execute_console(self, string='', wildcards=[], quantifier=None):
99 """Execute a command for the console"""
100 command_name = string.split()[0]
101 cmd_class = self.commands.get_command(command_name, abbrev=False)
102 if cmd_class is None:
103 self.notify("Command not found: `%s'" % command_name, bad=True)
104 return
105 cmd = cmd_class(string)
106 if cmd.resolve_macros and _MacroTemplate.delimiter in string:
107 macros = dict(('any%d'%i, key_to_string(char)) \
108 for i, char in enumerate(wildcards))
109 if 'any0' in macros:
110 macros['any'] = macros['any0']
111 try:
112 string = self.substitute_macros(string, additional=macros,
113 escape=cmd.escape_macros_for_shell)
114 except ValueError as e:
115 if ranger.arg.debug:
116 raise
117 else:
118 return self.notify(e)
119 try:
120 cmd_class(string, quantifier=quantifier).execute()
121 except Exception as e:
122 if ranger.arg.debug:
123 raise
124 else:
125 self.notify(e)
127 def substitute_macros(self, string, additional=dict(), escape=False):
128 macros = self._get_macros()
129 macros.update(additional)
130 if escape:
131 for key, value in macros.items():
132 if isinstance(value, list):
133 macros[key] = " ".join(shell_quote(s) for s in value)
134 elif value != MACRO_FAIL:
135 macros[key] = shell_quote(value)
136 else:
137 for key, value in macros.items():
138 if isinstance(value, list):
139 macros[key] = " ".join(value)
140 result = _MacroTemplate(string).safe_substitute(macros)
141 if MACRO_FAIL in result:
142 raise ValueError("Could not apply macros to `%s'" % string)
143 return result
145 def _get_macros(self):
146 macros = {}
148 macros['rangerdir'] = ranger.RANGERDIR
150 if self.fm.env.cf:
151 macros['f'] = self.fm.env.cf.basename
152 else:
153 macros['f'] = MACRO_FAIL
155 if self.fm.env.get_selection:
156 macros['s'] = [fl.basename for fl in self.fm.env.get_selection()]
157 else:
158 macros['s'] = MACRO_FAIL
160 if self.fm.env.copy:
161 macros['c'] = [fl.path for fl in self.fm.env.copy]
162 else:
163 macros['c'] = MACRO_FAIL
165 if self.fm.env.cwd.files:
166 macros['t'] = [fl.basename for fl in self.fm.env.cwd.files
167 if fl.realpath in (self.fm.tags or [])]
168 else:
169 macros['t'] = MACRO_FAIL
171 if self.fm.env.cwd:
172 macros['d'] = self.fm.env.cwd.path
173 else:
174 macros['d'] = '.'
176 # define d/f/s macros for each tab
177 for i in range(1,10):
178 try:
179 tab_dir_path = self.fm.tabs[i]
180 except:
181 continue
182 tab_dir = self.fm.env.get_directory(tab_dir_path)
183 i = str(i)
184 macros[i + 'd'] = tab_dir_path
185 if tab_dir.get_selection():
186 macros[i + 's'] = [fl.path for fl in tab_dir.get_selection()]
187 else:
188 macros[i + 's'] = MACRO_FAIL
189 if tab_dir.pointed_obj:
190 macros[i + 'f'] = tab_dir.pointed_obj.path
191 else:
192 macros[i + 'f'] = MACRO_FAIL
194 # define D/F/S for the next tab
195 found_current_tab = False
196 next_tab_path = None
197 first_tab = None
198 for tab in self.fm.tabs:
199 if not first_tab:
200 first_tab = tab
201 if found_current_tab:
202 next_tab_path = self.fm.tabs[tab]
203 break
204 if self.fm.current_tab == tab:
205 found_current_tab = True
206 if found_current_tab and not next_tab_path:
207 next_tab_path = self.fm.tabs[first_tab]
208 next_tab = self.fm.env.get_directory(next_tab_path)
210 if next_tab:
211 macros['D'] = str(next_tab.path)
212 if next_tab.pointed_obj:
213 macros['F'] = next_tab.pointed_obj.path
214 else:
215 macros['F'] = MACRO_FAIL
216 if next_tab.get_selection():
217 macros['S'] = [fl.path for fl in next_tab.get_selection()]
218 except:
219 macros['S'] = MACRO_FAIL
220 else:
221 macros['D'] = MACRO_FAIL
222 macros['F'] = MACRO_FAIL
223 macros['S'] = MACRO_FAIL
225 return macros
227 def source(self, filename):
228 filename = os.path.expanduser(filename)
229 for line in open(filename, 'r'):
230 line = line.rstrip("\r\n")
231 if line.startswith("#") or not line.strip():
232 continue
233 try:
234 self.execute_console(line)
235 except Exception as e:
236 if ranger.arg.debug:
237 raise
238 else:
239 self.notify('Error in line `%s\':\n %s' %
240 (line, str(e)), bad=True)
242 def execute_file(self, files, **kw):
243 """Execute a file.
244 app is the name of a method in Applications, without the "app_"
245 flags is a string consisting of runner.ALLOWED_FLAGS
246 mode is a positive integer.
247 Both flags and mode specify how the program is run."""
249 # ranger can act as a file chooser when running with --choosefile=...
250 if ranger.arg.choosefile:
251 open(ranger.arg.choosefile, 'w').write(self.fm.env.cf.path)
252 raise SystemExit()
254 if isinstance(files, set):
255 files = list(files)
256 elif type(files) not in (list, tuple):
257 files = [files]
259 if 'flags' in kw:
260 from ranger.core.runner import Context
261 context = Context(files=list(files), flags=kw['flags'])
262 context.squash_flags()
263 if 'c' in context.flags:
264 files = [self.fm.env.cf]
266 self.signal_emit('execute.before', keywords=kw)
267 try:
268 return self.run(files=list(files), **kw)
269 finally:
270 self.signal_emit('execute.after')
272 # --------------------------
273 # -- Moving Around
274 # --------------------------
276 def move(self, narg=None, **kw):
278 A universal movement method.
280 Accepts these parameters:
281 (int) down, (int) up, (int) left, (int) right, (int) to,
282 (bool) absolute, (bool) relative, (bool) pages,
283 (bool) percentage
285 to=X is translated to down=X, absolute=True
287 Example:
288 self.move(down=4, pages=True) # moves down by 4 pages.
289 self.move(to=2, pages=True) # moves to page 2.
290 self.move(to=1, percentage=True) # moves to 80%
292 cwd = self.env.cwd
293 direction = Direction(kw)
294 if 'left' in direction or direction.left() > 0:
295 steps = direction.left()
296 if narg is not None:
297 steps *= narg
298 try:
299 directory = os.path.join(*(['..'] * steps))
300 except:
301 return
302 self.env.enter_dir(directory)
303 if cwd and cwd.accessible and cwd.content_loaded:
304 if 'right' in direction:
305 mode = 0
306 if narg is not None:
307 mode = narg
308 cf = self.env.cf
309 selection = self.env.get_selection()
310 if not self.env.enter_dir(cf) and selection:
311 if self.execute_file(selection, mode=mode) is False:
312 self.open_console('open_with ')
313 elif direction.vertical() and cwd.files:
314 newpos = direction.move(
315 direction=direction.down(),
316 override=narg,
317 maximum=len(cwd),
318 current=cwd.pointer,
319 pagesize=self.ui.browser.hei)
320 cwd.move(to=newpos)
322 def move_parent(self, n, narg=None):
323 if narg is not None:
324 n *= narg
325 parent = self.env.at_level(-1)
326 if parent is not None:
327 if parent.pointer + n < 0:
328 n = 0 - parent.pointer
329 try:
330 self.env.enter_dir(parent.files[parent.pointer+n])
331 except IndexError:
332 pass
334 def history_go(self, relative):
335 """Move back and forth in the history"""
336 self.env.history_go(int(relative))
338 def scroll(self, relative):
339 """Scroll down by <relative> lines"""
340 if self.ui.browser and self.ui.browser.main_column:
341 self.ui.browser.main_column.scroll(relative)
342 self.env.cf = self.env.cwd.pointed_obj
344 def enter_dir(self, path, remember=False, history=True):
345 """Enter the directory at the given path"""
346 if remember:
347 cwd = self.env.cwd
348 result = self.env.enter_dir(path, history=history)
349 self.bookmarks.remember(cwd)
350 return result
351 return self.env.enter_dir(path, history=history)
353 def cd(self, path, remember=True):
354 """enter the directory at the given path, remember=True"""
355 self.enter_dir(path, remember=remember)
357 def traverse(self):
358 cf = self.env.cf
359 cwd = self.env.cwd
360 if cf is not None and cf.is_directory:
361 self.enter_dir(cf.path)
362 elif cwd.pointer >= len(cwd) - 1:
363 while True:
364 self.move(left=1)
365 cwd = self.env.cwd
366 if cwd.pointer < len(cwd) - 1:
367 break
368 if cwd.path == '/':
369 break
370 self.move(down=1)
371 self.traverse()
372 else:
373 self.move(down=1)
374 self.traverse()
376 # --------------------------
377 # -- Shortcuts / Wrappers
378 # --------------------------
380 def pager_move(self, narg=None, **kw):
381 self.ui.browser.pager.move(narg=narg, **kw)
383 def taskview_move(self, narg=None, **kw):
384 self.ui.taskview.move(narg=narg, **kw)
386 def pager_close(self):
387 if self.ui.pager.visible:
388 self.ui.close_pager()
389 if self.ui.browser.pager.visible:
390 self.ui.close_embedded_pager()
392 def taskview_open(self):
393 self.ui.open_taskview()
395 def taskview_close(self):
396 self.ui.close_taskview()
398 def execute_command(self, cmd, **kw):
399 return self.run(cmd, **kw)
401 def edit_file(self, file=None):
402 """Calls execute_file with the current file and app='editor'"""
403 if file is None:
404 file = self.env.cf
405 elif isinstance(file, str):
406 file = File(os.path.expanduser(file))
407 if file is None:
408 return
409 self.execute_file(file, app = 'editor')
411 def toggle_option(self, string):
412 """Toggle a boolean option named <string>"""
413 if isinstance(self.env.settings[string], bool):
414 self.env.settings[string] ^= True
416 def set_option(self, optname, value):
417 """Set the value of an option named <optname>"""
418 self.env.settings[optname] = value
420 def sort(self, func=None, reverse=None):
421 if reverse is not None:
422 self.env.settings['sort_reverse'] = bool(reverse)
424 if func is not None:
425 self.env.settings['sort'] = str(func)
427 def set_filter(self, fltr):
428 try:
429 self.env.cwd.filter = fltr
430 except:
431 pass
433 def mark_files(self, all=False, toggle=False, val=None, movedown=None, narg=1):
435 A wrapper for the directory.mark_xyz functions.
437 Arguments:
438 all - change all files of the current directory at once?
439 toggle - toggle the marked-status?
440 val - mark or unmark?
443 if self.env.cwd is None:
444 return
446 cwd = self.env.cwd
448 if not cwd.accessible:
449 return
451 if movedown is None:
452 movedown = not all
454 if val is None and toggle is False:
455 return
457 if all:
458 if toggle:
459 cwd.toggle_all_marks()
460 else:
461 cwd.mark_all(val)
462 else:
463 for i in range(cwd.pointer, min(cwd.pointer + narg, len(cwd))):
464 item = cwd.files[i]
465 if item is not None:
466 if toggle:
467 cwd.toggle_mark(item)
468 else:
469 cwd.mark_item(item, val)
471 if movedown:
472 self.move(down=narg)
474 self.ui.redraw_main_column()
475 self.ui.status.need_redraw = True
477 def mark_in_direction(self, val=True, dirarg=None):
478 cwd = self.env.cwd
479 direction = Direction(dirarg)
480 pos, selected = direction.select(lst=cwd.files, current=cwd.pointer,
481 pagesize=self.env.termsize[0])
482 cwd.pointer = pos
483 cwd.correct_pointer()
484 for item in selected:
485 cwd.mark_item(item, val)
487 # --------------------------
488 # -- Searching
489 # --------------------------
491 def search_file(self, text, offset=1, regexp=True):
492 if isinstance(text, str) and regexp:
493 try:
494 text = re.compile(text, re.L | re.U | re.I)
495 except:
496 return False
497 self.env.last_search = text
498 self.search_next(order='search', offset=offset)
500 def search_next(self, order=None, offset=1, forward=True):
501 original_order = order
503 if order is None:
504 order = self.search_method
505 else:
506 self.set_search_method(order=order)
508 if order in ('search', 'tag'):
509 if order == 'search':
510 arg = self.env.last_search
511 if arg is None:
512 return False
513 if hasattr(arg, 'search'):
514 fnc = lambda x: arg.search(x.basename)
515 else:
516 fnc = lambda x: arg in x.basename
517 elif order == 'tag':
518 fnc = lambda x: x.realpath in self.tags
520 return self.env.cwd.search_fnc(fnc=fnc, offset=offset, forward=forward)
522 elif order in ('size', 'mimetype', 'ctime', 'mtime', 'atime'):
523 cwd = self.env.cwd
524 if original_order is not None or not cwd.cycle_list:
525 lst = list(cwd.files)
526 if order == 'size':
527 fnc = lambda item: -item.size
528 elif order == 'mimetype':
529 fnc = lambda item: item.mimetype
530 elif order == 'ctime':
531 fnc = lambda item: -int(item.stat and item.stat.st_ctime)
532 elif order == 'atime':
533 fnc = lambda item: -int(item.stat and item.stat.st_atime)
534 elif order == 'mtime':
535 fnc = lambda item: -int(item.stat and item.stat.st_mtime)
536 lst.sort(key=fnc)
537 cwd.set_cycle_list(lst)
538 return cwd.cycle(forward=None)
540 return cwd.cycle(forward=forward)
542 def set_search_method(self, order, forward=True):
543 if order in ('search', 'tag', 'size', 'mimetype', 'ctime'):
544 self.search_method = order
546 # --------------------------
547 # -- Tags
548 # --------------------------
549 # Tags are saved in ~/.config/ranger/tagged and simply mark if a
550 # file is important to you in any context.
552 def tag_toggle(self, paths=None, value=None, movedown=None, tag=None):
553 if not self.tags:
554 return
555 if paths is None:
556 tags = tuple(x.realpath for x in self.env.get_selection())
557 else:
558 tags = [realpath(path) for path in paths]
559 if value is True:
560 self.tags.add(*tags, tag=tag or self.tags.default_tag)
561 elif value is False:
562 self.tags.remove(*tags)
563 else:
564 self.tags.toggle(*tags, tag=tag or self.tags.default_tag)
566 if movedown is None:
567 movedown = len(tags) == 1 and paths is None
568 if movedown:
569 self.move(down=1)
571 self.ui.redraw_main_column()
573 def tag_remove(self, paths=None, movedown=None):
574 self.tag_toggle(paths=paths, value=False, movedown=movedown)
576 def tag_add(self, paths=None, movedown=None):
577 self.tag_toggle(paths=paths, value=True, movedown=movedown)
579 # --------------------------
580 # -- Bookmarks
581 # --------------------------
582 # Using ranger.container.bookmarks.
584 def enter_bookmark(self, key):
585 """Enter the bookmark with the name <key>"""
586 try:
587 self.bookmarks.update_if_outdated()
588 destination = self.bookmarks[str(key)]
589 cwd = self.env.cwd
590 if destination.path != cwd.path:
591 self.bookmarks.enter(str(key))
592 self.bookmarks.remember(cwd)
593 except KeyError:
594 pass
596 def set_bookmark(self, key):
597 """Set the bookmark with the name <key> to the current directory"""
598 self.bookmarks.update_if_outdated()
599 self.bookmarks[str(key)] = self.env.cwd
601 def unset_bookmark(self, key):
602 """Delete the bookmark with the name <key>"""
603 self.bookmarks.update_if_outdated()
604 self.bookmarks.delete(str(key))
606 def draw_bookmarks(self):
607 self.ui.browser.draw_bookmarks = True
609 def hide_bookmarks(self):
610 self.ui.browser.draw_bookmarks = False
612 # --------------------------
613 # -- Pager
614 # --------------------------
615 # These commands open the built-in pager and set specific sources.
617 def display_command_help(self, console_widget):
618 try:
619 command = console_widget._get_cmd_class()
620 except:
621 self.notify("Feature not available!", bad=True)
622 return
624 if not command:
625 self.notify("Command not found!", bad=True)
626 return
628 if not command.__doc__:
629 self.notify("Command has no docstring. Try using python without -OO",
630 bad=True)
631 return
633 pager = self.ui.open_pager()
634 lines = cleandoc(command.__doc__).split('\n')
635 pager.set_source(lines)
637 def display_help(self):
638 manualpath = self.relpath('../doc/ranger.1')
639 if os.path.exists(manualpath):
640 process = self.run(['man', manualpath])
641 if process.poll() != 16:
642 return
643 process = self.run(['man', 'ranger'])
644 if process.poll() == 16:
645 self.notify("Could not find manpage.", bad=True)
647 def display_log(self):
648 pager = self.ui.open_pager()
649 if self.log:
650 pager.set_source(["Message Log:"] + list(self.log))
651 else:
652 pager.set_source(["Message Log:", "No messages!"])
654 def display_file(self):
655 if not self.env.cf or not self.env.cf.is_file:
656 return
658 pager = self.ui.open_embedded_pager()
659 pager.set_source(self.env.cf.get_preview_source(pager.wid, pager.hei))
661 # --------------------------
662 # -- Previews
663 # --------------------------
664 def update_preview(self, path):
665 try:
666 del self.previews[path]
667 self.ui.need_redraw = True
668 except:
669 return False
671 def get_preview(self, path, width, height):
672 if self.settings.preview_script and self.settings.use_preview_script:
673 # self.previews is a 2 dimensional dict:
674 # self.previews['/tmp/foo.jpg'][(80, 24)] = "the content..."
675 # self.previews['/tmp/foo.jpg']['loading'] = False
676 # A -1 in tuples means "any"; (80, -1) = wid. of 80 and any hei.
677 # The key 'foundpreview' is added later. Values in (True, False)
678 try:
679 data = self.previews[path]
680 except:
681 data = self.previews[path] = {'loading': False}
682 else:
683 if data['loading']:
684 return None
686 found = data.get((-1, -1), data.get((width, -1),
687 data.get((-1, height), data.get((width, height), False))))
688 if found == False:
689 data['loading'] = True
690 loadable = CommandLoader(args=[self.settings.preview_script,
691 path, str(width), str(height)], read=True,
692 silent=True, descr="Getting preview of %s" % path)
693 def on_after(signal):
694 exit = signal.process.poll()
695 content = signal.loader.stdout_buffer
696 data['foundpreview'] = True
697 if exit == 0:
698 data[(width, height)] = content
699 elif exit == 3:
700 data[(-1, height)] = content
701 elif exit == 4:
702 data[(width, -1)] = content
703 elif exit == 5:
704 data[(-1, -1)] = content
705 elif exit == 1:
706 data[(-1, -1)] = None
707 data['foundpreview'] = False
708 elif exit == 2:
709 f = codecs.open(path, 'r', errors='ignore')
710 try:
711 data[(-1, -1)] = f.read(1024 * 32)
712 except UnicodeDecodeError:
713 f.close()
714 f = codecs.open(path, 'r', encoding='latin-1',
715 errors='ignore')
716 data[(-1, -1)] = f.read(1024 * 32)
717 f.close()
718 else:
719 data[(-1, -1)] = None
720 if self.env.cf.realpath == path:
721 self.ui.browser.need_redraw = True
722 data['loading'] = False
723 pager = self.ui.browser.pager
724 if self.env.cf and self.env.cf.is_file:
725 pager.set_source(self.env.cf.get_preview_source(
726 pager.wid, pager.hei))
727 def on_destroy(signal):
728 try:
729 del self.previews[path]
730 except:
731 pass
732 loadable.signal_bind('after', on_after)
733 loadable.signal_bind('destroy', on_destroy)
734 self.loader.add(loadable)
735 return None
736 else:
737 return found
738 else:
739 try:
740 return codecs.open(path, 'r', errors='ignore')
741 except:
742 return None
744 # --------------------------
745 # -- Tabs
746 # --------------------------
747 # This implementation of tabs is very simple and keeps track of
748 # directory paths only.
750 def tab_open(self, name, path=None):
751 do_emit_signal = name != self.current_tab
752 self.current_tab = name
753 if path or (name in self.tabs):
754 self.enter_dir(path or self.tabs[name])
755 else:
756 self._update_current_tab()
757 if do_emit_signal:
758 self.signal_emit('tab.change')
760 def tab_close(self, name=None):
761 if name is None:
762 name = self.current_tab
763 if name == self.current_tab:
764 direction = -1 if name == self._get_tab_list()[-1] else 1
765 previous = self.current_tab
766 self.tab_move(direction)
767 if previous == self.current_tab:
768 return # can't close last tab
769 if name in self.tabs:
770 del self.tabs[name]
772 def tab_move(self, offset):
773 assert isinstance(offset, int)
774 tablist = self._get_tab_list()
775 current_index = tablist.index(self.current_tab)
776 newtab = tablist[(current_index + offset) % len(tablist)]
777 if newtab != self.current_tab:
778 self.tab_open(newtab)
780 def tab_new(self, path=None):
781 for i in range(1, 10):
782 if not i in self.tabs:
783 self.tab_open(i, path)
784 break
786 def _get_tab_list(self):
787 assert len(self.tabs) > 0, "There must be >=1 tabs at all times"
788 return sorted(self.tabs)
790 def _update_current_tab(self):
791 self.tabs[self.current_tab] = self.env.cwd.path
793 # --------------------------
794 # -- Overview of internals
795 # --------------------------
797 def dump_keybindings(self, *contexts):
798 if not contexts:
799 contexts = 'browser', 'console', 'pager', 'taskview'
801 temporary_file = tempfile.NamedTemporaryFile()
802 def write(string):
803 temporary_file.write(string.encode('utf-8'))
805 def recurse(before, pointer):
806 for key, value in pointer.items():
807 keys = before + [key]
808 if isinstance(value, dict):
809 recurse(keys, value)
810 else:
811 write("%12s %s\n" % (construct_keybinding(keys), value))
813 for context in contexts:
814 write("Keybindings in `%s'\n" % context)
815 if context in self.env.keymaps:
816 recurse([], self.env.keymaps[context])
817 else:
818 write(" None\n")
819 write("\n")
821 temporary_file.flush()
822 self.run(app='pager', files=[File(temporary_file.name)])
824 def dump_commands(self):
825 temporary_file = tempfile.NamedTemporaryFile()
826 def write(string):
827 temporary_file.write(string.encode('utf-8'))
829 undocumented = []
830 for cmd_name in sorted(self.commands.commands):
831 cmd = self.commands.commands[cmd_name]
832 if hasattr(cmd, '__doc__') and cmd.__doc__:
833 write(cleandoc(cmd.__doc__))
834 write("\n\n" + "-" * 60 + "\n")
835 else:
836 undocumented.append(cmd)
838 if undocumented:
839 write("Undocumented commands:\n\n")
840 for cmd in undocumented:
841 write(" :%s\n" % cmd.get_name())
843 temporary_file.flush()
844 self.run(app='pager', files=[File(temporary_file.name)])
846 def dump_settings(self):
847 from ranger.container.settingobject import ALLOWED_SETTINGS
848 temporary_file = tempfile.NamedTemporaryFile()
849 def write(string):
850 temporary_file.write(string.encode('utf-8'))
852 for setting in sorted(ALLOWED_SETTINGS):
853 write("%30s = %s\n" % (setting, getattr(self.settings, setting)))
855 temporary_file.flush()
856 self.run(app='pager', files=[File(temporary_file.name)])
858 # --------------------------
859 # -- File System Operations
860 # --------------------------
862 def uncut(self):
863 self.env.copy = set()
864 self.env.cut = False
865 self.ui.browser.main_column.request_redraw()
867 def copy(self, mode='set', narg=None, dirarg=None):
868 """Copy the selected items. Modes are: 'set', 'add', 'remove'."""
869 assert mode in ('set', 'add', 'remove')
870 cwd = self.env.cwd
871 if not narg and not dirarg:
872 selected = (f for f in self.env.get_selection() if f in cwd.files)
873 else:
874 if not dirarg and narg:
875 direction = Direction(down=1)
876 offset = 0
877 else:
878 direction = Direction(dirarg)
879 offset = 1
880 pos, selected = direction.select(
881 override=narg, lst=cwd.files, current=cwd.pointer,
882 pagesize=self.env.termsize[0], offset=offset)
883 cwd.pointer = pos
884 cwd.correct_pointer()
885 if mode == 'set':
886 self.env.copy = set(selected)
887 elif mode == 'add':
888 self.env.copy.update(set(selected))
889 elif mode == 'remove':
890 self.env.copy.difference_update(set(selected))
891 self.env.cut = False
892 self.ui.browser.main_column.request_redraw()
894 def cut(self, mode='set', narg=None, dirarg=None):
895 self.copy(mode=mode, narg=narg, dirarg=dirarg)
896 self.env.cut = True
897 self.ui.browser.main_column.request_redraw()
899 def paste_symlink(self, relative=False):
900 copied_files = self.env.copy
901 for f in copied_files:
902 try:
903 if relative:
904 relative_symlink(f.path, join(getcwd(), f.basename))
905 else:
906 symlink(f.path, join(getcwd(), f.basename))
907 except Exception as x:
908 self.notify(x)
910 def paste_hardlink(self):
911 for f in self.env.copy:
912 try:
913 link(f.path, join(getcwd(), f.basename))
914 except Exception as x:
915 self.notify(x)
917 def paste(self, overwrite=False):
918 """Paste the selected items into the current directory"""
919 copied_files = tuple(self.env.copy)
921 if not copied_files:
922 return
924 def refresh(_):
925 cwd = self.env.get_directory(original_path)
926 cwd.load_content()
928 cwd = self.env.cwd
929 original_path = cwd.path
930 one_file = copied_files[0]
931 if overwrite:
932 cp_flags = ['-af', '--']
933 mv_flags = ['-f', '--']
934 else:
935 cp_flags = ['--backup=numbered', '-a', '--']
936 mv_flags = ['--backup=numbered', '--']
938 if self.env.cut:
939 self.env.copy.clear()
940 self.env.cut = False
941 if len(copied_files) == 1:
942 descr = "moving: " + one_file.path
943 else:
944 descr = "moving files from: " + one_file.dirname
945 obj = CommandLoader(args=['mv'] + mv_flags \
946 + [f.path for f in copied_files] \
947 + [cwd.path], descr=descr)
948 else:
949 if len(copied_files) == 1:
950 descr = "copying: " + one_file.path
951 else:
952 descr = "copying files from: " + one_file.dirname
953 if not overwrite and len(copied_files) == 1 \
954 and one_file.dirname == cwd.path:
955 # Special case: yypp
956 # copying a file onto itself -> create a backup
957 obj = CommandLoader(args=['cp', '-f'] + cp_flags \
958 + [one_file.path, one_file.path], descr=descr)
959 else:
960 obj = CommandLoader(args=['cp'] + cp_flags \
961 + [f.path for f in copied_files] \
962 + [cwd.path], descr=descr)
964 obj.signal_bind('after', refresh)
965 self.loader.add(obj)
967 def delete(self):
968 # XXX: warn when deleting mount points/unseen marked files?
969 self.notify("Deleting!")
970 selected = self.env.get_selection()
971 self.env.copy -= set(selected)
972 if selected:
973 for f in selected:
974 if isdir(f.path) and not os.path.islink(f.path):
975 try:
976 shutil.rmtree(f.path)
977 except OSError as err:
978 self.notify(err)
979 else:
980 try:
981 os.remove(f.path)
982 except OSError as err:
983 self.notify(err)
984 self.env.ensure_correct_pointer()
986 def mkdir(self, name):
987 try:
988 os.mkdir(os.path.join(self.env.cwd.path, name))
989 except OSError as err:
990 self.notify(err)
992 def rename(self, src, dest):
993 if hasattr(src, 'path'):
994 src = src.path
996 try:
997 os.renames(src, dest)
998 except OSError as err:
999 self.notify(err)