def __init__(self, win): Widget.__init__(self, win) self.pos = 0 self.line = '' self.history = History(self.settings.max_console_history_size) # load history from files if not ranger.args.clean: self.historypath = self.fm.datapath('history') if os.path.exists(self.historypath): try: fobj = open(self.historypath, 'r') except OSError as ex: self.fm.notify('Failed to read history file', bad=True, exception=ex) else: try: for line in fobj: self.history.add(line[:-1]) except UnicodeDecodeError as ex: self.fm.notify('Failed to parse corrupt history file', bad=True, exception=ex) fobj.close() self.history_backup = History(self.history) # NOTE: the console is considered in the "question mode" when the # question_queue is non-empty. In that case, the console will draw the # question instead of the regular console, and the input you give is # used to answer the question instead of typing in commands. # # A question is a tuple of (question_string, callback_func, # tuple_of_choices). callback_func is a function that is called when # the question is answered which gets the answer as an argument. # tuple_of_choices looks like ('y', 'n'). Only one-letter-answers are # currently supported. Pressing enter uses the first choice whereas # pressing ESC uses the second choice. self.question_queue = []
def __init__(self, win): Widget.__init__(self, win) self.clear() self.history = History(self.settings.max_console_history_size) # load history from files if not ranger.arg.clean: self.historypath = self.fm.confpath('history') try: f = open(self.historypath, 'r') except: pass else: for line in f: self.history.add(line[:-1]) f.close() self.line = "" self.history_backup = History(self.history) # NOTE: the console is considered in the "question mode" when the # question_queue is non-empty. In that case, the console will draw the # question instead of the regular console, and the input you give is # used to answer the question instead of typing in commands. # # A question is a tuple of (question_string, callback_func, # tuple_of_choices). callback_func is a function that is called when # the question is answered which gets the answer as an argument. # tuple_of_choices looks like ('y', 'n'). Only one-letter-answers are # currently supported. Pressing enter uses the first choice whereas # pressing ESC uses the second choice. self.question_queue = []
def open(self, string='', prompt=None, position=None): if prompt is not None: assert isinstance(prompt, str) self.prompt = prompt elif 'prompt' in self.__dict__: del self.prompt if self.last_cursor_mode is None: try: self.last_cursor_mode = curses.curs_set(1) except curses.error: pass self.allow_close = False self.tab_deque = None self.unicode_buffer = "" self.line = string self.history_search_pattern = self.line self.pos = len(string) if position is not None: self.pos = min(self.pos, position) self.history_backup.fast_forward() self.history = History(self.history_backup) self.history.add('') self.wait_for_command_input = True return True
def __init__(self, path): self.thisdir = None # Current Working Directory self._thisfile = None # Current File self.history = History(self.settings.max_history_size, unique=False) self.last_search = None self.pointer = 0 self.path = abspath(expanduser(path)) self.pathway = () # NOTE: in the line below, weak=True works only in python3. In python2, # weak references are not equal to the original object when tested with # "==", and this breaks _set_thisfile_from_signal and _on_tab_change. self.fm.signal_bind('move', self._set_thisfile_from_signal, priority=0.1, weak=(sys.version > '3')) self.fm.signal_bind('tab.change', self._on_tab_change, weak=(sys.version > '3'))
def __init__(self, win): Widget.__init__(self, win) self.clear() self.history = History(self.settings.max_console_history_size) # load history from files if not ranger.arg.clean: self.historypath = self.fm.confpath('history') try: f = open(self.historypath, 'r') except: pass else: for line in f: self.history.add(line[:-1]) f.close() self.history_backup = History(self.history)
def open(self, string="", prompt=None, position=None): if prompt is not None: assert isinstance(prompt, str) self.prompt = prompt elif "prompt" in self.__dict__: del self.prompt if self.last_cursor_mode is None: try: self.last_cursor_mode = curses.curs_set(1) except: pass self.allow_close = False self.tab_deque = None self.focused = True self.visible = True self.unicode_buffer = "" self.line = string self.history_search_pattern = self.line self.pos = len(string) if position is not None: self.pos = min(self.pos, position) self.history_backup.fast_forward() self.history = History(self.history_backup) self.history.add("") return True
def __init__(self, path): self.thisdir = None # Current Working Directory self._thisfile = None # Current File self.history = History(self.settings.max_history_size, unique=False) self.last_search = None self.pointer = 0 self.path = abspath(expanduser(path)) self.pathway = () # NOTE: in the line below, weak=True works only in python3. In python2, # weak references are not equal to the original object when tested with # "==", and this breaks _set_thisfile_from_signal and _on_tab_change. self.fm.signal_bind("move", self._set_thisfile_from_signal, priority=0.1, weak=(sys.version > "3")) self.fm.signal_bind("tab.change", self._on_tab_change, weak=(sys.version > "3"))
def __init__(self, path): SignalDispatcher.__init__(self) self.path = abspath(expanduser(path)) self._cf = None self.pathway = () self.directories = {} self.keybuffer = KeyBuffer() self.keymaps = KeyMaps(self.keybuffer) self.copy = set() self.history = History(self.settings.max_history_size, unique=False) try: self.username = pwd.getpwuid(os.geteuid()).pw_name except: self.username = "******" + str(os.geteuid()) self.hostname = socket.gethostname() self.home_path = os.path.expanduser("~") self.signal_bind("move", self._set_cf_from_signal, priority=0.1, weak=True)
def add_to_history(self): self.history_backup.fast_forward() self.history_backup.add(self.line) self.history = History(self.history_backup)
class Console(Widget): # pylint: disable=too-many-instance-attributes,too-many-public-methods visible = False last_cursor_mode = None history_search_pattern = None prompt = ':' copy = '' tab_deque = None original_line = None history = None history_backup = None override = None allow_close = False historypath = None wait_for_command_input = False unicode_buffer = "" def __init__(self, win): Widget.__init__(self, win) self.pos = 0 self.line = '' self.history = History(self.settings.max_console_history_size) # load history from files if not ranger.args.clean: self.historypath = self.fm.datapath('history') if os.path.exists(self.historypath): try: fobj = open(self.historypath, 'r') except OSError as ex: self.fm.notify('Failed to read history file', bad=True, exception=ex) else: try: for line in fobj: self.history.add(line[:-1]) except UnicodeDecodeError as ex: self.fm.notify('Failed to parse corrupt history file', bad=True, exception=ex) fobj.close() self.history_backup = History(self.history) # NOTE: the console is considered in the "question mode" when the # question_queue is non-empty. In that case, the console will draw the # question instead of the regular console, and the input you give is # used to answer the question instead of typing in commands. # # A question is a tuple of (question_string, callback_func, # tuple_of_choices). callback_func is a function that is called when # the question is answered which gets the answer as an argument. # tuple_of_choices looks like ('y', 'n'). Only one-letter-answers are # currently supported. Pressing enter uses the first choice whereas # pressing ESC uses the second choice. self.question_queue = [] def destroy(self): # save history to files if ranger.args.clean or not self.settings.save_console_history: return if self.historypath: try: fobj = open(self.historypath, 'w') except OSError as ex: self.fm.notify('Failed to write history file', bad=True, exception=ex) else: for entry in self.history_backup: try: fobj.write(entry + '\n') except UnicodeEncodeError: pass fobj.close() Widget.destroy(self) def draw(self): self.win.erase() if self.question_queue: assert isinstance(self.question_queue[0], tuple) assert len(self.question_queue[0]) == 3 self.addstr(0, 0, self.question_queue[0][0][self.pos:]) return self.addstr(0, 0, self.prompt) line = WideString(self.line) overflow = -self.wid + len(self.prompt) + len(line) + 1 if overflow > 0: self.addstr(0, len(self.prompt), str(line[overflow:])) else: self.addstr(0, len(self.prompt), self.line) def finalize(self): move = self.fm.ui.win.move if self.question_queue: try: move(self.y, len(self.question_queue[0][0])) except curses.error: pass else: try: pos = uwid(self.line[0:self.pos]) + len(self.prompt) move(self.y, self.x + min(self.wid - 1, pos)) except curses.error: pass def open(self, string='', prompt=None, position=None): if prompt is not None: assert isinstance(prompt, str) self.prompt = prompt elif 'prompt' in self.__dict__: del self.prompt if self.last_cursor_mode is None: try: self.last_cursor_mode = curses.curs_set(1) except curses.error: pass self.allow_close = False self.tab_deque = None self.unicode_buffer = "" self.line = string self.history_search_pattern = self.line self.pos = len(string) if position is not None: self.pos = min(self.pos, position) self.history_backup.fast_forward() self.history = History(self.history_backup) self.history.add('') self.wait_for_command_input = True return True def close(self, trigger_cancel_function=True): if self.question_queue: question = self.question_queue[0] answers = question[2] if len(answers) >= 2: self._answer_question(answers[1]) else: self._close_command_prompt(trigger_cancel_function) def _close_command_prompt(self, trigger_cancel_function=True): if trigger_cancel_function: cmd = self._get_cmd(quiet=True) if cmd: cmd.cancel() if self.last_cursor_mode is not None: try: curses.curs_set(self.last_cursor_mode) except curses.error: pass self.last_cursor_mode = None self.fm.hide_console_info() self.add_to_history() self.tab_deque = None self.clear() self.__class__ = Console self.wait_for_command_input = False def clear(self): self.pos = 0 self.line = '' def press(self, key): self.fm.ui.keymaps.use_keymap('console') if not self.fm.ui.press(key): self.type_key(key) def _answer_question(self, answer): if not self.question_queue: return False question = self.question_queue[0] _, callback, answers = question if answer in answers: self.question_queue.pop(0) callback(answer) return True return False def type_key(self, key): self.tab_deque = None line = "" if self.question_queue else self.line result = self._add_character(key, self.unicode_buffer, line, self.pos) if result[1] == line: # line didn't change, so we don't need to do anything, just update # the unicode _buffer. self.unicode_buffer = result[0] return if self.question_queue: self.unicode_buffer, answer, _ = result self._answer_question(answer) else: self.unicode_buffer, self.line, self.pos = result self.on_line_change() def _add_character(self, key, unicode_buffer, line, pos): # Takes the pressed key, a string "unicode_buffer" containing a # potentially incomplete unicode character, the current line and the # position of the cursor inside the line. # This function returns the new unicode buffer, the modified line and # position. if isinstance(key, int): try: key = chr(key) except ValueError: return unicode_buffer, line, pos if self.fm.py3: if len(unicode_buffer) >= 4: unicode_buffer = "" if ord(key) in range(0, 256): unicode_buffer += key try: decoded = unicode_buffer.encode("latin-1").decode("utf-8") except UnicodeDecodeError: return unicode_buffer, line, pos except UnicodeEncodeError: return unicode_buffer, line, pos else: unicode_buffer = "" if pos == len(line): line += decoded else: line = line[:pos] + decoded + line[pos:] pos += len(decoded) else: if pos == len(line): line += key else: line = line[:pos] + key + line[pos:] pos += len(key) return unicode_buffer, line, pos def history_move(self, n): try: current = self.history.current() except HistoryEmptyException: pass else: if self.line != current and self.line != self.history.top(): self.history.modify(self.line) if self.history_search_pattern: self.history.search(self.history_search_pattern, n) else: self.history.move(n) current = self.history.current() if self.line != current: self.line = self.history.current() self.pos = len(self.line) def add_to_history(self): self.history_backup.fast_forward() self.history_backup.add(self.line) self.history = History(self.history_backup) def move(self, **keywords): direction = Direction(keywords) if direction.horizontal(): # Ensure that the pointer is moved utf-char-wise if self.fm.py3: if self.question_queue: umax = len(self.question_queue[0][0]) + 1 - self.wid else: umax = len(self.line) + 1 self.pos = direction.move( direction=direction.right(), minimum=0, maximum=umax, current=self.pos) else: if self.question_queue: uchar = list(self.question_queue[0][0].decode('utf-8', 'ignore')) upos = len(self.question_queue[0][0][:self.pos].decode('utf-8', 'ignore')) umax = len(uchar) + 1 - self.wid else: uchar = list(self.line.decode('utf-8', 'ignore')) upos = len(self.line[:self.pos].decode('utf-8', 'ignore')) umax = len(uchar) + 1 newupos = direction.move( direction=direction.right(), minimum=0, maximum=umax, current=upos) self.pos = len(''.join(uchar[:newupos]).encode('utf-8', 'ignore')) def move_word(self, **keywords): direction = Direction(keywords) if direction.horizontal(): self.pos = self.move_by_word(self.line, self.pos, direction.right()) self.on_line_change() @staticmethod def move_by_word(line, position, direction): """ Returns a new position by moving word-wise in the line >>> import sys >>> if sys.version_info < (3, ): ... # Didn't get the unicode test to work on python2, even though ... # it works fine in ranger, even with unicode input... ... line = "ohai world, this is dog" ... else: ... line = "\\u30AA\\u30CF\\u30E8\\u30A6 world, this is dog" >>> Console.move_by_word(line, 0, -1) 0 >>> Console.move_by_word(line, 0, 1) 5 >>> Console.move_by_word(line, 2, -1) 0 >>> Console.move_by_word(line, 2, 1) 5 >>> Console.move_by_word(line, 15, -2) 5 >>> Console.move_by_word(line, 15, 2) 21 >>> Console.move_by_word(line, 24, -1) 21 >>> Console.move_by_word(line, 24, 1) 24 """ word_beginnings = [] seen_whitespace = True current_word = None cursor_inside_word = False # Scan the line for word boundaries and determine position of cursor for i, char in enumerate(line): if i == position: current_word = len(word_beginnings) if not seen_whitespace: cursor_inside_word = True if char == " ": seen_whitespace = True elif seen_whitespace: seen_whitespace = False word_beginnings.append(i) word_beginnings.append(len(line)) # Handle corner cases: if current_word is None: current_word = len(word_beginnings) if direction > 0 and cursor_inside_word: current_word -= 1 if direction < 0 and position == len(line): current_word -= 1 new_word = current_word + direction new_word = max(0, min(len(word_beginnings) - 1, new_word)) return word_beginnings[new_word] def delete_rest(self, direction): self.tab_deque = None if direction > 0: self.copy = self.line[self.pos:] self.line = self.line[:self.pos] else: self.copy = self.line[:self.pos] self.line = self.line[self.pos:] self.pos = 0 self.on_line_change() def paste(self): if self.pos == len(self.line): self.line += self.copy else: self.line = self.line[:self.pos] + self.copy + self.line[self.pos:] self.pos += len(self.copy) self.on_line_change() def delete_word(self, backward=True): if self.line: self.tab_deque = None if backward: right_part = self.line[self.pos:] i = self.pos - 2 while i >= 0 and re.match( r'[\w\d]', self.line[i], re.UNICODE): # pylint: disable=no-member i -= 1 self.copy = self.line[i + 1:self.pos] self.line = self.line[:i + 1] + right_part self.pos = i + 1 else: left_part = self.line[:self.pos] i = self.pos + 1 while i < len(self.line) and re.match( r'[\w\d]', self.line[i], re.UNICODE): # pylint: disable=no-member i += 1 self.copy = self.line[self.pos:i] if i >= len(self.line): self.line = left_part self.pos = len(self.line) else: self.line = left_part + self.line[i:] self.pos = len(left_part) self.on_line_change() def delete(self, mod): self.tab_deque = None if mod == -1 and self.pos == 0: if not self.line: self.close(trigger_cancel_function=False) return # Delete utf-char-wise if self.fm.py3: left_part = self.line[:self.pos + mod] self.pos = len(left_part) self.line = left_part + self.line[self.pos + 1:] else: uchar = list(self.line.decode('utf-8', 'ignore')) upos = len(self.line[:self.pos].decode('utf-8', 'ignore')) + mod left_part = ''.join(uchar[:upos]).encode('utf-8', 'ignore') self.pos = len(left_part) self.line = left_part + ''.join(uchar[upos + 1:]).encode('utf-8', 'ignore') self.on_line_change() def execute(self, cmd=None): if self.question_queue and cmd is None: question = self.question_queue[0] answers = question[2] if len(answers) >= 1: self._answer_question(answers[0]) else: self.question_queue.pop(0) return self.allow_close = True if cmd: cmd.execute() else: self.fm.execute_console(self.line) if self.allow_close: self._close_command_prompt(trigger_cancel_function=False) def _get_cmd(self, quiet=False): try: command_class = self.get_cmd_class() except IndexError: return None except KeyError: if not quiet: self.fm.notify("Command not found: `%s'" % self.line.split()[0], bad=True) return None return command_class(self.line) def get_cmd_class(self): return self.fm.commands.get_command(self.line.split()[0], abbrev=True) def _get_tab(self, tabnum): if ' ' in self.line: cmd = self._get_cmd() if cmd: return cmd.tab(tabnum) return None return self.fm.commands.command_generator(self.line) def tab(self, tabnum=1): if self.tab_deque is None: tab_result = self._get_tab(tabnum) if tab_result is None: pass elif isinstance(tab_result, str): self.line = tab_result self.pos = len(tab_result) self.on_line_change() elif hasattr(tab_result, '__iter__'): self.tab_deque = deque(tab_result) self.tab_deque.appendleft(self.line) if self.tab_deque is not None: self.tab_deque.rotate(-tabnum) self.line = self.tab_deque[0] self.pos = len(self.line) self.on_line_change() def on_line_change(self): self.history_search_pattern = self.line try: cls = self.get_cmd_class() except (KeyError, ValueError, IndexError): pass else: cmd = cls(self.line) if cmd and cmd.quick(): cmd.quickly_executed = True self.execute(cmd) def ask(self, text, callback, choices=None): """Open a question prompt with predefined choices The "text" is displayed as the question text and should include a list of possible keys that the user can type. The "callback" is a function that is called when the question is answered. It only gets the answer as an argument. "choices" is a tuple of one-letter strings that can be typed in by the user. Every other input gets ignored, except <Enter> and <ESC>. The first choice is used when the user presses <Enter>, the second choice is used when the user presses <ESC>. """ self.question_queue.append( (text, callback, choices if choices is not None else ['y', 'n']))
class Tab(FileManagerAware, SettingsAware): def __init__(self, path): self.thisdir = None # Current Working Directory self._thisfile = None # Current File self.history = History(self.settings.max_history_size, unique=False) self.last_search = None self.pointer = 0 self.path = abspath(expanduser(path)) self.pathway = () # NOTE: in the line below, weak=True works only in python3. In python2, # weak references are not equal to the original object when tested with # "==", and this breaks _set_thisfile_from_signal and _on_tab_change. self.fm.signal_bind('move', self._set_thisfile_from_signal, priority=0.1, weak=(sys.version > '3')) self.fm.signal_bind('tab.change', self._on_tab_change, weak=(sys.version > '3')) def _set_thisfile_from_signal(self, signal): if self == signal.tab: self._thisfile = signal.new if self == self.fm.thistab: self.pointer = self.thisdir.pointer def _on_tab_change(self, signal): if self == signal.new and self.thisdir: # restore the pointer whenever this tab is reopened self.thisdir.pointer = self.pointer self.thisdir.correct_pointer() def _set_thisfile(self, value): if value is not self._thisfile: previous = self._thisfile self.fm.signal_emit('move', previous=previous, new=value, tab=self) def _get_thisfile(self): return self._thisfile thisfile = property(_get_thisfile, _set_thisfile) def at_level(self, level): """Returns the FileSystemObject at the given level. level >0 => previews level 0 => current file/directory level <0 => parent directories """ if level <= 0: try: return self.pathway[level - 1] except IndexError: return None else: directory = self.thisdir for i in range(level): if directory is None: return None if directory.is_directory: directory = directory.pointed_obj else: return None return directory def get_selection(self): if self.thisdir: if self.thisdir.marked_items: return self.thisdir.get_selection() elif self._thisfile: return [self._thisfile] return [] def assign_cursor_positions_for_subdirs(self): """Assign correct cursor positions for subdirectories""" last_path = None for path in reversed(self.pathway): if last_path is None: last_path = path continue path.move_to_obj(last_path) last_path = path def ensure_correct_pointer(self): if self.thisdir: self.thisdir.correct_pointer() def history_go(self, relative): """Move relative in history""" if self.history: self.history.move(relative).go(history=False) def inherit_history(self, other_history): self.history.rebase(other_history) def enter_dir(self, path, history = True): """Enter given path""" # TODO: Ensure that there is always a self.thisdir if path is None: return path = str(path) previous = self.thisdir # get the absolute path path = normpath(join(self.path, expanduser(path))) if not isdir(path): return False new_thisdir = self.fm.get_directory(path) try: os.chdir(path) except: return True self.path = path self.thisdir = new_thisdir self.thisdir.load_content_if_outdated() # build the pathway, a tuple of directory objects which lie # on the path to the current directory. if path == '/': self.pathway = (self.fm.get_directory('/'), ) else: pathway = [] currentpath = '/' for dir in path.split('/'): currentpath = join(currentpath, dir) pathway.append(self.fm.get_directory(currentpath)) self.pathway = tuple(pathway) self.assign_cursor_positions_for_subdirs() # set the current file. self.thisdir.sort_directories_first = self.fm.settings.sort_directories_first self.thisdir.sort_reverse = self.fm.settings.sort_reverse self.thisdir.sort_if_outdated() if previous and previous.path != path: self.thisfile = self.thisdir.pointed_obj else: # This avoids setting self.pointer (through the 'move' signal) and # is required so that you can use enter_dir when switching tabs # without messing up the pointer. self._thisfile = self.thisdir.pointed_obj if history: self.history.add(new_thisdir) self.fm.signal_emit('cd', previous=previous, new=self.thisdir) return True def __repr__(self): return "<Tab '%s'>" % self.thisdir
class Console(Widget): visible = False last_cursor_mode = None history_search_pattern = None prompt = ':' copy = '' tab_deque = None original_line = None history = None history_backup = None override = None allow_close = False historypath = None def __init__(self, win): Widget.__init__(self, win) self.clear() self.history = History(self.settings.max_console_history_size) # load history from files if not ranger.arg.clean: self.historypath = self.fm.confpath('history') try: f = open(self.historypath, 'r') except: pass else: for line in f: self.history.add(line[:-1]) f.close() self.history_backup = History(self.history) def destroy(self): # save history to files if ranger.arg.clean or not self.settings.save_console_history: return if self.historypath: try: f = open(self.historypath, 'w') except: pass else: for entry in self.history_backup: f.write(entry + '\n') f.close() def draw(self): self.win.erase() self.addstr(0, 0, self.prompt) line = WideString(self.line) overflow = -self.wid + len(self.prompt) + len(line) + 1 if overflow > 0: self.addstr(str(line[overflow:])) else: self.addstr(self.line) def finalize(self): try: pos = uwid(self.line[0:self.pos]) + len(self.prompt) self.fm.ui.win.move(self.y, self.x + min(self.wid-1, pos)) except: pass def open(self, string='', prompt=None, position=None): if prompt is not None: assert isinstance(prompt, str) self.prompt = prompt elif 'prompt' in self.__dict__: del self.prompt if self.last_cursor_mode is None: try: self.last_cursor_mode = curses.curs_set(1) except: pass self.allow_close = False self.tab_deque = None self.focused = True self.visible = True self.unicode_buffer = "" self.line = string self.history_search_pattern = self.line self.pos = len(string) if position is not None: self.pos = min(self.pos, position) self.history_backup.fast_forward() self.history = History(self.history_backup) self.history.add('') return True def close(self, trigger_cancel_function=True): if trigger_cancel_function: cmd = self._get_cmd(quiet=True) if cmd: try: cmd.cancel() except Exception as error: self.fm.notify(error) if self.last_cursor_mode is not None: try: curses.curs_set(self.last_cursor_mode) except: pass self.last_cursor_mode = None self.add_to_history() self.tab_deque = None self.clear() self.__class__ = Console self.focused = False self.visible = False if hasattr(self, 'on_close'): self.on_close() def clear(self): self.pos = 0 self.line = '' def press(self, key): self.env.keymaps.use_keymap('console') if not self.fm.ui.press(key): self.type_key(key) def type_key(self, key): self.tab_deque = None if isinstance(key, int): try: key = chr(key) except ValueError: return if self.fm.py3: self.unicode_buffer += key try: decoded = self.unicode_buffer.encode("latin-1").decode("utf-8") except UnicodeDecodeError: return except UnicodeEncodeError: return else: self.unicode_buffer = "" if self.pos == len(self.line): self.line += decoded else: pos = self.pos self.line = self.line[:pos] + decoded + self.line[pos:] self.pos += len(decoded) else: if self.pos == len(self.line): self.line += key else: self.line = self.line[:self.pos] + key + self.line[self.pos:] self.pos += len(key) self.on_line_change() def history_move(self, n): try: current = self.history.current() except HistoryEmptyException: pass else: if self.line != current and self.line != self.history.top(): self.history.modify(self.line) if self.history_search_pattern: self.history.search(self.history_search_pattern, n) else: self.history.move(n) current = self.history.current() if self.line != current: self.line = self.history.current() self.pos = len(self.line) def add_to_history(self): self.history_backup.fast_forward() self.history_backup.add(self.line) self.history = History(self.history_backup) def move(self, **keywords): direction = Direction(keywords) if direction.horizontal(): # Ensure that the pointer is moved utf-char-wise if self.fm.py3: self.pos = direction.move( direction=direction.right(), minimum=0, maximum=len(self.line) + 1, current=self.pos) else: if self.fm.py3: uc = list(self.line) upos = len(self.line[:self.pos]) else: uc = list(self.line.decode('utf-8', 'ignore')) upos = len(self.line[:self.pos].decode('utf-8', 'ignore')) newupos = direction.move( direction=direction.right(), minimum=0, maximum=len(uc) + 1, current=upos) self.pos = len(''.join(uc[:newupos]).encode('utf-8', 'ignore')) def delete_rest(self, direction): self.tab_deque = None if direction > 0: self.copy = self.line[self.pos:] self.line = self.line[:self.pos] else: self.copy = self.line[:self.pos] self.line = self.line[self.pos:] self.pos = 0 self.on_line_change() def paste(self): if self.pos == len(self.line): self.line += self.copy else: self.line = self.line[:self.pos] + self.copy + self.line[self.pos:] self.pos += len(self.copy) self.on_line_change() def delete_word(self, backward=True): if self.line: self.tab_deque = None if backward: right_part = self.line[self.pos:] i = self.pos - 2 while i >= 0 and re.match(r'[\w\d]', self.line[i], re.U): i -= 1 self.copy = self.line[i + 1:self.pos] self.line = self.line[:i + 1] + right_part self.pos = i + 1 else: left_part = self.line[:self.pos] i = self.pos + 1 while i < len(self.line) and re.match(r'[\w\d]', self.line[i], re.U): i += 1 self.copy = self.line[self.pos:i] if i >= len(self.line): self.line = left_part self.pos = len(self.line) else: self.line = left_part + self.line[i:] self.pos = len(left_part) self.on_line_change() def delete(self, mod): self.tab_deque = None if mod == -1 and self.pos == 0: if not self.line: self.close(trigger_cancel_function=False) return # Delete utf-char-wise if self.fm.py3: left_part = self.line[:self.pos + mod] self.pos = len(left_part) self.line = left_part + self.line[self.pos + 1:] else: uc = list(self.line.decode('utf-8', 'ignore')) upos = len(self.line[:self.pos].decode('utf-8', 'ignore')) + mod left_part = ''.join(uc[:upos]).encode('utf-8', 'ignore') self.pos = len(left_part) self.line = left_part + ''.join(uc[upos+1:]).encode('utf-8', 'ignore') self.on_line_change() def execute(self, cmd=None): self.allow_close = True self.fm.execute_console(self.line) if self.allow_close: self.close(trigger_cancel_function=False) def _get_cmd(self, quiet=False): try: command_class = self._get_cmd_class() except KeyError: if not quiet: error = "Command not found: `%s'" % self.line.split()[0] self.fm.notify(error, bad=True) except: return None else: return command_class(self.line) def _get_cmd_class(self): return self.fm.commands.get_command(self.line.split()[0]) def _get_tab(self): if ' ' in self.line: cmd = self._get_cmd() if cmd: return cmd.tab() else: return None return self.fm.commands.command_generator(self.line) def tab(self, n=1): if self.tab_deque is None: tab_result = self._get_tab() if isinstance(tab_result, str): self.line = tab_result self.pos = len(tab_result) self.on_line_change() elif tab_result == None: pass elif hasattr(tab_result, '__iter__'): self.tab_deque = deque(tab_result) self.tab_deque.appendleft(self.line) if self.tab_deque is not None: self.tab_deque.rotate(-n) self.line = self.tab_deque[0] self.pos = len(self.line) self.on_line_change() def on_line_change(self): self.history_search_pattern = self.line try: cls = self._get_cmd_class() except (KeyError, ValueError, IndexError): pass else: cmd = cls(self.line) if cmd and cmd.quick(): self.execute(cmd)
class Console(Widget): visible = False last_cursor_mode = None history_search_pattern = None prompt = ':' copy = '' tab_deque = None original_line = None history = None history_backup = None override = None allow_close = False historypath = None wait_for_command_input = False unicode_buffer = "" def __init__(self, win): Widget.__init__(self, win) self.clear() self.history = History(self.settings.max_console_history_size) # load history from files if not ranger.arg.clean: self.historypath = self.fm.confpath('history') try: f = open(self.historypath, 'r') except: pass else: for line in f: self.history.add(line[:-1]) f.close() self.line = "" self.history_backup = History(self.history) # NOTE: the console is considered in the "question mode" when the # question_queue is non-empty. In that case, the console will draw the # question instead of the regular console, and the input you give is # used to answer the question instead of typing in commands. # # A question is a tuple of (question_string, callback_func, # tuple_of_choices). callback_func is a function that is called when # the question is answered which gets the answer as an argument. # tuple_of_choices looks like ('y', 'n'). Only one-letter-answers are # currently supported. Pressing enter uses the first choice whereas # pressing ESC uses the second choice. self.question_queue = [] def destroy(self): # save history to files if ranger.arg.clean or not self.settings.save_console_history: return if self.historypath: try: f = open(self.historypath, 'w') except: pass else: for entry in self.history_backup: try: f.write(entry + '\n') except UnicodeEncodeError: pass f.close() def draw(self): self.win.erase() if self.question_queue: assert isinstance(self.question_queue[0], tuple) assert len(self.question_queue[0]) == 3 self.addstr(0, 0, self.question_queue[0][0]) return self.addstr(0, 0, self.prompt) line = WideString(self.line) overflow = -self.wid + len(self.prompt) + len(line) + 1 if overflow > 0: self.addstr(0, len(self.prompt), str(line[overflow:])) else: self.addstr(0, len(self.prompt), self.line) def finalize(self): move = self.fm.ui.win.move if self.question_queue: try: move(self.y, len(self.question_queue[0][0])) except: pass else: try: pos = uwid(self.line[0:self.pos]) + len(self.prompt) move(self.y, self.x + min(self.wid-1, pos)) except: pass def open(self, string='', prompt=None, position=None): if prompt is not None: assert isinstance(prompt, str) self.prompt = prompt elif 'prompt' in self.__dict__: del self.prompt if self.last_cursor_mode is None: try: self.last_cursor_mode = curses.curs_set(1) except: pass self.allow_close = False self.tab_deque = None self.unicode_buffer = "" self.line = string self.history_search_pattern = self.line self.pos = len(string) if position is not None: self.pos = min(self.pos, position) self.history_backup.fast_forward() self.history = History(self.history_backup) self.history.add('') self.wait_for_command_input = True self.on_line_change() return True def close(self, trigger_cancel_function=True): if self.question_queue: question = self.question_queue[0] answers = question[2] if len(answers) >= 2: self._answer_question(answers[1]) else: self._close_command_prompt(trigger_cancel_function) def _close_command_prompt(self, trigger_cancel_function=True): if trigger_cancel_function: cmd = self._get_cmd(quiet=True) if cmd: try: cmd.cancel() except Exception as error: self.fm.notify(error) if self.last_cursor_mode is not None: try: curses.curs_set(self.last_cursor_mode) except: pass self.last_cursor_mode = None self.fm.hide_console_info() self.add_to_history() self.tab_deque = None self.clear() self.__class__ = Console self.wait_for_command_input = False def clear(self): self.pos = 0 self.line = '' def press(self, key): if not self.fm.ui.quick_jump.press(key): self.fm.ui.keymaps.use_keymap('console') if not self.fm.ui.press(key): self.type_key(key) def _answer_question(self, answer): if not self.question_queue: return False question = self.question_queue[0] text, callback, answers = question if answer in answers: self.question_queue.pop(0) callback(answer) return True return False def type_key(self, key): self.tab_deque = None line = "" if self.question_queue else self.line result = self._add_character(key, self.unicode_buffer, line, self.pos) if result[1] == line: # line didn't change, so we don't need to do anything, just update # the unicode _buffer. self.unicode_buffer = result[0] return if self.question_queue: self.unicode_buffer, answer, self.pos = result self._answer_question(answer) else: self.unicode_buffer, self.line, self.pos = result self.on_line_change() def _add_character(self, key, unicode_buffer, line, pos): # Takes the pressed key, a string "unicode_buffer" containing a # potentially incomplete unicode character, the current line and the # position of the cursor inside the line. # This function returns the new unicode buffer, the modified line and # position. if isinstance(key, int): try: key = chr(key) except ValueError: return unicode_buffer, line, pos if self.fm.py3: if len(unicode_buffer) >= 4: unicode_buffer = "" if ord(key) in range(0, 256): unicode_buffer += key try: decoded = unicode_buffer.encode("latin-1").decode("utf-8") except UnicodeDecodeError: return unicode_buffer, line, pos except UnicodeEncodeError: return unicode_buffer, line, pos else: unicode_buffer = "" if pos == len(line): line += decoded else: line = line[:pos] + decoded + line[pos:] pos += len(decoded) else: if pos == len(line): line += key else: line = line[:pos] + key + line[pos:] pos += len(key) return unicode_buffer, line, pos def history_move(self, n): try: current = self.history.current() except HistoryEmptyException: pass else: if self.line != current and self.line != self.history.top(): self.history.modify(self.line) if self.history_search_pattern: self.history.search(self.history_search_pattern, n) else: self.history.move(n) current = self.history.current() if self.line != current: self.line = self.history.current() self.pos = len(self.line) def add_to_history(self): self.history_backup.fast_forward() self.history_backup.add(self.line) self.history = History(self.history_backup) def move(self, **keywords): direction = Direction(keywords) if direction.horizontal(): # Ensure that the pointer is moved utf-char-wise if self.fm.py3: self.pos = direction.move( direction=direction.right(), minimum=0, maximum=len(self.line) + 1, current=self.pos) else: if self.fm.py3: uc = list(self.line) upos = len(self.line[:self.pos]) else: uc = list(self.line.decode('utf-8', 'ignore')) upos = len(self.line[:self.pos].decode('utf-8', 'ignore')) newupos = direction.move( direction=direction.right(), minimum=0, maximum=len(uc) + 1, current=upos) self.pos = len(''.join(uc[:newupos]).encode('utf-8', 'ignore')) def delete_rest(self, direction): self.tab_deque = None if direction > 0: self.copy = self.line[self.pos:] self.line = self.line[:self.pos] else: self.copy = self.line[:self.pos] self.line = self.line[self.pos:] self.pos = 0 self.on_line_change() def paste(self): if self.pos == len(self.line): self.line += self.copy else: self.line = self.line[:self.pos] + self.copy + self.line[self.pos:] self.pos += len(self.copy) self.on_line_change() def delete_word(self, backward=True): if self.line: self.tab_deque = None if backward: right_part = self.line[self.pos:] i = self.pos - 2 while i >= 0 and re.match(r'[\w\d]', self.line[i], re.U): i -= 1 self.copy = self.line[i + 1:self.pos] self.line = self.line[:i + 1] + right_part self.pos = i + 1 else: left_part = self.line[:self.pos] i = self.pos + 1 while i < len(self.line) and re.match(r'[\w\d]', self.line[i], re.U): i += 1 self.copy = self.line[self.pos:i] if i >= len(self.line): self.line = left_part self.pos = len(self.line) else: self.line = left_part + self.line[i:] self.pos = len(left_part) self.on_line_change() def delete(self, mod): self.tab_deque = None if mod == -1 and self.pos == 0: if not self.line: self.close(trigger_cancel_function=False) return # Delete utf-char-wise if self.fm.py3: left_part = self.line[:self.pos + mod] self.pos = len(left_part) self.line = left_part + self.line[self.pos + 1:] else: uc = list(self.line.decode('utf-8', 'ignore')) upos = len(self.line[:self.pos].decode('utf-8', 'ignore')) + mod left_part = ''.join(uc[:upos]).encode('utf-8', 'ignore') self.pos = len(left_part) self.line = left_part + ''.join(uc[upos+1:]).encode('utf-8', 'ignore') self.on_line_change() def execute(self, cmd=None): if self.question_queue and cmd is None: question = self.question_queue[0] answers = question[2] if len(answers) >= 1: self._answer_question(answers[0]) else: self.question_queue.pop(0) return self.allow_close = True self.fm.execute_console(self.line) if self.allow_close: self._close_command_prompt(trigger_cancel_function=False) def _get_cmd(self, quiet=False): try: command_class = self._get_cmd_class() except KeyError: if not quiet: error = "Command not found: `%s'" % self.line.split()[0] self.fm.notify(error, bad=True) except: return None else: return command_class(self.line) def _get_cmd_class(self): return self.fm.commands.get_command(self.line.split()[0]) def _get_tab(self): if ' ' in self.line: cmd = self._get_cmd() if cmd: return cmd.tab() else: return None return self.fm.commands.command_generator(self.line) def tab(self, n=1): if self.tab_deque is None: tab_result = self._get_tab() if isinstance(tab_result, str): self.line = tab_result self.pos = len(tab_result) self.on_line_change() elif tab_result == None: pass elif hasattr(tab_result, '__iter__'): self.tab_deque = deque(tab_result) self.tab_deque.appendleft(self.line) if self.tab_deque is not None: self.tab_deque.rotate(-n) self.line = self.tab_deque[0] self.pos = len(self.line) self.on_line_change() def on_line_change(self): self.history_search_pattern = self.line try: cls = self._get_cmd_class() except (KeyError, ValueError, IndexError): pass else: cmd = cls(self.line) if cmd and cmd.quick(): self.execute(cmd) def ask(self, text, callback, choices=['y', 'n']): """Open a question prompt with predefined choices The "text" is displayed as the question text and should include a list of possible keys that the user can type. The "callback" is a function that is called when the question is answered. It only gets the answer as an argument. "choices" is a tuple of one-letter strings that can be typed in by the user. Every other input gets ignored, except <Enter> and <ESC>. The first choice is used when the user presses <Enter>, the second choice is used when the user presses <ESC>. """ self.question_queue.append((text, callback, choices))
class Console(Widget): visible = False last_cursor_mode = None history_search_pattern = None prompt = ':' copy = '' tab_deque = None original_line = None history = None history_backup = None override = None allow_close = False historypath = None wait_for_command_input = False unicode_buffer = "" def __init__(self, win): Widget.__init__(self, win) self.clear() self.history = History(self.settings.max_console_history_size) # load history from files if not ranger.arg.clean: self.historypath = self.fm.confpath('history') try: f = open(self.historypath, 'r') except: pass else: for line in f: self.history.add(line[:-1]) f.close() self.line = "" self.history_backup = History(self.history) # NOTE: the console is considered in the "question mode" when the # question_queue is non-empty. In that case, the console will draw the # question instead of the regular console, and the input you give is # used to answer the question instead of typing in commands. # # A question is a tuple of (question_string, callback_func, # tuple_of_choices). callback_func is a function that is called when # the question is answered which gets the answer as an argument. # tuple_of_choices looks like ('y', 'n'). Only one-letter-answers are # currently supported. Pressing enter uses the first choice whereas # pressing ESC uses the second choice. self.question_queue = [] def destroy(self): # save history to files if ranger.arg.clean or not self.settings.save_console_history: return if self.historypath: try: f = open(self.historypath, 'w') except: pass else: for entry in self.history_backup: try: f.write(entry + '\n') except UnicodeEncodeError: pass f.close() def draw(self): self.win.erase() if self.question_queue: assert isinstance(self.question_queue[0], tuple) assert len(self.question_queue[0]) == 3 self.addstr(0, 0, self.question_queue[0][0]) return self.addstr(0, 0, self.prompt) line = WideString(self.line) overflow = -self.wid + len(self.prompt) + len(line) + 1 if overflow > 0: self.addstr(0, len(self.prompt), str(line[overflow:])) else: self.addstr(0, len(self.prompt), self.line) def finalize(self): move = self.fm.ui.win.move if self.question_queue: try: move(self.y, len(self.question_queue[0][0])) except: pass else: try: pos = uwid(self.line[0:self.pos]) + len(self.prompt) move(self.y, self.x + min(self.wid - 1, pos)) except: pass def open(self, string='', prompt=None, position=None): if prompt is not None: assert isinstance(prompt, str) self.prompt = prompt elif 'prompt' in self.__dict__: del self.prompt if self.last_cursor_mode is None: try: self.last_cursor_mode = curses.curs_set(1) except: pass self.allow_close = False self.tab_deque = None self.unicode_buffer = "" self.line = string self.history_search_pattern = self.line self.pos = len(string) if position is not None: self.pos = min(self.pos, position) self.history_backup.fast_forward() self.history = History(self.history_backup) self.history.add('') self.wait_for_command_input = True self.on_line_change() return True def close(self, trigger_cancel_function=True): if self.question_queue: question = self.question_queue[0] answers = question[2] if len(answers) >= 2: self._answer_question(answers[1]) else: self._close_command_prompt(trigger_cancel_function) def _close_command_prompt(self, trigger_cancel_function=True): if trigger_cancel_function: cmd = self._get_cmd(quiet=True) if cmd: try: cmd.cancel() except Exception as error: self.fm.notify(error) if self.last_cursor_mode is not None: try: curses.curs_set(self.last_cursor_mode) except: pass self.last_cursor_mode = None self.fm.hide_console_info() self.add_to_history() self.tab_deque = None self.clear() self.__class__ = Console self.wait_for_command_input = False def clear(self): self.pos = 0 self.line = '' def press(self, key): if not self.fm.ui.quick_jump.press(key): self.fm.ui.keymaps.use_keymap('console') if not self.fm.ui.press(key): self.type_key(key) def _answer_question(self, answer): if not self.question_queue: return False question = self.question_queue[0] text, callback, answers = question if answer in answers: self.question_queue.pop(0) callback(answer) return True return False def type_key(self, key): self.tab_deque = None line = "" if self.question_queue else self.line result = self._add_character(key, self.unicode_buffer, line, self.pos) if result[1] == line: # line didn't change, so we don't need to do anything, just update # the unicode _buffer. self.unicode_buffer = result[0] return if self.question_queue: self.unicode_buffer, answer, self.pos = result self._answer_question(answer) else: self.unicode_buffer, self.line, self.pos = result self.on_line_change() def _add_character(self, key, unicode_buffer, line, pos): # Takes the pressed key, a string "unicode_buffer" containing a # potentially incomplete unicode character, the current line and the # position of the cursor inside the line. # This function returns the new unicode buffer, the modified line and # position. if isinstance(key, int): try: key = chr(key) except ValueError: return unicode_buffer, line, pos if self.fm.py3: if len(unicode_buffer) >= 4: unicode_buffer = "" if ord(key) in range(0, 256): unicode_buffer += key try: decoded = unicode_buffer.encode("latin-1").decode("utf-8") except UnicodeDecodeError: return unicode_buffer, line, pos except UnicodeEncodeError: return unicode_buffer, line, pos else: unicode_buffer = "" if pos == len(line): line += decoded else: line = line[:pos] + decoded + line[pos:] pos += len(decoded) else: if pos == len(line): line += key else: line = line[:pos] + key + line[pos:] pos += len(key) return unicode_buffer, line, pos def history_move(self, n): try: current = self.history.current() except HistoryEmptyException: pass else: if self.line != current and self.line != self.history.top(): self.history.modify(self.line) if self.history_search_pattern: self.history.search(self.history_search_pattern, n) else: self.history.move(n) current = self.history.current() if self.line != current: self.line = self.history.current() self.pos = len(self.line) def add_to_history(self): self.history_backup.fast_forward() self.history_backup.add(self.line) self.history = History(self.history_backup) def move(self, **keywords): direction = Direction(keywords) if direction.horizontal(): # Ensure that the pointer is moved utf-char-wise if self.fm.py3: self.pos = direction.move(direction=direction.right(), minimum=0, maximum=len(self.line) + 1, current=self.pos) else: if self.fm.py3: uc = list(self.line) upos = len(self.line[:self.pos]) else: uc = list(self.line.decode('utf-8', 'ignore')) upos = len(self.line[:self.pos].decode('utf-8', 'ignore')) newupos = direction.move(direction=direction.right(), minimum=0, maximum=len(uc) + 1, current=upos) self.pos = len(''.join(uc[:newupos]).encode('utf-8', 'ignore')) def delete_rest(self, direction): self.tab_deque = None if direction > 0: self.copy = self.line[self.pos:] self.line = self.line[:self.pos] else: self.copy = self.line[:self.pos] self.line = self.line[self.pos:] self.pos = 0 self.on_line_change() def paste(self): if self.pos == len(self.line): self.line += self.copy else: self.line = self.line[:self.pos] + self.copy + self.line[self.pos:] self.pos += len(self.copy) self.on_line_change() def delete_word(self, backward=True): if self.line: self.tab_deque = None if backward: right_part = self.line[self.pos:] i = self.pos - 2 while i >= 0 and re.match(r'[\w\d]', self.line[i], re.U): i -= 1 self.copy = self.line[i + 1:self.pos] self.line = self.line[:i + 1] + right_part self.pos = i + 1 else: left_part = self.line[:self.pos] i = self.pos + 1 while i < len(self.line) and re.match(r'[\w\d]', self.line[i], re.U): i += 1 self.copy = self.line[self.pos:i] if i >= len(self.line): self.line = left_part self.pos = len(self.line) else: self.line = left_part + self.line[i:] self.pos = len(left_part) self.on_line_change() def delete(self, mod): self.tab_deque = None if mod == -1 and self.pos == 0: if not self.line: self.close(trigger_cancel_function=False) return # Delete utf-char-wise if self.fm.py3: left_part = self.line[:self.pos + mod] self.pos = len(left_part) self.line = left_part + self.line[self.pos + 1:] else: uc = list(self.line.decode('utf-8', 'ignore')) upos = len(self.line[:self.pos].decode('utf-8', 'ignore')) + mod left_part = ''.join(uc[:upos]).encode('utf-8', 'ignore') self.pos = len(left_part) self.line = left_part + ''.join(uc[upos + 1:]).encode( 'utf-8', 'ignore') self.on_line_change() def execute(self, cmd=None): if self.question_queue and cmd is None: question = self.question_queue[0] answers = question[2] if len(answers) >= 1: self._answer_question(answers[0]) else: self.question_queue.pop(0) return self.allow_close = True self.fm.execute_console(self.line) if self.allow_close: self._close_command_prompt(trigger_cancel_function=False) def _get_cmd(self, quiet=False): try: command_class = self._get_cmd_class() except KeyError: if not quiet: error = "Command not found: `%s'" % self.line.split()[0] self.fm.notify(error, bad=True) except: return None else: return command_class(self.line) def _get_cmd_class(self): return self.fm.commands.get_command(self.line.split()[0]) def _get_tab(self): if ' ' in self.line: cmd = self._get_cmd() if cmd: return cmd.tab() else: return None return self.fm.commands.command_generator(self.line) def tab(self, n=1): if self.tab_deque is None: tab_result = self._get_tab() if isinstance(tab_result, str): self.line = tab_result self.pos = len(tab_result) self.on_line_change() elif tab_result == None: pass elif hasattr(tab_result, '__iter__'): self.tab_deque = deque(tab_result) self.tab_deque.appendleft(self.line) if self.tab_deque is not None: self.tab_deque.rotate(-n) self.line = self.tab_deque[0] self.pos = len(self.line) self.on_line_change() def on_line_change(self): self.history_search_pattern = self.line try: cls = self._get_cmd_class() except (KeyError, ValueError, IndexError): pass else: cmd = cls(self.line) if cmd and cmd.quick(): self.execute(cmd) def ask(self, text, callback, choices=['y', 'n']): """Open a question prompt with predefined choices The "text" is displayed as the question text and should include a list of possible keys that the user can type. The "callback" is a function that is called when the question is answered. It only gets the answer as an argument. "choices" is a tuple of one-letter strings that can be typed in by the user. Every other input gets ignored, except <Enter> and <ESC>. The first choice is used when the user presses <Enter>, the second choice is used when the user presses <ESC>. """ self.question_queue.append((text, callback, choices))
class Environment(SettingsAware, SignalDispatcher): """ A collection of data which is relevant for more than one class. """ cwd = None # current directory copy = None cmd = None cut = None termsize = None history = None directories = None last_search = None pathway = None path = None def __init__(self, path): SignalDispatcher.__init__(self) self.path = abspath(expanduser(path)) self._cf = None self.pathway = () self.directories = {} self.keybuffer = KeyBuffer() self.keymaps = KeyMaps(self.keybuffer) self.copy = set() self.history = History(self.settings.max_history_size, unique=False) try: self.username = pwd.getpwuid(os.geteuid()).pw_name except: self.username = '******' + str(os.geteuid()) self.hostname = socket.gethostname() self.home_path = os.path.expanduser('~') self.signal_bind('move', self._set_cf_from_signal, priority=0.1, weak=True) def _set_cf_from_signal(self, signal): self._cf = signal.new def _set_cf(self, value): if value is not self._cf: previous = self._cf self.signal_emit('move', previous=previous, new=value) def _get_cf(self): return self._cf cf = property(_get_cf, _set_cf) def key_append(self, key): """Append a key to the keybuffer""" # special keys: if key == curses.KEY_RESIZE: self.keybuffer.clear() self.keybuffer.add(key) def key_clear(self): """Clear the keybuffer""" self.keybuffer.clear() def at_level(self, level): """ Returns the FileSystemObject at the given level. level >0 => previews level 0 => current file/directory level <0 => parent directories """ if level <= 0: try: return self.pathway[level - 1] except IndexError: return None else: directory = self.cf for i in range(level - 1): if directory is None: return None if directory.is_directory: directory = directory.pointed_obj else: return None try: return self.directories[directory.path] except AttributeError: return None except KeyError: return directory def garbage_collect(self, age, tabs): """Delete unused directory objects""" for key in tuple(self.directories): value = self.directories[key] if age != -1: if not value.is_older_than(age) or value in self.pathway: continue if value in tabs.values(): continue del self.directories[key] if value.is_directory: value.files = None self.settings.signal_garbage_collect() self.signal_garbage_collect() def get_selection(self): if self.cwd: return self.cwd.get_selection() return set() def get_directory(self, path): """Get the directory object at the given path""" path = abspath(path) try: return self.directories[path] except KeyError: obj = Directory(path) self.directories[path] = obj return obj def get_free_space(self, path): stat = os.statvfs(path) return stat.f_bavail * stat.f_bsize def assign_cursor_positions_for_subdirs(self): """Assign correct cursor positions for subdirectories""" last_path = None for path in reversed(self.pathway): if last_path is None: last_path = path continue path.move_to_obj(last_path) last_path = path def ensure_correct_pointer(self): if self.cwd: self.cwd.correct_pointer() def history_go(self, relative): """Move relative in history""" if self.history: self.history.move(relative).go(history=False) def enter_dir(self, path, history = True): """Enter given path""" if path is None: return path = str(path) previous = self.cwd # get the absolute path path = normpath(join(self.path, expanduser(path))) if not isdir(path): return False new_cwd = self.get_directory(path) try: os.chdir(path) except: return True self.path = path self.cwd = new_cwd self.cwd.load_content_if_outdated() # build the pathway, a tuple of directory objects which lie # on the path to the current directory. if path == '/': self.pathway = (self.get_directory('/'), ) else: pathway = [] currentpath = '/' for dir in path.split('/'): currentpath = join(currentpath, dir) pathway.append(self.get_directory(currentpath)) self.pathway = tuple(pathway) self.assign_cursor_positions_for_subdirs() # set the current file. self.cwd.sort_directories_first = self.settings.sort_directories_first self.cwd.sort_reverse = self.settings.sort_reverse self.cwd.sort_if_outdated() self.cf = self.cwd.pointed_obj if history: self.history.add(new_cwd) self.signal_emit('cd', previous=previous, new=self.cwd) return True
class Tab(FileManagerAware, SettingsAware): # pylint: disable=too-many-instance-attributes def __init__(self, path): self.thisdir = None # Current Working Directory self._thisfile = None # Current File self.history = History(self.settings.max_history_size, unique=False) self.last_search = None self.pointer = 0 self.path = abspath(expanduser(path)) self.pathway = () # NOTE: in the line below, weak=True works only in python3. In python2, # weak references are not equal to the original object when tested with # "==", and this breaks _set_thisfile_from_signal and _on_tab_change. self.fm.signal_bind('move', self._set_thisfile_from_signal, priority=settings.SIGNAL_PRIORITY_AFTER_SYNC, weak=(sys.version_info[0] >= 3)) self.fm.signal_bind('tab.change', self._on_tab_change, weak=(sys.version_info[0] >= 3)) def _set_thisfile_from_signal(self, signal): if self == signal.tab: self._thisfile = signal.new if self == self.fm.thistab: self.pointer = self.thisdir.pointer def _on_tab_change(self, signal): if self == signal.new and self.thisdir: # restore the pointer whenever this tab is reopened self.thisdir.pointer = self.pointer self.thisdir.correct_pointer() def _set_thisfile(self, value): if value is not self._thisfile: previous = self._thisfile self.fm.signal_emit('move', previous=previous, new=value, tab=self) def _get_thisfile(self): return self._thisfile thisfile = property(_get_thisfile, _set_thisfile) def at_level(self, level): """Returns the FileSystemObject at the given level. level >0 => previews level 0 => current file/directory level <0 => parent directories """ if level <= 0: try: return self.pathway[level - 1] except IndexError: return None else: directory = self.thisdir for _ in range(level): if directory is None: return None if directory.is_directory: directory = directory.pointed_obj else: return None return directory def get_selection(self): if self.thisdir: if self.thisdir.marked_items: return self.thisdir.get_selection() elif self._thisfile: return [self._thisfile] return [] def assign_cursor_positions_for_subdirs(self): # pylint: disable=invalid-name """Assign correct cursor positions for subdirectories""" last_path = None for path in reversed(self.pathway): if last_path is None: last_path = path continue path.move_to_obj(last_path) last_path = path def ensure_correct_pointer(self): if self.thisdir: self.thisdir.correct_pointer() def history_go(self, relative): """Move relative in history""" if self.history: self.history.move(relative).go(history=False) def inherit_history(self, other_history): self.history.rebase(other_history) def enter_dir(self, path, history=True): """Enter given path""" # TODO: Ensure that there is always a self.thisdir if path is None: return None path = str(path) # clear filter in the folder we're leaving if self.fm.settings.clear_filters_on_dir_change and self.thisdir: self.thisdir.filter = None self.thisdir.refilter() previous = self.thisdir # get the absolute path path = normpath(join(self.path, expanduser(path))) selectfile = None if not isdir(path): selectfile = path path = dirname(path) new_thisdir = self.fm.get_directory(path) try: os.chdir(path) except OSError: return True self.path = path self.thisdir = new_thisdir self.thisdir.load_content_if_outdated() # build the pathway, a tuple of directory objects which lie # on the path to the current directory. if path == '/': self.pathway = (self.fm.get_directory('/'), ) else: pathway = [] currentpath = '/' for comp in path.split('/'): currentpath = join(currentpath, comp) pathway.append(self.fm.get_directory(currentpath)) self.pathway = tuple(pathway) self.assign_cursor_positions_for_subdirs() # set the current file. self.thisdir.sort_directories_first = self.fm.settings.sort_directories_first self.thisdir.sort_reverse = self.fm.settings.sort_reverse self.thisdir.sort_if_outdated() if selectfile: self.thisdir.move_to_obj(selectfile) if previous and previous.path != path: self.thisfile = self.thisdir.pointed_obj else: # This avoids setting self.pointer (through the 'move' signal) and # is required so that you can use enter_dir when switching tabs # without messing up the pointer. self._thisfile = self.thisdir.pointed_obj if history: self.history.add(new_thisdir) self.fm.signal_emit('cd', previous=previous, new=self.thisdir) return True def __repr__(self): return "<Tab '%s'>" % self.thisdir
class Console(Widget): # pylint: disable=too-many-instance-attributes,too-many-public-methods visible = False last_cursor_mode = None history_search_pattern = None prompt = ':' copy = '' tab_deque = None original_line = None history = None history_backup = None override = None allow_close = False historypath = None wait_for_command_input = False unicode_buffer = "" def __init__(self, win): Widget.__init__(self, win) self.pos = 0 self.line = '' self.history = History(self.settings.max_console_history_size) # load history from files if not ranger.args.clean: self.historypath = self.fm.datapath('history') if os.path.exists(self.historypath): try: with open(self.historypath, "r") as fobj: try: for line in fobj: self.history.add(line[:-1]) except UnicodeDecodeError as ex: self.fm.notify( "Failed to parse corrupt history file", bad=True, exception=ex, ) except (OSError, IOError) as ex: self.fm.notify("Failed to read history file", bad=True, exception=ex) self.history_backup = History(self.history) # NOTE: the console is considered in the "question mode" when the # question_queue is non-empty. In that case, the console will draw the # question instead of the regular console, and the input you give is # used to answer the question instead of typing in commands. # # A question is a tuple of (question_string, callback_func, # tuple_of_choices). callback_func is a function that is called when # the question is answered which gets the answer as an argument. # tuple_of_choices looks like ('y', 'n'). Only one-letter-answers are # currently supported. Pressing enter uses the first choice whereas # pressing ESC uses the second choice. self.question_queue = [] def destroy(self): # save history to files if ranger.args.clean or not self.settings.save_console_history: return if self.historypath: try: with open(self.historypath, 'w') as fobj: for entry in self.history_backup: try: fobj.write(entry + '\n') except UnicodeEncodeError: pass except (OSError, IOError) as ex: self.fm.notify("Failed to write history file", bad=True, exception=ex) Widget.destroy(self) def _calculate_offset(self): wid = self.wid - 2 whalf = wid // 2 if self.pos < whalf or len(self.line) < wid: return 0 if self.pos > len(self.line) - (wid - whalf): return len(self.line) - wid return self.pos - whalf def draw(self): self.win.erase() if self.question_queue: assert isinstance(self.question_queue[0], tuple) assert len(self.question_queue[0]) == 3 self.addstr(0, 0, self.question_queue[0][0][self.pos:]) return self.addstr(0, 0, self.prompt) line = WideString(self.line) if line: x = self._calculate_offset() self.addstr(0, len(self.prompt), str(line[x:])) def finalize(self): move = self.fm.ui.win.move if self.question_queue: try: move(self.y, len(self.question_queue[0][0])) except curses.error: pass else: try: x = self._calculate_offset() pos = uwid(self.line[x:self.pos]) + len(self.prompt) move(self.y, self.x + min(self.wid - 1, pos)) except curses.error: pass def open(self, string='', prompt=None, position=None): if prompt is not None: assert isinstance(prompt, str) self.prompt = prompt elif 'prompt' in self.__dict__: del self.prompt if self.last_cursor_mode is None: try: self.last_cursor_mode = curses.curs_set(1) except curses.error: pass self.allow_close = False self.tab_deque = None self.unicode_buffer = "" self.line = string self.history_search_pattern = self.line self.pos = len(string) if position is not None: self.pos = min(self.pos, position) self.history_backup.fast_forward() self.history = History(self.history_backup) self.history.add('') self.wait_for_command_input = True return True def close(self, trigger_cancel_function=True): if self.question_queue: question = self.question_queue[0] answers = question[2] if len(answers) >= 2: self._answer_question(answers[1]) else: self._close_command_prompt(trigger_cancel_function) def _close_command_prompt(self, trigger_cancel_function=True): if trigger_cancel_function: cmd = self._get_cmd(quiet=True) if cmd: cmd.cancel() if self.last_cursor_mode is not None: try: curses.curs_set(self.last_cursor_mode) except curses.error: pass self.last_cursor_mode = None self.fm.hide_console_info() self.add_to_history() self.tab_deque = None self.clear() self.__class__ = Console self.wait_for_command_input = False def clear(self): self.pos = 0 self.line = '' def press(self, key): self.fm.ui.keymaps.use_keymap('console') if not self.fm.ui.press(key): self.type_key(key) def _answer_question(self, answer): if not self.question_queue: return False question = self.question_queue[0] _, callback, answers = question if answer in answers: self.question_queue.pop(0) callback(answer) return True return False def type_key(self, key): self.tab_deque = None line = "" if self.question_queue else self.line result = self._add_character(key, self.unicode_buffer, line, self.pos) if result[1] == line: # line didn't change, so we don't need to do anything, just update # the unicode _buffer. self.unicode_buffer = result[0] return if self.question_queue: self.unicode_buffer, answer, _ = result self._answer_question(answer) else: self.unicode_buffer, self.line, self.pos = result self.on_line_change() @staticmethod def _add_character(key, unicode_buffer, line, pos): # Takes the pressed key, a string "unicode_buffer" containing a # potentially incomplete unicode character, the current line and the # position of the cursor inside the line. # This function returns the new unicode buffer, the modified line and # position. if isinstance(key, int): try: key = chr(key) except ValueError: return unicode_buffer, line, pos if PY3: if len(unicode_buffer) >= 4: unicode_buffer = "" if ord(key) in range(0, 256): unicode_buffer += key try: decoded = unicode_buffer.encode("latin-1").decode("utf-8") except UnicodeDecodeError: return unicode_buffer, line, pos except UnicodeEncodeError: return unicode_buffer, line, pos else: unicode_buffer = "" if pos == len(line): line += decoded else: line = line[:pos] + decoded + line[pos:] pos += len(decoded) else: if pos == len(line): line += key else: line = line[:pos] + key + line[pos:] pos += len(key) return unicode_buffer, line, pos def history_move(self, n): try: current = self.history.current() except HistoryEmptyException: pass else: if self.line != current and self.line != self.history.top(): self.history.modify(self.line) if self.history_search_pattern: self.history.search(self.history_search_pattern, n) else: self.history.move(n) current = self.history.current() if self.line != current: self.line = self.history.current() self.pos = len(self.line) def add_to_history(self): self.history_backup.fast_forward() self.history_backup.add(self.line) self.history = History(self.history_backup) def move(self, **keywords): direction = Direction(keywords) if direction.horizontal(): # Ensure that the pointer is moved utf-char-wise if PY3: if self.question_queue: umax = len(self.question_queue[0][0]) + 1 - self.wid else: umax = len(self.line) + 1 self.pos = direction.move(direction=direction.right(), minimum=0, maximum=umax, current=self.pos) else: if self.question_queue: uchar = list(self.question_queue[0][0].decode( 'utf-8', 'ignore')) upos = len(self.question_queue[0][0][:self.pos].decode( 'utf-8', 'ignore')) umax = len(uchar) + 1 - self.wid else: uchar = list(self.line.decode('utf-8', 'ignore')) upos = len(self.line[:self.pos].decode('utf-8', 'ignore')) umax = len(uchar) + 1 newupos = direction.move(direction=direction.right(), minimum=0, maximum=umax, current=upos) self.pos = len(''.join(uchar[:newupos]).encode( 'utf-8', 'ignore')) def move_word(self, **keywords): direction = Direction(keywords) if direction.horizontal(): self.pos = self.move_by_word(self.line, self.pos, direction.right()) self.on_line_change() @staticmethod def move_by_word(line, position, direction): """ Returns a new position by moving word-wise in the line >>> from ranger import PY3 >>> if PY3: ... line = "\\u30AA\\u30CF\\u30E8\\u30A6 world, this is dog" ... else: ... # Didn't get the unicode test to work on python2, even though ... # it works fine in ranger, even with unicode input... ... line = "ohai world, this is dog" >>> Console.move_by_word(line, 0, -1) 0 >>> Console.move_by_word(line, 0, 1) 5 >>> Console.move_by_word(line, 2, -1) 0 >>> Console.move_by_word(line, 2, 1) 5 >>> Console.move_by_word(line, 15, -2) 5 >>> Console.move_by_word(line, 15, 2) 21 >>> Console.move_by_word(line, 24, -1) 21 >>> Console.move_by_word(line, 24, 1) 24 """ word_beginnings = [] seen_whitespace = True current_word = None cursor_inside_word = False # Scan the line for word boundaries and determine position of cursor for i, char in enumerate(line): if i == position: current_word = len(word_beginnings) if not seen_whitespace: cursor_inside_word = True if char == " ": seen_whitespace = True elif seen_whitespace: seen_whitespace = False word_beginnings.append(i) word_beginnings.append(len(line)) # Handle corner cases: if current_word is None: current_word = len(word_beginnings) if direction > 0 and cursor_inside_word: current_word -= 1 if direction < 0 and position == len(line): current_word -= 1 new_word = current_word + direction new_word = max(0, min(len(word_beginnings) - 1, new_word)) return word_beginnings[new_word] def delete_rest(self, direction): self.tab_deque = None if direction > 0: self.copy = self.line[self.pos:] self.line = self.line[:self.pos] else: self.copy = self.line[:self.pos] self.line = self.line[self.pos:] self.pos = 0 self.on_line_change() def paste(self): if self.pos == len(self.line): self.line += self.copy else: self.line = self.line[:self.pos] + self.copy + self.line[self.pos:] self.pos += len(self.copy) self.on_line_change() def delete_word(self, backward=True): if self.line: self.tab_deque = None if backward: right_part = self.line[self.pos:] i = self.pos - 2 while i >= 0 and re.match(r'[\w\d]', self.line[i], re.UNICODE): # pylint: disable=no-member i -= 1 self.copy = self.line[i + 1:self.pos] self.line = self.line[:i + 1] + right_part self.pos = i + 1 else: left_part = self.line[:self.pos] i = self.pos + 1 while i < len(self.line) and re.match(r'[\w\d]', self.line[i], re.UNICODE): # pylint: disable=no-member i += 1 self.copy = self.line[self.pos:i] if i >= len(self.line): self.line = left_part self.pos = len(self.line) else: self.line = left_part + self.line[i:] self.pos = len(left_part) self.on_line_change() def delete(self, mod): self.tab_deque = None if mod == -1 and self.pos == 0: if not self.line: self.close(trigger_cancel_function=False) return # Delete utf-char-wise if PY3: left_part = self.line[:self.pos + mod] self.pos = len(left_part) self.line = left_part + self.line[self.pos + 1:] else: uchar = list(self.line.decode('utf-8', 'ignore')) upos = len(self.line[:self.pos].decode('utf-8', 'ignore')) + mod left_part = ''.join(uchar[:upos]).encode('utf-8', 'ignore') self.pos = len(left_part) self.line = left_part + ''.join(uchar[upos + 1:]).encode( 'utf-8', 'ignore') self.on_line_change() def transpose_subr(self, line, x, y): # Transpose substrings x & y of line # x & y are tuples of length two containing positions of endpoints if not 0 <= x[0] < x[1] <= y[0] < y[1] <= len(line): self.fm.notify("Tried to transpose invalid regions.", bad=True) return line line_begin = line[:x[0]] word_x = line[x[0]:x[1]] line_middle = line[x[1]:y[0]] word_y = line[y[0]:y[1]] line_end = line[y[1]:] line = line_begin + word_y + line_middle + word_x + line_end return line def transpose_chars(self): if self.pos == 0: return elif self.pos == len(self.line): x = max(0, self.pos - 2), max(0, self.pos - 1) y = max(0, self.pos - 1), self.pos else: x = max(0, self.pos - 1), self.pos y = self.pos, min(len(self.line), self.pos + 1) self.line = self.transpose_subr(self.line, x, y) self.pos = y[1] self.on_line_change() def transpose_words(self): # Interchange adjacent words at the console with Alt-t # like in Emacs and many terminal emulators if self.line: # If before the first word, interchange next two words if not re.search(r'[\w\d]', self.line[:self.pos], re.UNICODE): self.pos = self.move_by_word(self.line, self.pos, 1) # If in/after last word, interchange last two words if (re.match(r'[\w\d]*\s*$', self.line[self.pos:], re.UNICODE) and (re.match(r'[\w\d]', self.line[self.pos - 1], re.UNICODE) if self.pos - 1 >= 0 else True)): self.pos = self.move_by_word(self.line, self.pos, -1) # Util function to increment position until out of word/whitespace def _traverse(line, pos, regex): while pos < len(line) and re.match(regex, line[pos], re.UNICODE): pos += 1 return pos # Calculate endpoints of target words and pass them to # 'self.transpose_subr' x_begin = self.move_by_word(self.line, self.pos, -1) x_end = _traverse(self.line, x_begin, r'[\w\d]') x = x_begin, x_end y_begin = self.pos # If in middle of word, move to end if re.match(r'[\w\d]', self.line[self.pos - 1], re.UNICODE): y_begin = _traverse(self.line, y_begin, r'[\w\d]') # Traverse whitespace to beginning of next word y_begin = _traverse(self.line, y_begin, r'\s') y_end = _traverse(self.line, y_begin, r'[\w\d]') y = y_begin, y_end self.line = self.transpose_subr(self.line, x, y) self.pos = y[1] self.on_line_change() def execute(self, cmd=None): if self.question_queue and cmd is None: question = self.question_queue[0] answers = question[2] if len(answers) >= 1: self._answer_question(answers[0]) else: self.question_queue.pop(0) return self.allow_close = True if cmd: cmd.execute() else: self.fm.execute_console(self.line) if self.allow_close: self._close_command_prompt(trigger_cancel_function=False) def _get_cmd(self, quiet=False): try: command_class = self.get_cmd_class() except IndexError: return None except KeyError: if not quiet: self.fm.notify("Command not found: `%s'" % self.line.split()[0], bad=True) return None return command_class(self.line) def get_cmd_class(self): return self.fm.commands.get_command(self.line.split()[0], abbrev=True) def _get_tab(self, tabnum): if ' ' in self.line: cmd = self._get_cmd() if cmd: return cmd.tab(tabnum) return None return self.fm.commands.command_generator(self.line) def tab(self, tabnum=1): if self.tab_deque is None: tab_result = self._get_tab(tabnum) if tab_result is None: pass elif isinstance(tab_result, str): self.line = tab_result self.pos = len(tab_result) self.on_line_change() elif hasattr(tab_result, '__iter__'): self.tab_deque = deque(tab_result) self.tab_deque.appendleft(self.line) if self.tab_deque is not None: self.tab_deque.rotate(-tabnum) self.line = self.tab_deque[0] self.pos = len(self.line) self.on_line_change() def on_line_change(self): self.history_search_pattern = self.line try: cls = self.get_cmd_class() except (KeyError, ValueError, IndexError): pass else: cmd = cls(self.line) if cmd and cmd.quick(): cmd.quickly_executed = True self.execute(cmd) def ask(self, text, callback, choices=None): """Open a question prompt with predefined choices The "text" is displayed as the question text and should include a list of possible keys that the user can type. The "callback" is a function that is called when the question is answered. It only gets the answer as an argument. "choices" is a tuple of one-letter strings that can be typed in by the user. Every other input gets ignored, except <Enter> and <ESC>. The first choice is used when the user presses <Enter>, the second choice is used when the user presses <ESC>. """ self.question_queue.append( (text, callback, choices if choices is not None else ['y', 'n']))