1# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: 2 3# Copyright 2014-2021 Florian Bruhin (The Compiler) <mail@qutebrowser.org> 4# 5# This file is part of qutebrowser. 6# 7# qutebrowser is free software: you can redistribute it and/or modify 8# it under the terms of the GNU General Public License as published by 9# the Free Software Foundation, either version 3 of the License, or 10# (at your option) any later version. 11# 12# qutebrowser is distributed in the hope that it will be useful, 13# but WITHOUT ANY WARRANTY; without even the implied warranty of 14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15# GNU General Public License for more details. 16# 17# You should have received a copy of the GNU General Public License 18# along with qutebrowser. If not, see <https://www.gnu.org/licenses/>. 19 20"""Bridge to provide readline-like shortcuts for QLineEdits.""" 21 22from typing import Iterable, Optional, MutableMapping 23 24from PyQt5.QtWidgets import QApplication, QLineEdit 25 26from qutebrowser.api import cmdutils 27 28 29class _ReadlineBridge: 30 31 """Bridge which provides readline-like commands for the current QLineEdit. 32 33 Attributes: 34 _deleted: Mapping from widgets to their last deleted text. 35 """ 36 37 def __init__(self) -> None: 38 self._deleted: MutableMapping[QLineEdit, str] = {} 39 40 def _widget(self) -> Optional[QLineEdit]: 41 """Get the currently active QLineEdit.""" 42 # FIXME add this to api.utils or so 43 qapp = QApplication.instance() 44 assert qapp is not None 45 w = qapp.focusWidget() 46 47 if isinstance(w, QLineEdit): 48 return w 49 else: 50 return None 51 52 def _dispatch(self, name: str, *, 53 mark: bool = None, 54 delete: bool = False) -> None: 55 widget = self._widget() 56 if widget is None: 57 return 58 59 method = getattr(widget, name) 60 if mark is None: 61 method() 62 else: 63 method(mark) 64 65 if delete: 66 self._deleted[widget] = widget.selectedText() 67 widget.del_() 68 69 def backward_char(self) -> None: 70 self._dispatch('cursorBackward', mark=False) 71 72 def forward_char(self) -> None: 73 self._dispatch('cursorForward', mark=False) 74 75 def backward_word(self) -> None: 76 self._dispatch('cursorWordBackward', mark=False) 77 78 def forward_word(self) -> None: 79 self._dispatch('cursorWordForward', mark=False) 80 81 def beginning_of_line(self) -> None: 82 self._dispatch('home', mark=False) 83 84 def end_of_line(self) -> None: 85 self._dispatch('end', mark=False) 86 87 def unix_line_discard(self) -> None: 88 self._dispatch('home', mark=True, delete=True) 89 90 def kill_line(self) -> None: 91 self._dispatch('end', mark=True, delete=True) 92 93 def _rubout(self, delim: Iterable[str]) -> None: 94 """Delete backwards using the characters in delim as boundaries.""" 95 widget = self._widget() 96 if widget is None: 97 return 98 cursor_position = widget.cursorPosition() 99 text = widget.text() 100 101 target_position = cursor_position 102 103 is_boundary = True 104 while is_boundary and target_position > 0: 105 is_boundary = text[target_position - 1] in delim 106 target_position -= 1 107 108 is_boundary = False 109 while not is_boundary and target_position > 0: 110 is_boundary = text[target_position - 1] in delim 111 target_position -= 1 112 113 moveby = cursor_position - target_position - 1 114 widget.cursorBackward(True, moveby) 115 self._deleted[widget] = widget.selectedText() 116 widget.del_() 117 118 def unix_word_rubout(self) -> None: 119 self._rubout([' ']) 120 121 def unix_filename_rubout(self) -> None: 122 self._rubout([' ', '/']) 123 124 def backward_kill_word(self) -> None: 125 self._dispatch('cursorWordBackward', mark=True, delete=True) 126 127 def kill_word(self) -> None: 128 self._dispatch('cursorWordForward', mark=True, delete=True) 129 130 def yank(self) -> None: 131 """Paste previously deleted text.""" 132 widget = self._widget() 133 if widget is None or widget not in self._deleted: 134 return 135 widget.insert(self._deleted[widget]) 136 137 def delete_char(self) -> None: 138 self._dispatch('del_') 139 140 def backward_delete_char(self) -> None: 141 self._dispatch('backspace') 142 143 144bridge = _ReadlineBridge() 145_register = cmdutils.register( 146 modes=[cmdutils.KeyMode.command, cmdutils.KeyMode.prompt]) 147 148 149@_register 150def rl_backward_char() -> None: 151 """Move back a character. 152 153 This acts like readline's backward-char. 154 """ 155 bridge.backward_char() 156 157 158@_register 159def rl_forward_char() -> None: 160 """Move forward a character. 161 162 This acts like readline's forward-char. 163 """ 164 bridge.forward_char() 165 166 167@_register 168def rl_backward_word() -> None: 169 """Move back to the start of the current or previous word. 170 171 This acts like readline's backward-word. 172 """ 173 bridge.backward_word() 174 175 176@_register 177def rl_forward_word() -> None: 178 """Move forward to the end of the next word. 179 180 This acts like readline's forward-word. 181 """ 182 bridge.forward_word() 183 184 185@_register 186def rl_beginning_of_line() -> None: 187 """Move to the start of the line. 188 189 This acts like readline's beginning-of-line. 190 """ 191 bridge.beginning_of_line() 192 193 194@_register 195def rl_end_of_line() -> None: 196 """Move to the end of the line. 197 198 This acts like readline's end-of-line. 199 """ 200 bridge.end_of_line() 201 202 203@_register 204def rl_unix_line_discard() -> None: 205 """Remove chars backward from the cursor to the beginning of the line. 206 207 This acts like readline's unix-line-discard. 208 """ 209 bridge.unix_line_discard() 210 211 212@_register 213def rl_kill_line() -> None: 214 """Remove chars from the cursor to the end of the line. 215 216 This acts like readline's kill-line. 217 """ 218 bridge.kill_line() 219 220 221@_register 222def rl_unix_word_rubout() -> None: 223 """Remove chars from the cursor to the beginning of the word. 224 225 This acts like readline's unix-word-rubout. Whitespace is used as a 226 word delimiter. 227 """ 228 bridge.unix_word_rubout() 229 230 231@_register 232def rl_unix_filename_rubout() -> None: 233 """Remove chars from the cursor to the previous path separator. 234 235 This acts like readline's unix-filename-rubout. 236 """ 237 bridge.unix_filename_rubout() 238 239 240@_register 241def rl_backward_kill_word() -> None: 242 """Remove chars from the cursor to the beginning of the word. 243 244 This acts like readline's backward-kill-word. Any non-alphanumeric 245 character is considered a word delimiter. 246 """ 247 bridge.backward_kill_word() 248 249 250@_register 251def rl_kill_word() -> None: 252 """Remove chars from the cursor to the end of the current word. 253 254 This acts like readline's kill-word. 255 """ 256 bridge.kill_word() 257 258 259@_register 260def rl_yank() -> None: 261 """Paste the most recently deleted text. 262 263 This acts like readline's yank. 264 """ 265 bridge.yank() 266 267 268@_register 269def rl_delete_char() -> None: 270 """Delete the character after the cursor. 271 272 This acts like readline's delete-char. 273 """ 274 bridge.delete_char() 275 276 277@_register 278def rl_backward_delete_char() -> None: 279 """Delete the character before the cursor. 280 281 This acts like readline's backward-delete-char. 282 """ 283 bridge.backward_delete_char() 284