Ejemplo n.º 1
0
    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 = []
Ejemplo n.º 2
0
    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 = []
Ejemplo n.º 3
0
    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 = []
Ejemplo n.º 4
0
    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 = []
Ejemplo n.º 5
0
    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
Ejemplo n.º 6
0
 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'))
Ejemplo n.º 7
0
	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)
Ejemplo n.º 8
0
    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
Ejemplo n.º 9
0
    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
Ejemplo n.º 10
0
 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"))
Ejemplo n.º 11
0
    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)
Ejemplo n.º 12
0
 def add_to_history(self):
     self.history_backup.fast_forward()
     self.history_backup.add(self.line)
     self.history = History(self.history_backup)
Ejemplo n.º 13
0
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']))
Ejemplo n.º 14
0
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
Ejemplo n.º 15
0
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)
Ejemplo n.º 16
0
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))
Ejemplo n.º 17
0
 def add_to_history(self):
     self.history_backup.fast_forward()
     self.history_backup.add(self.line)
     self.history = History(self.history_backup)
Ejemplo n.º 18
0
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))
Ejemplo n.º 19
0
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
Ejemplo n.º 20
0
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
Ejemplo n.º 21
0
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']))