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"""Custom useful data types.""" 21 22import html 23import operator 24import enum 25import dataclasses 26from typing import Optional, Sequence, TypeVar, Union 27 28from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QTimer 29from PyQt5.QtCore import QUrl 30 31from qutebrowser.utils import log, qtutils, utils 32 33 34_T = TypeVar('_T', bound=utils.Comparable) 35 36 37class Unset: 38 39 """Class for an unset object.""" 40 41 __slots__ = () 42 43 def __repr__(self) -> str: 44 return '<UNSET>' 45 46 47UNSET = Unset() 48 49 50class NeighborList(Sequence[_T]): 51 52 """A list of items which saves its current position. 53 54 Class attributes: 55 Modes: Different modes, see constructor documentation. 56 57 Attributes: 58 fuzzyval: The value which is currently set but not in the list. 59 _idx: The current position in the list. 60 _items: A list of all items, accessed through item property. 61 _mode: The current mode. 62 """ 63 64 class Modes(enum.Enum): 65 66 """Behavior for the 'mode' argument.""" 67 68 edge = enum.auto() 69 exception = enum.auto() 70 71 def __init__(self, items: Sequence[_T] = None, 72 default: Union[_T, Unset] = UNSET, 73 mode: Modes = Modes.exception) -> None: 74 """Constructor. 75 76 Args: 77 items: The list of items to iterate in. 78 _default: The initially selected value. 79 _mode: Behavior when the first/last item is reached. 80 Modes.edge: Go to the first/last item 81 Modes.exception: Raise an IndexError. 82 """ 83 if not isinstance(mode, self.Modes): 84 raise TypeError("Mode {} is not a Modes member!".format(mode)) 85 if items is None: 86 self._items: Sequence[_T] = [] 87 else: 88 self._items = list(items) 89 self._default = default 90 91 if not isinstance(default, Unset): 92 idx = self._items.index(default) 93 self._idx: Optional[int] = idx 94 else: 95 self._idx = None 96 97 self._mode = mode 98 self.fuzzyval: Optional[int] = None 99 100 def __getitem__(self, key: int) -> _T: # type: ignore[override] 101 return self._items[key] 102 103 def __len__(self) -> int: 104 return len(self._items) 105 106 def __repr__(self) -> str: 107 return utils.get_repr(self, items=self._items, mode=self._mode, 108 idx=self._idx, fuzzyval=self.fuzzyval) 109 110 def _snap_in(self, offset: int) -> bool: 111 """Set the current item to the closest item to self.fuzzyval. 112 113 Args: 114 offset: negative to get the next smaller item, positive for the 115 next bigger one. 116 117 Return: 118 True if the value snapped in (changed), 119 False when the value already was in the list. 120 """ 121 assert isinstance(self.fuzzyval, (int, float)), self.fuzzyval 122 123 op = operator.le if offset < 0 else operator.ge 124 items = [(idx, e) for (idx, e) in enumerate(self._items) 125 if op(e, self.fuzzyval)] 126 if items: 127 item = min( 128 items, 129 key=lambda tpl: 130 abs(self.fuzzyval - tpl[1])) # type: ignore[operator] 131 else: 132 sorted_items = sorted(enumerate(self.items), key=lambda e: e[1]) 133 idx = 0 if offset < 0 else -1 134 item = sorted_items[idx] 135 self._idx = item[0] 136 return self.fuzzyval not in self._items 137 138 def _get_new_item(self, offset: int) -> _T: 139 """Logic for getitem to get the item at offset. 140 141 Args: 142 offset: The offset of the current item, relative to the last one. 143 144 Return: 145 The new item. 146 """ 147 assert self._idx is not None 148 try: 149 if self._idx + offset >= 0: 150 new = self._items[self._idx + offset] 151 else: 152 raise IndexError 153 except IndexError: 154 if self._mode == self.Modes.edge: 155 assert offset != 0 156 if offset > 0: 157 new = self.lastitem() 158 else: 159 new = self.firstitem() 160 elif self._mode == self.Modes.exception: # pragma: no branch 161 raise 162 else: 163 self._idx += offset 164 return new 165 166 @property 167 def items(self) -> Sequence[_T]: 168 """Getter for items, which should not be set.""" 169 return self._items 170 171 def getitem(self, offset: int) -> _T: 172 """Get the item with a relative position. 173 174 Args: 175 offset: The offset of the current item, relative to the last one. 176 177 Return: 178 The new item. 179 """ 180 log.misc.debug("{} items, idx {}, offset {}".format( 181 len(self._items), self._idx, offset)) 182 if not self._items: 183 raise IndexError("No items found!") 184 if self.fuzzyval is not None: 185 # Value has been set to something not in the list, so we snap in to 186 # the closest value in the right direction and count this as one 187 # step towards offset. 188 snapped = self._snap_in(offset) 189 if snapped and offset > 0: 190 offset -= 1 191 elif snapped: 192 offset += 1 193 self.fuzzyval = None 194 return self._get_new_item(offset) 195 196 def curitem(self) -> _T: 197 """Get the current item in the list.""" 198 if self._idx is not None: 199 return self._items[self._idx] 200 else: 201 raise IndexError("No current item!") 202 203 def nextitem(self) -> _T: 204 """Get the next item in the list.""" 205 return self.getitem(1) 206 207 def previtem(self) -> _T: 208 """Get the previous item in the list.""" 209 return self.getitem(-1) 210 211 def firstitem(self) -> _T: 212 """Get the first item in the list.""" 213 if not self._items: 214 raise IndexError("No items found!") 215 self._idx = 0 216 return self.curitem() 217 218 def lastitem(self) -> _T: 219 """Get the last item in the list.""" 220 if not self._items: 221 raise IndexError("No items found!") 222 self._idx = len(self._items) - 1 223 return self.curitem() 224 225 def reset(self) -> _T: 226 """Reset the position to the default.""" 227 if self._default is UNSET: 228 raise ValueError("No default set!") 229 self._idx = self._items.index(self._default) 230 return self.curitem() 231 232 233class PromptMode(enum.Enum): 234 235 """The mode of a Question.""" 236 237 yesno = enum.auto() 238 text = enum.auto() 239 user_pwd = enum.auto() 240 alert = enum.auto() 241 download = enum.auto() 242 243 244class ClickTarget(enum.Enum): 245 246 """How to open a clicked link.""" 247 248 normal = enum.auto() #: Open the link in the current tab 249 tab = enum.auto() #: Open the link in a new foreground tab 250 tab_bg = enum.auto() #: Open the link in a new background tab 251 window = enum.auto() #: Open the link in a new window 252 hover = enum.auto() #: Only hover over the link 253 254 255class KeyMode(enum.Enum): 256 257 """Key input modes.""" 258 259 normal = enum.auto() #: Normal mode (no mode was entered) 260 hint = enum.auto() #: Hint mode (showing labels for links) 261 command = enum.auto() #: Command mode (after pressing the colon key) 262 yesno = enum.auto() #: Yes/No prompts 263 prompt = enum.auto() #: Text prompts 264 insert = enum.auto() #: Insert mode (passing through most keys) 265 passthrough = enum.auto() #: Passthrough mode (passing through all keys) 266 caret = enum.auto() #: Caret mode (moving cursor with keys) 267 set_mark = enum.auto() 268 jump_mark = enum.auto() 269 record_macro = enum.auto() 270 run_macro = enum.auto() 271 # 'register' is a bit of an oddball here: It's not really a "real" mode, 272 # but it's used in the config for common bindings for 273 # set_mark/jump_mark/record_macro/run_macro. 274 register = enum.auto() 275 276 277class Exit(enum.IntEnum): 278 279 """Exit statuses for errors. Needs to be an int for sys.exit.""" 280 281 ok = 0 282 reserved = 1 283 exception = 2 284 err_ipc = 3 285 err_init = 4 286 287 288class LoadStatus(enum.Enum): 289 290 """Load status of a tab.""" 291 292 none = enum.auto() 293 success = enum.auto() 294 success_https = enum.auto() 295 error = enum.auto() 296 warn = enum.auto() 297 loading = enum.auto() 298 299 300class Backend(enum.Enum): 301 302 """The backend being used (usertypes.backend).""" 303 304 QtWebKit = enum.auto() 305 QtWebEngine = enum.auto() 306 307 308class JsWorld(enum.Enum): 309 310 """World/context to run JavaScript code in.""" 311 312 main = enum.auto() #: Same world as the web page's JavaScript. 313 application = enum.auto() #: Application world, used by qutebrowser internally. 314 user = enum.auto() #: User world, currently not used. 315 jseval = enum.auto() #: World used for the jseval-command. 316 317 318class JsLogLevel(enum.Enum): 319 320 """Log level of a JS message. 321 322 This needs to match up with the keys allowed for the 323 content.javascript.log setting. 324 """ 325 326 unknown = enum.auto() 327 info = enum.auto() 328 warning = enum.auto() 329 error = enum.auto() 330 331 332class MessageLevel(enum.Enum): 333 334 """The level of a message being shown.""" 335 336 error = enum.auto() 337 warning = enum.auto() 338 info = enum.auto() 339 340 341class IgnoreCase(enum.Enum): 342 343 """Possible values for the 'search.ignore_case' setting.""" 344 345 smart = enum.auto() 346 never = enum.auto() 347 always = enum.auto() 348 349 350class CommandValue(enum.Enum): 351 352 """Special values which are injected when running a command handler.""" 353 354 count = enum.auto() 355 win_id = enum.auto() 356 cur_tab = enum.auto() 357 count_tab = enum.auto() 358 359 360class Question(QObject): 361 362 """A question asked to the user, e.g. via the status bar. 363 364 Note the creator is responsible for cleaning up the question after it 365 doesn't need it anymore, e.g. via connecting Question.completed to 366 Question.deleteLater. 367 368 Attributes: 369 mode: A PromptMode enum member. 370 yesno: A question which can be answered with yes/no. 371 text: A question which requires a free text answer. 372 user_pwd: A question for a username and password. 373 default: The default value. 374 For yesno, None (no default), True or False. 375 For text, a default text as string. 376 For user_pwd, a default username as string. 377 title: The question title to show. 378 text: The prompt text to display to the user. 379 url: Any URL referenced in prompts. 380 option: Boolean option to be set when answering always/never. 381 answer: The value the user entered (as password for user_pwd). 382 is_aborted: Whether the question was aborted. 383 interrupted: Whether the question was interrupted by another one. 384 385 Signals: 386 answered: Emitted when the question has been answered by the user. 387 arg: The answer to the question. 388 cancelled: Emitted when the question has been cancelled by the user. 389 aborted: Emitted when the question was aborted programmatically. 390 In this case, cancelled is not emitted. 391 answered_yes: Convenience signal emitted when a yesno question was 392 answered with yes. 393 answered_no: Convenience signal emitted when a yesno question was 394 answered with no. 395 completed: Emitted when the question was completed in any way. 396 """ 397 398 answered = pyqtSignal(object) 399 cancelled = pyqtSignal() 400 aborted = pyqtSignal() 401 answered_yes = pyqtSignal() 402 answered_no = pyqtSignal() 403 completed = pyqtSignal() 404 405 def __init__(self, parent: QObject = None) -> None: 406 super().__init__(parent) 407 self.mode: Optional[PromptMode] = None 408 self.default: Union[bool, str, None] = None 409 self.title: Optional[str] = None 410 self.text: Optional[str] = None 411 self.url: Optional[str] = None 412 self.option: Optional[bool] = None 413 self.answer: Union[str, bool, None] = None 414 self.is_aborted = False 415 self.interrupted = False 416 417 def __repr__(self) -> str: 418 return utils.get_repr(self, title=self.title, text=self.text, 419 mode=self.mode, default=self.default, 420 option=self.option) 421 422 @pyqtSlot() 423 def done(self) -> None: 424 """Must be called when the question was answered completely.""" 425 self.answered.emit(self.answer) 426 if self.mode == PromptMode.yesno: 427 if self.answer: 428 self.answered_yes.emit() 429 else: 430 self.answered_no.emit() 431 self.completed.emit() 432 433 @pyqtSlot() 434 def cancel(self) -> None: 435 """Cancel the question (resulting from user-input).""" 436 self.cancelled.emit() 437 self.completed.emit() 438 439 @pyqtSlot() 440 def abort(self) -> None: 441 """Abort the question.""" 442 if self.is_aborted: 443 log.misc.debug("Question was already aborted") 444 return 445 self.is_aborted = True 446 self.aborted.emit() 447 self.completed.emit() 448 449 450class Timer(QTimer): 451 452 """A timer which has a name to show in __repr__ and checks for overflows. 453 454 Attributes: 455 _name: The name of the timer. 456 """ 457 458 def __init__(self, parent: QObject = None, name: str = None) -> None: 459 super().__init__(parent) 460 if name is None: 461 self._name = "unnamed" 462 else: 463 self.setObjectName(name) 464 self._name = name 465 466 def __repr__(self) -> str: 467 return utils.get_repr(self, name=self._name) 468 469 def setInterval(self, msec: int) -> None: 470 """Extend setInterval to check for overflows.""" 471 qtutils.check_overflow(msec, 'int') 472 super().setInterval(msec) 473 474 def start(self, msec: int = None) -> None: 475 """Extend start to check for overflows.""" 476 if msec is not None: 477 qtutils.check_overflow(msec, 'int') 478 super().start(msec) 479 else: 480 super().start() 481 482 483class AbstractCertificateErrorWrapper: 484 485 """A wrapper over an SSL/certificate error.""" 486 487 def __str__(self) -> str: 488 raise NotImplementedError 489 490 def __repr__(self) -> str: 491 raise NotImplementedError 492 493 def is_overridable(self) -> bool: 494 raise NotImplementedError 495 496 def html(self) -> str: 497 return f'<p>{html.escape(str(self))}</p>' 498 499 500@dataclasses.dataclass 501class NavigationRequest: 502 503 """A request to navigate to the given URL.""" 504 505 class Type(enum.Enum): 506 507 """The type of a request. 508 509 Based on QWebEngineUrlRequestInfo::NavigationType and QWebPage::NavigationType. 510 """ 511 512 #: Navigation initiated by clicking a link. 513 link_clicked = 1 514 #: Navigation explicitly initiated by typing a URL (QtWebEngine only). 515 typed = 2 516 #: Navigation submits a form. 517 form_submitted = 3 518 #: An HTML form was submitted a second time (QtWebKit only). 519 form_resubmitted = 4 520 #: Navigation initiated by a history action. 521 back_forward = 5 522 #: Navigation initiated by refreshing the page. 523 reloaded = 6 524 #: Navigation triggered automatically by page content or remote server 525 #: (QtWebEngine >= 5.14 only) 526 redirect = 7 527 #: None of the above. 528 other = 8 529 530 url: QUrl 531 navigation_type: Type 532 is_main_frame: bool 533 accepted: bool = True 534