2014-06-19 09:04:37 +02:00
|
|
|
|
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
|
|
|
|
|
2016-01-04 07:12:39 +01:00
|
|
|
|
# Copyright 2014-2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
2014-02-10 17:54:24 +01:00
|
|
|
|
#
|
|
|
|
|
# This file is part of qutebrowser.
|
|
|
|
|
#
|
|
|
|
|
# qutebrowser is free software: you can redistribute it and/or modify
|
|
|
|
|
# it under the terms of the GNU General Public License as published by
|
|
|
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
|
|
|
# (at your option) any later version.
|
|
|
|
|
#
|
|
|
|
|
# qutebrowser is distributed in the hope that it will be useful,
|
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
|
# GNU General Public License for more details.
|
|
|
|
|
#
|
|
|
|
|
# You should have received a copy of the GNU General Public License
|
|
|
|
|
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
2015-03-26 07:08:13 +01:00
|
|
|
|
"""Other utilities which don't fit anywhere else."""
|
2014-02-17 12:23:52 +01:00
|
|
|
|
|
2014-08-13 06:09:18 +02:00
|
|
|
|
import io
|
2014-05-05 11:09:10 +02:00
|
|
|
|
import sys
|
2014-09-03 10:47:27 +02:00
|
|
|
|
import enum
|
2016-02-03 20:52:45 +01:00
|
|
|
|
import json
|
2014-05-08 22:33:24 +02:00
|
|
|
|
import os.path
|
2014-08-26 19:10:14 +02:00
|
|
|
|
import collections
|
|
|
|
|
import functools
|
|
|
|
|
import contextlib
|
2015-03-13 19:25:48 +01:00
|
|
|
|
import itertools
|
2014-02-11 10:26:37 +01:00
|
|
|
|
|
2014-10-08 06:19:45 +02:00
|
|
|
|
from PyQt5.QtCore import Qt
|
2016-02-03 20:27:11 +01:00
|
|
|
|
from PyQt5.QtGui import QKeySequence, QColor, QClipboard
|
|
|
|
|
from PyQt5.QtWidgets import QApplication
|
2014-08-26 19:10:14 +02:00
|
|
|
|
import pkg_resources
|
2014-05-08 22:33:24 +02:00
|
|
|
|
|
2014-02-14 16:32:56 +01:00
|
|
|
|
import qutebrowser
|
2014-09-23 04:22:51 +02:00
|
|
|
|
from qutebrowser.utils import qtutils, log
|
2014-06-16 10:18:04 +02:00
|
|
|
|
|
|
|
|
|
|
2016-02-03 20:27:11 +01:00
|
|
|
|
fake_clipboard = None
|
|
|
|
|
log_clipboard = False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SelectionUnsupportedError(Exception):
|
|
|
|
|
|
|
|
|
|
"""Raised if [gs]et_clipboard is used and selection=True is unsupported."""
|
|
|
|
|
|
|
|
|
|
|
2014-05-16 07:46:56 +02:00
|
|
|
|
def elide(text, length):
|
|
|
|
|
"""Elide text so it uses a maximum of length chars."""
|
|
|
|
|
if length < 1:
|
|
|
|
|
raise ValueError("length must be >= 1!")
|
|
|
|
|
if len(text) <= length:
|
|
|
|
|
return text
|
|
|
|
|
else:
|
2014-05-16 07:49:04 +02:00
|
|
|
|
return text[:length - 1] + '\u2026'
|
2014-05-16 07:46:56 +02:00
|
|
|
|
|
|
|
|
|
|
2014-07-16 09:15:52 +02:00
|
|
|
|
def compact_text(text, elidelength=None):
|
|
|
|
|
"""Remove leading whitespace and newlines from a text and maybe elide it.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
text: The text to compact.
|
|
|
|
|
elidelength: To how many chars to elide.
|
|
|
|
|
"""
|
2015-11-30 07:10:39 +01:00
|
|
|
|
lines = []
|
2014-07-16 09:15:52 +02:00
|
|
|
|
for line in text.splitlines():
|
2015-11-30 07:10:39 +01:00
|
|
|
|
lines.append(line.strip())
|
|
|
|
|
out = ''.join(lines)
|
2014-07-16 09:15:52 +02:00
|
|
|
|
if elidelength is not None:
|
|
|
|
|
out = elide(out, elidelength)
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
2014-12-28 14:50:25 +01:00
|
|
|
|
def read_file(filename, binary=False):
|
2014-02-19 10:58:32 +01:00
|
|
|
|
"""Get the contents of a file contained with qutebrowser.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
filename: The filename to open as string.
|
2014-12-22 23:44:09 +01:00
|
|
|
|
binary: Whether to return a binary string.
|
|
|
|
|
If False, the data is UTF-8-decoded.
|
2014-02-19 10:58:32 +01:00
|
|
|
|
|
|
|
|
|
Return:
|
|
|
|
|
The file contents as string.
|
|
|
|
|
"""
|
2014-05-13 10:39:37 +02:00
|
|
|
|
if hasattr(sys, 'frozen'):
|
|
|
|
|
# cx_Freeze doesn't support pkg_resources :(
|
|
|
|
|
fn = os.path.join(os.path.dirname(sys.executable), filename)
|
2014-12-22 23:44:09 +01:00
|
|
|
|
if binary:
|
|
|
|
|
with open(fn, 'rb') as f:
|
|
|
|
|
return f.read()
|
|
|
|
|
else:
|
|
|
|
|
with open(fn, 'r', encoding='utf-8') as f:
|
|
|
|
|
return f.read()
|
2014-05-13 10:39:37 +02:00
|
|
|
|
else:
|
2014-12-28 14:50:25 +01:00
|
|
|
|
data = pkg_resources.resource_string(qutebrowser.__name__, filename)
|
2014-12-22 23:44:09 +01:00
|
|
|
|
if not binary:
|
|
|
|
|
data = data.decode('UTF-8')
|
|
|
|
|
return data
|
2014-02-18 12:10:36 +01:00
|
|
|
|
|
|
|
|
|
|
2015-08-08 19:47:47 +02:00
|
|
|
|
def resource_filename(filename):
|
|
|
|
|
"""Get the absolute filename of a file contained with qutebrowser.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
filename: The filename.
|
|
|
|
|
|
|
|
|
|
Return:
|
|
|
|
|
The absolute filename.
|
|
|
|
|
"""
|
|
|
|
|
if hasattr(sys, 'frozen'):
|
|
|
|
|
return os.path.join(os.path.dirname(sys.executable), filename)
|
|
|
|
|
return pkg_resources.resource_filename(qutebrowser.__name__, filename)
|
|
|
|
|
|
|
|
|
|
|
2014-05-09 06:57:44 +02:00
|
|
|
|
def actute_warning():
|
|
|
|
|
"""Display a warning about the dead_actute issue if needed."""
|
2014-09-02 20:44:58 +02:00
|
|
|
|
# WORKAROUND (remove this when we bump the requirements to 5.3.0)
|
2015-03-31 20:49:29 +02:00
|
|
|
|
# Non Linux OS' aren't affected
|
2014-06-02 23:07:46 +02:00
|
|
|
|
if not sys.platform.startswith('linux'):
|
2014-05-09 06:57:44 +02:00
|
|
|
|
return
|
2014-06-02 23:07:46 +02:00
|
|
|
|
# If no compose file exists for some reason, we're not affected
|
|
|
|
|
if not os.path.exists('/usr/share/X11/locale/en_US.UTF-8/Compose'):
|
|
|
|
|
return
|
|
|
|
|
# Qt >= 5.3 doesn't seem to be affected
|
|
|
|
|
try:
|
2014-08-26 19:23:06 +02:00
|
|
|
|
if qtutils.version_check('5.3.0'):
|
2014-06-02 23:07:46 +02:00
|
|
|
|
return
|
2015-05-19 16:24:43 +02:00
|
|
|
|
except ValueError: # pragma: no cover
|
2014-06-02 23:07:46 +02:00
|
|
|
|
pass
|
2014-12-10 18:00:49 +01:00
|
|
|
|
try:
|
|
|
|
|
with open('/usr/share/X11/locale/en_US.UTF-8/Compose', 'r',
|
|
|
|
|
encoding='utf-8') as f:
|
2015-05-19 16:24:43 +02:00
|
|
|
|
for line in f: # pragma: no branch
|
2014-12-10 18:00:49 +01:00
|
|
|
|
if '<dead_actute>' in line:
|
|
|
|
|
if sys.stdout is not None:
|
|
|
|
|
sys.stdout.flush()
|
|
|
|
|
print("Note: If you got a 'dead_actute' warning above, "
|
|
|
|
|
"that is not a bug in qutebrowser! See "
|
|
|
|
|
"https://bugs.freedesktop.org/show_bug.cgi?id=69476 "
|
|
|
|
|
"for details.")
|
2015-05-19 16:24:43 +02:00
|
|
|
|
break # pragma: no branch
|
2014-12-10 18:00:49 +01:00
|
|
|
|
except OSError:
|
2015-04-13 08:49:04 +02:00
|
|
|
|
log.init.exception("Failed to read Compose file")
|
2014-06-10 11:54:14 +02:00
|
|
|
|
|
|
|
|
|
|
2014-06-12 21:43:30 +02:00
|
|
|
|
def _get_color_percentage(a_c1, a_c2, a_c3, b_c1, b_c2, b_c3, percent):
|
|
|
|
|
"""Get a color which is percent% interpolated between start and end.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
a_c1, a_c2, a_c3: Start color components (R, G, B / H, S, V / H, S, L)
|
|
|
|
|
b_c1, b_c2, b_c3: End color components (R, G, B / H, S, V / H, S, L)
|
|
|
|
|
percent: Percentage to interpolate, 0-100.
|
|
|
|
|
0: Start color will be returned.
|
|
|
|
|
100: End color will be returned.
|
|
|
|
|
|
|
|
|
|
Return:
|
|
|
|
|
A (c1, c2, c3) tuple with the interpolated color components.
|
|
|
|
|
"""
|
|
|
|
|
if not 0 <= percent <= 100:
|
|
|
|
|
raise ValueError("percent needs to be between 0 and 100!")
|
|
|
|
|
out_c1 = round(a_c1 + (b_c1 - a_c1) * percent / 100)
|
|
|
|
|
out_c2 = round(a_c2 + (b_c2 - a_c2) * percent / 100)
|
|
|
|
|
out_c3 = round(a_c3 + (b_c3 - a_c3) * percent / 100)
|
|
|
|
|
return (out_c1, out_c2, out_c3)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def interpolate_color(start, end, percent, colorspace=QColor.Rgb):
|
|
|
|
|
"""Get an interpolated color value.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
start: The start color.
|
|
|
|
|
end: The end color.
|
|
|
|
|
percent: Which value to get (0 - 100)
|
2015-03-31 20:49:29 +02:00
|
|
|
|
colorspace: The desired interpolation color system,
|
2014-06-12 21:43:30 +02:00
|
|
|
|
QColor::{Rgb,Hsv,Hsl} (from QColor::Spec enum)
|
2015-10-24 16:01:32 +02:00
|
|
|
|
If None, start is used except when percent is 100.
|
2014-06-12 21:43:30 +02:00
|
|
|
|
|
|
|
|
|
Return:
|
|
|
|
|
The interpolated QColor, with the same spec as the given start color.
|
|
|
|
|
"""
|
2014-08-26 19:23:06 +02:00
|
|
|
|
qtutils.ensure_valid(start)
|
|
|
|
|
qtutils.ensure_valid(end)
|
2015-10-24 16:01:32 +02:00
|
|
|
|
|
|
|
|
|
if colorspace is None:
|
|
|
|
|
if percent == 100:
|
2015-10-26 06:45:36 +01:00
|
|
|
|
return QColor(*end.getRgb())
|
2015-10-24 16:01:32 +02:00
|
|
|
|
else:
|
2015-10-26 06:45:36 +01:00
|
|
|
|
return QColor(*start.getRgb())
|
2015-10-24 16:01:32 +02:00
|
|
|
|
|
2014-06-12 21:43:30 +02:00
|
|
|
|
out = QColor()
|
|
|
|
|
if colorspace == QColor.Rgb:
|
|
|
|
|
a_c1, a_c2, a_c3, _alpha = start.getRgb()
|
|
|
|
|
b_c1, b_c2, b_c3, _alpha = end.getRgb()
|
|
|
|
|
components = _get_color_percentage(a_c1, a_c2, a_c3, b_c1, b_c2, b_c3,
|
|
|
|
|
percent)
|
|
|
|
|
out.setRgb(*components)
|
|
|
|
|
elif colorspace == QColor.Hsv:
|
|
|
|
|
a_c1, a_c2, a_c3, _alpha = start.getHsv()
|
|
|
|
|
b_c1, b_c2, b_c3, _alpha = end.getHsv()
|
|
|
|
|
components = _get_color_percentage(a_c1, a_c2, a_c3, b_c1, b_c2, b_c3,
|
|
|
|
|
percent)
|
|
|
|
|
out.setHsv(*components)
|
|
|
|
|
elif colorspace == QColor.Hsl:
|
|
|
|
|
a_c1, a_c2, a_c3, _alpha = start.getHsl()
|
|
|
|
|
b_c1, b_c2, b_c3, _alpha = end.getHsl()
|
|
|
|
|
components = _get_color_percentage(a_c1, a_c2, a_c3, b_c1, b_c2, b_c3,
|
|
|
|
|
percent)
|
|
|
|
|
out.setHsl(*components)
|
|
|
|
|
else:
|
|
|
|
|
raise ValueError("Invalid colorspace!")
|
2014-06-21 16:42:58 +02:00
|
|
|
|
out = out.convertTo(start.spec())
|
2014-08-26 19:23:06 +02:00
|
|
|
|
qtutils.ensure_valid(out)
|
2014-06-21 16:42:58 +02:00
|
|
|
|
return out
|
2014-06-12 23:29:34 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def format_seconds(total_seconds):
|
|
|
|
|
"""Format a count of seconds to get a [H:]M:SS string."""
|
|
|
|
|
prefix = '-' if total_seconds < 0 else ''
|
|
|
|
|
hours, rem = divmod(abs(round(total_seconds)), 3600)
|
|
|
|
|
minutes, seconds = divmod(rem, 60)
|
|
|
|
|
chunks = []
|
|
|
|
|
if hours:
|
|
|
|
|
chunks.append(str(hours))
|
|
|
|
|
min_format = '{:02}'
|
|
|
|
|
else:
|
|
|
|
|
min_format = '{}'
|
|
|
|
|
chunks.append(min_format.format(minutes))
|
|
|
|
|
chunks.append('{:02}'.format(seconds))
|
|
|
|
|
return prefix + ':'.join(chunks)
|
2014-06-13 07:13:47 +02:00
|
|
|
|
|
|
|
|
|
|
2015-03-22 22:39:56 +01:00
|
|
|
|
def format_timedelta(td):
|
|
|
|
|
"""Format a timedelta to get a "1h 5m 1s" string."""
|
|
|
|
|
prefix = '-' if td.total_seconds() < 0 else ''
|
|
|
|
|
hours, rem = divmod(abs(round(td.total_seconds())), 3600)
|
|
|
|
|
minutes, seconds = divmod(rem, 60)
|
|
|
|
|
chunks = []
|
|
|
|
|
if hours:
|
|
|
|
|
chunks.append('{}h'.format(hours))
|
|
|
|
|
if minutes:
|
|
|
|
|
chunks.append('{}m'.format(minutes))
|
|
|
|
|
if seconds or not chunks:
|
|
|
|
|
chunks.append('{}s'.format(seconds))
|
|
|
|
|
return prefix + ' '.join(chunks)
|
|
|
|
|
|
|
|
|
|
|
2014-06-13 07:13:47 +02:00
|
|
|
|
def format_size(size, base=1024, suffix=''):
|
|
|
|
|
"""Format a byte size so it's human readable.
|
|
|
|
|
|
|
|
|
|
Inspired by http://stackoverflow.com/q/1094841
|
|
|
|
|
"""
|
|
|
|
|
prefixes = ['', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']
|
|
|
|
|
if size is None:
|
|
|
|
|
return '?.??' + suffix
|
|
|
|
|
for p in prefixes:
|
|
|
|
|
if -base < size < base:
|
|
|
|
|
return '{:.02f}{}{}'.format(size, p, suffix)
|
|
|
|
|
size /= base
|
|
|
|
|
return '{:.02f}{}{}'.format(size, prefixes[-1], suffix)
|
2014-06-23 12:15:10 +02:00
|
|
|
|
|
|
|
|
|
|
2014-07-03 06:51:24 +02:00
|
|
|
|
def key_to_string(key):
|
|
|
|
|
"""Convert a Qt::Key member to a meaningful name.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
key: A Qt::Key member.
|
|
|
|
|
|
|
|
|
|
Return:
|
|
|
|
|
A name of the key as a string.
|
|
|
|
|
"""
|
2014-07-30 18:55:08 +02:00
|
|
|
|
special_names_str = {
|
2014-07-03 07:13:37 +02:00
|
|
|
|
# Some keys handled in a weird way by QKeySequence::toString.
|
2015-06-12 16:59:33 +02:00
|
|
|
|
# See https://bugreports.qt.io/browse/QTBUG-40030
|
2014-07-03 07:13:37 +02:00
|
|
|
|
# Most are unlikely to be ever needed, but you never know ;)
|
|
|
|
|
# For dead/combining keys, we return the corresponding non-combining
|
|
|
|
|
# key, as that's easier to add to the config.
|
2014-07-30 18:55:08 +02:00
|
|
|
|
'Key_Blue': 'Blue',
|
|
|
|
|
'Key_Calendar': 'Calendar',
|
|
|
|
|
'Key_ChannelDown': 'Channel Down',
|
|
|
|
|
'Key_ChannelUp': 'Channel Up',
|
|
|
|
|
'Key_ContrastAdjust': 'Contrast Adjust',
|
|
|
|
|
'Key_Dead_Abovedot': '˙',
|
|
|
|
|
'Key_Dead_Abovering': '˚',
|
|
|
|
|
'Key_Dead_Acute': '´',
|
|
|
|
|
'Key_Dead_Belowdot': 'Belowdot',
|
|
|
|
|
'Key_Dead_Breve': '˘',
|
|
|
|
|
'Key_Dead_Caron': 'ˇ',
|
|
|
|
|
'Key_Dead_Cedilla': '¸',
|
|
|
|
|
'Key_Dead_Circumflex': '^',
|
|
|
|
|
'Key_Dead_Diaeresis': '¨',
|
|
|
|
|
'Key_Dead_Doubleacute': '˝',
|
|
|
|
|
'Key_Dead_Grave': '`',
|
|
|
|
|
'Key_Dead_Hook': 'Hook',
|
|
|
|
|
'Key_Dead_Horn': 'Horn',
|
|
|
|
|
'Key_Dead_Iota': 'Iota',
|
|
|
|
|
'Key_Dead_Macron': '¯',
|
|
|
|
|
'Key_Dead_Ogonek': '˛',
|
|
|
|
|
'Key_Dead_Semivoiced_Sound': 'Semivoiced Sound',
|
|
|
|
|
'Key_Dead_Tilde': '~',
|
|
|
|
|
'Key_Dead_Voiced_Sound': 'Voiced Sound',
|
|
|
|
|
'Key_Exit': 'Exit',
|
|
|
|
|
'Key_Green': 'Green',
|
|
|
|
|
'Key_Guide': 'Guide',
|
|
|
|
|
'Key_Info': 'Info',
|
|
|
|
|
'Key_LaunchG': 'LaunchG',
|
|
|
|
|
'Key_LaunchH': 'LaunchH',
|
|
|
|
|
'Key_MediaLast': 'MediaLast',
|
|
|
|
|
'Key_Memo': 'Memo',
|
|
|
|
|
'Key_MicMute': 'Mic Mute',
|
|
|
|
|
'Key_Mode_switch': 'Mode switch',
|
|
|
|
|
'Key_Multi_key': 'Multi key',
|
|
|
|
|
'Key_PowerDown': 'Power Down',
|
|
|
|
|
'Key_Red': 'Red',
|
|
|
|
|
'Key_Settings': 'Settings',
|
|
|
|
|
'Key_SingleCandidate': 'Single Candidate',
|
|
|
|
|
'Key_ToDoList': 'Todo List',
|
|
|
|
|
'Key_TouchpadOff': 'Touchpad Off',
|
|
|
|
|
'Key_TouchpadOn': 'Touchpad On',
|
|
|
|
|
'Key_TouchpadToggle': 'Touchpad toggle',
|
|
|
|
|
'Key_Yellow': 'Yellow',
|
2015-06-12 16:37:17 +02:00
|
|
|
|
'Key_Alt': 'Alt',
|
|
|
|
|
'Key_AltGr': 'AltGr',
|
|
|
|
|
'Key_Control': 'Control',
|
|
|
|
|
'Key_Direction_L': 'Direction L',
|
|
|
|
|
'Key_Direction_R': 'Direction R',
|
|
|
|
|
'Key_Hyper_L': 'Hyper L',
|
|
|
|
|
'Key_Hyper_R': 'Hyper R',
|
|
|
|
|
'Key_Meta': 'Meta',
|
|
|
|
|
'Key_Shift': 'Shift',
|
|
|
|
|
'Key_Super_L': 'Super L',
|
|
|
|
|
'Key_Super_R': 'Super R',
|
|
|
|
|
'Key_unknown': 'Unknown',
|
2014-07-03 07:13:37 +02:00
|
|
|
|
}
|
2014-07-30 18:55:08 +02:00
|
|
|
|
# We now build our real special_names dict from the string mapping above.
|
|
|
|
|
# The reason we don't do this directly is that certain Qt versions don't
|
|
|
|
|
# have all the keys, so we want to ignore AttributeErrors.
|
|
|
|
|
special_names = {}
|
|
|
|
|
for k, v in special_names_str.items():
|
|
|
|
|
try:
|
|
|
|
|
special_names[getattr(Qt, k)] = v
|
|
|
|
|
except AttributeError:
|
|
|
|
|
pass
|
|
|
|
|
# Now we check if the key is any special one - if not, we use
|
|
|
|
|
# QKeySequence::toString.
|
2014-07-03 07:13:37 +02:00
|
|
|
|
try:
|
|
|
|
|
return special_names[key]
|
|
|
|
|
except KeyError:
|
2014-07-30 18:55:34 +02:00
|
|
|
|
name = QKeySequence(key).toString()
|
2014-07-03 07:52:58 +02:00
|
|
|
|
morphings = {
|
|
|
|
|
'Backtab': 'Tab',
|
|
|
|
|
'Esc': 'Escape',
|
|
|
|
|
}
|
|
|
|
|
if name in morphings:
|
|
|
|
|
return morphings[name]
|
|
|
|
|
else:
|
|
|
|
|
return name
|
2014-07-03 06:51:24 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def keyevent_to_string(e):
|
|
|
|
|
"""Convert a QKeyEvent to a meaningful name.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
e: A QKeyEvent.
|
|
|
|
|
|
|
|
|
|
Return:
|
|
|
|
|
A name of the key (combination) as a string or
|
|
|
|
|
None if only modifiers are pressed..
|
|
|
|
|
"""
|
2014-09-25 22:34:44 +02:00
|
|
|
|
if sys.platform == 'darwin':
|
2015-05-17 18:44:36 +02:00
|
|
|
|
# Qt swaps Ctrl/Meta on OS X, so we switch it back here so the user can
|
|
|
|
|
# use it in the config as expected. See:
|
2014-10-01 22:23:27 +02:00
|
|
|
|
# https://github.com/The-Compiler/qutebrowser/issues/110
|
2015-05-17 18:44:36 +02:00
|
|
|
|
# http://doc.qt.io/qt-5.4/osx-issues.html#special-keys
|
2015-05-17 18:59:40 +02:00
|
|
|
|
modmask2str = collections.OrderedDict([
|
|
|
|
|
(Qt.MetaModifier, 'Ctrl'),
|
|
|
|
|
(Qt.AltModifier, 'Alt'),
|
|
|
|
|
(Qt.ControlModifier, 'Meta'),
|
|
|
|
|
(Qt.ShiftModifier, 'Shift'),
|
|
|
|
|
])
|
|
|
|
|
else:
|
|
|
|
|
modmask2str = collections.OrderedDict([
|
|
|
|
|
(Qt.ControlModifier, 'Ctrl'),
|
|
|
|
|
(Qt.AltModifier, 'Alt'),
|
|
|
|
|
(Qt.MetaModifier, 'Meta'),
|
|
|
|
|
(Qt.ShiftModifier, 'Shift'),
|
|
|
|
|
])
|
2014-07-03 06:51:24 +02:00
|
|
|
|
modifiers = (Qt.Key_Control, Qt.Key_Alt, Qt.Key_Shift, Qt.Key_Meta,
|
|
|
|
|
Qt.Key_AltGr, Qt.Key_Super_L, Qt.Key_Super_R,
|
2014-07-03 07:13:37 +02:00
|
|
|
|
Qt.Key_Hyper_L, Qt.Key_Hyper_R, Qt.Key_Direction_L,
|
|
|
|
|
Qt.Key_Direction_R)
|
2014-07-03 06:51:24 +02:00
|
|
|
|
if e.key() in modifiers:
|
|
|
|
|
# Only modifier pressed
|
|
|
|
|
return None
|
|
|
|
|
mod = e.modifiers()
|
|
|
|
|
parts = []
|
|
|
|
|
for (mask, s) in modmask2str.items():
|
2014-09-26 07:31:59 +02:00
|
|
|
|
if mod & mask and s not in parts:
|
2014-07-03 06:51:24 +02:00
|
|
|
|
parts.append(s)
|
|
|
|
|
parts.append(key_to_string(e.key()))
|
|
|
|
|
return '+'.join(parts)
|
2014-07-03 07:34:09 +02:00
|
|
|
|
|
|
|
|
|
|
2015-11-19 07:35:14 +01:00
|
|
|
|
class KeyInfo:
|
|
|
|
|
|
|
|
|
|
"""Stores information about a key, like used in a QKeyEvent.
|
|
|
|
|
|
|
|
|
|
Attributes:
|
|
|
|
|
key: Qt::Key
|
|
|
|
|
modifiers: Qt::KeyboardModifiers
|
|
|
|
|
text: str
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def __init__(self, key, modifiers, text):
|
|
|
|
|
self.key = key
|
|
|
|
|
self.modifiers = modifiers
|
|
|
|
|
self.text = text
|
|
|
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
|
# Meh, dependency cycle...
|
|
|
|
|
from qutebrowser.utils.debug import qenum_key
|
|
|
|
|
if self.modifiers is None:
|
|
|
|
|
modifiers = None
|
|
|
|
|
else:
|
|
|
|
|
#modifiers = qflags_key(Qt, self.modifiers)
|
|
|
|
|
modifiers = hex(int(self.modifiers))
|
|
|
|
|
return get_repr(self, constructor=True, key=qenum_key(Qt, self.key),
|
|
|
|
|
modifiers=modifiers, text=self.text)
|
|
|
|
|
|
|
|
|
|
def __eq__(self, other):
|
|
|
|
|
return (self.key == other.key and self.modifiers == other.modifiers and
|
|
|
|
|
self.text == other.text)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class KeyParseError(Exception):
|
|
|
|
|
|
|
|
|
|
"""Raised by _parse_single_key/parse_keystring on parse errors."""
|
|
|
|
|
|
|
|
|
|
def __init__(self, keystr, error):
|
|
|
|
|
super().__init__("Could not parse {!r}: {}".format(keystr, error))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _parse_single_key(keystr):
|
|
|
|
|
"""Convert a single key string to a (Qt.Key, Qt.Modifiers, text) tuple."""
|
|
|
|
|
if keystr.startswith('<') and keystr.endswith('>'):
|
|
|
|
|
# Special key
|
|
|
|
|
keystr = keystr[1:-1]
|
|
|
|
|
elif len(keystr) == 1:
|
|
|
|
|
# vim-like key
|
|
|
|
|
pass
|
|
|
|
|
else:
|
|
|
|
|
raise KeyParseError(keystr, "Expecting either a single key or a "
|
|
|
|
|
"<Ctrl-x> like keybinding.")
|
|
|
|
|
|
|
|
|
|
seq = QKeySequence(normalize_keystr(keystr), QKeySequence.PortableText)
|
|
|
|
|
if len(seq) != 1:
|
|
|
|
|
raise KeyParseError(keystr, "Got {} keys instead of 1.".format(
|
|
|
|
|
len(seq)))
|
|
|
|
|
result = seq[0]
|
|
|
|
|
|
|
|
|
|
if result == Qt.Key_unknown:
|
|
|
|
|
raise KeyParseError(keystr, "Got unknown key.")
|
|
|
|
|
|
|
|
|
|
modifier_mask = int(Qt.ShiftModifier | Qt.ControlModifier |
|
|
|
|
|
Qt.AltModifier | Qt.MetaModifier | Qt.KeypadModifier |
|
|
|
|
|
Qt.GroupSwitchModifier)
|
|
|
|
|
assert Qt.Key_unknown & ~modifier_mask == Qt.Key_unknown
|
|
|
|
|
|
|
|
|
|
modifiers = result & modifier_mask
|
|
|
|
|
key = result & ~modifier_mask
|
|
|
|
|
|
|
|
|
|
if len(keystr) == 1 and keystr.isupper():
|
|
|
|
|
modifiers |= Qt.ShiftModifier
|
|
|
|
|
|
|
|
|
|
assert key != 0, key
|
|
|
|
|
key = Qt.Key(key)
|
|
|
|
|
modifiers = Qt.KeyboardModifiers(modifiers)
|
|
|
|
|
|
|
|
|
|
# Let's hope this is accurate...
|
|
|
|
|
if len(keystr) == 1 and not modifiers:
|
|
|
|
|
text = keystr
|
|
|
|
|
elif len(keystr) == 1 and modifiers == Qt.ShiftModifier:
|
|
|
|
|
text = keystr.upper()
|
|
|
|
|
else:
|
|
|
|
|
text = ''
|
|
|
|
|
|
|
|
|
|
return KeyInfo(key, modifiers, text)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_keystring(keystr):
|
|
|
|
|
"""Parse a keystring like <Ctrl-x> or xyz and return a KeyInfo list."""
|
|
|
|
|
if keystr.startswith('<') and keystr.endswith('>'):
|
|
|
|
|
return [_parse_single_key(keystr)]
|
|
|
|
|
else:
|
|
|
|
|
return [_parse_single_key(char) for char in keystr]
|
|
|
|
|
|
|
|
|
|
|
2014-07-03 07:34:09 +02:00
|
|
|
|
def normalize_keystr(keystr):
|
|
|
|
|
"""Normalize a keystring like Ctrl-Q to a keystring like Ctrl+Q.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
keystr: The key combination as a string.
|
|
|
|
|
|
|
|
|
|
Return:
|
|
|
|
|
The normalized keystring.
|
|
|
|
|
"""
|
2015-04-13 22:08:57 +02:00
|
|
|
|
keystr = keystr.lower()
|
2014-07-03 07:34:09 +02:00
|
|
|
|
replacements = (
|
2015-04-13 22:08:57 +02:00
|
|
|
|
('control', 'ctrl'),
|
|
|
|
|
('windows', 'meta'),
|
|
|
|
|
('mod1', 'alt'),
|
|
|
|
|
('mod4', 'meta'),
|
2014-07-03 07:34:09 +02:00
|
|
|
|
)
|
|
|
|
|
for (orig, repl) in replacements:
|
|
|
|
|
keystr = keystr.replace(orig, repl)
|
2015-04-13 22:08:57 +02:00
|
|
|
|
for mod in ('ctrl', 'meta', 'alt', 'shift'):
|
2014-07-03 07:34:09 +02:00
|
|
|
|
keystr = keystr.replace(mod + '-', mod + '+')
|
2015-04-13 22:08:57 +02:00
|
|
|
|
return keystr
|
2014-08-13 06:09:18 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class FakeIOStream(io.TextIOBase):
|
|
|
|
|
|
|
|
|
|
"""A fake file-like stream which calls a function for write-calls."""
|
|
|
|
|
|
|
|
|
|
def __init__(self, write_func):
|
2015-10-26 21:07:08 +01:00
|
|
|
|
super().__init__()
|
2014-08-13 06:09:18 +02:00
|
|
|
|
self.write = write_func
|
|
|
|
|
|
|
|
|
|
def flush(self):
|
2015-03-26 07:08:13 +01:00
|
|
|
|
"""Override flush() to satisfy pylint."""
|
2014-08-13 06:09:18 +02:00
|
|
|
|
return super().flush()
|
|
|
|
|
|
|
|
|
|
def isatty(self):
|
2015-03-26 07:08:13 +01:00
|
|
|
|
"""Override isatty() to satisfy pylint."""
|
2014-08-13 06:09:18 +02:00
|
|
|
|
return super().isatty()
|
|
|
|
|
|
|
|
|
|
|
2014-08-26 19:10:14 +02:00
|
|
|
|
@contextlib.contextmanager
|
2014-08-13 06:09:18 +02:00
|
|
|
|
def fake_io(write_func):
|
|
|
|
|
"""Run code with stdout and stderr replaced by FakeIOStreams.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
write_func: The function to call when write is called.
|
|
|
|
|
"""
|
|
|
|
|
old_stdout = sys.stdout
|
|
|
|
|
old_stderr = sys.stderr
|
|
|
|
|
fake_stderr = FakeIOStream(write_func)
|
|
|
|
|
fake_stdout = FakeIOStream(write_func)
|
|
|
|
|
sys.stderr = fake_stderr
|
|
|
|
|
sys.stdout = fake_stdout
|
2015-02-26 17:47:07 +01:00
|
|
|
|
try:
|
|
|
|
|
yield
|
|
|
|
|
finally:
|
|
|
|
|
# If the code we did run did change sys.stdout/sys.stderr, we leave it
|
|
|
|
|
# unchanged. Otherwise, we reset it.
|
|
|
|
|
if sys.stdout is fake_stdout:
|
|
|
|
|
sys.stdout = old_stdout
|
|
|
|
|
if sys.stderr is fake_stderr:
|
|
|
|
|
sys.stderr = old_stderr
|
2014-08-13 06:09:18 +02:00
|
|
|
|
|
|
|
|
|
|
2014-08-26 19:10:14 +02:00
|
|
|
|
@contextlib.contextmanager
|
2014-08-13 06:09:18 +02:00
|
|
|
|
def disabled_excepthook():
|
2014-09-22 20:44:07 +02:00
|
|
|
|
"""Run code with the exception hook temporarily disabled."""
|
2014-08-13 06:09:18 +02:00
|
|
|
|
old_excepthook = sys.excepthook
|
|
|
|
|
sys.excepthook = sys.__excepthook__
|
2015-06-15 06:18:16 +02:00
|
|
|
|
try:
|
|
|
|
|
yield
|
|
|
|
|
finally:
|
|
|
|
|
# If the code we did run did change sys.excepthook, we leave it
|
|
|
|
|
# unchanged. Otherwise, we reset it.
|
|
|
|
|
if sys.excepthook is sys.__excepthook__:
|
|
|
|
|
sys.excepthook = old_excepthook
|
2014-09-16 22:06:48 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class prevent_exceptions: # pylint: disable=invalid-name
|
|
|
|
|
|
|
|
|
|
"""Decorator to ignore and log exceptions.
|
|
|
|
|
|
|
|
|
|
This needs to be used for some places where PyQt segfaults on exceptions or
|
|
|
|
|
silently ignores them.
|
|
|
|
|
|
2014-11-19 22:46:52 +01:00
|
|
|
|
We used to re-raise the exception with a single-shot QTimer in a similar
|
2015-03-31 20:49:29 +02:00
|
|
|
|
case, but that lead to a strange problem with a KeyError with some random
|
2014-09-16 22:06:48 +02:00
|
|
|
|
jinja template stuff as content. For now, we only log it, so it doesn't
|
|
|
|
|
pass 100% silently.
|
|
|
|
|
|
|
|
|
|
This could also be a function, but as a class (with a "wrong" name) it's
|
|
|
|
|
much cleaner to implement.
|
|
|
|
|
|
|
|
|
|
Attributes:
|
2014-09-24 22:17:53 +02:00
|
|
|
|
_retval: The value to return in case of an exception.
|
|
|
|
|
_predicate: The condition which needs to be True to prevent exceptions
|
2014-09-16 22:06:48 +02:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def __init__(self, retval, predicate=True):
|
|
|
|
|
"""Save decorator arguments.
|
|
|
|
|
|
|
|
|
|
Gets called on parse-time with the decorator arguments.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
See class attributes.
|
|
|
|
|
"""
|
2014-09-24 22:17:53 +02:00
|
|
|
|
self._retval = retval
|
|
|
|
|
self._predicate = predicate
|
2014-09-16 22:06:48 +02:00
|
|
|
|
|
|
|
|
|
def __call__(self, func):
|
2015-03-26 07:08:13 +01:00
|
|
|
|
"""Called when a function should be decorated.
|
2014-09-16 22:06:48 +02:00
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
func: The function to be decorated.
|
|
|
|
|
|
|
|
|
|
Return:
|
|
|
|
|
The decorated function.
|
|
|
|
|
"""
|
2014-09-24 22:17:53 +02:00
|
|
|
|
if not self._predicate:
|
2014-09-16 22:06:48 +02:00
|
|
|
|
return func
|
|
|
|
|
|
2014-09-24 22:17:53 +02:00
|
|
|
|
retval = self._retval
|
2014-09-16 22:06:48 +02:00
|
|
|
|
|
|
|
|
|
@functools.wraps(func)
|
2015-12-01 20:55:38 +01:00
|
|
|
|
def wrapper(*args, **kwargs):
|
2014-09-16 22:06:48 +02:00
|
|
|
|
try:
|
|
|
|
|
return func(*args, **kwargs)
|
|
|
|
|
except BaseException:
|
2014-10-08 21:11:04 +02:00
|
|
|
|
log.misc.exception("Error in {}".format(qualname(func)))
|
2014-09-16 22:06:48 +02:00
|
|
|
|
return retval
|
|
|
|
|
|
|
|
|
|
return wrapper
|
2014-09-22 19:09:48 +02:00
|
|
|
|
|
|
|
|
|
|
2014-09-03 10:47:27 +02:00
|
|
|
|
def is_enum(obj):
|
|
|
|
|
"""Check if a given object is an enum."""
|
|
|
|
|
try:
|
|
|
|
|
return issubclass(obj, enum.Enum)
|
|
|
|
|
except TypeError:
|
|
|
|
|
return False
|
2014-09-26 15:48:24 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_repr(obj, constructor=False, **attrs):
|
|
|
|
|
"""Get a suitable __repr__ string for an object.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
obj: The object to get a repr for.
|
|
|
|
|
constructor: If True, show the Foo(one=1, two=2) form instead of
|
|
|
|
|
<Foo one=1 two=2>.
|
|
|
|
|
attrs: The attributes to add.
|
|
|
|
|
"""
|
2014-10-08 21:11:04 +02:00
|
|
|
|
cls = qualname(obj.__class__)
|
2014-09-26 15:48:24 +02:00
|
|
|
|
parts = []
|
2015-05-19 16:23:50 +02:00
|
|
|
|
items = sorted(attrs.items())
|
|
|
|
|
for name, val in items:
|
2014-09-26 15:48:24 +02:00
|
|
|
|
parts.append('{}={!r}'.format(name, val))
|
|
|
|
|
if constructor:
|
|
|
|
|
return '{}({})'.format(cls, ', '.join(parts))
|
|
|
|
|
else:
|
|
|
|
|
if parts:
|
|
|
|
|
return '<{} {}>'.format(cls, ' '.join(parts))
|
|
|
|
|
else:
|
|
|
|
|
return '<{}>'.format(cls)
|
2014-10-08 21:11:04 +02:00
|
|
|
|
|
2014-10-08 22:20:38 +02:00
|
|
|
|
|
2014-10-08 21:11:04 +02:00
|
|
|
|
def qualname(obj):
|
|
|
|
|
"""Get the fully qualified name of an object.
|
|
|
|
|
|
|
|
|
|
Based on twisted.python.reflect.fullyQualifiedName.
|
|
|
|
|
|
|
|
|
|
Should work with:
|
|
|
|
|
- functools.partial objects
|
|
|
|
|
- functions
|
|
|
|
|
- classes
|
|
|
|
|
- methods
|
|
|
|
|
- modules
|
|
|
|
|
"""
|
|
|
|
|
if isinstance(obj, functools.partial):
|
|
|
|
|
obj = obj.func
|
2015-08-04 10:39:34 +02:00
|
|
|
|
|
|
|
|
|
if hasattr(obj, '__module__'):
|
|
|
|
|
prefix = '{}.'.format(obj.__module__)
|
2014-10-08 21:11:04 +02:00
|
|
|
|
else:
|
2015-08-04 10:39:34 +02:00
|
|
|
|
prefix = ''
|
2014-10-08 21:11:04 +02:00
|
|
|
|
|
2015-08-04 10:39:34 +02:00
|
|
|
|
if hasattr(obj, '__qualname__'):
|
|
|
|
|
return '{}{}'.format(prefix, obj.__qualname__)
|
|
|
|
|
elif hasattr(obj, '__name__'):
|
|
|
|
|
return '{}{}'.format(prefix, obj.__name__)
|
2014-10-08 21:11:04 +02:00
|
|
|
|
else:
|
2015-08-04 10:39:34 +02:00
|
|
|
|
return repr(obj)
|
2014-11-27 20:44:48 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def raises(exc, func, *args):
|
|
|
|
|
"""Check if a function raises a given exception.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
exc: A single exception or an iterable of exceptions.
|
|
|
|
|
func: A function to call.
|
|
|
|
|
*args: The arguments to pass to the function.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
True if the exception was raised, False otherwise.
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
func(*args)
|
|
|
|
|
except exc:
|
|
|
|
|
return True
|
|
|
|
|
else:
|
|
|
|
|
return False
|
2015-01-03 17:50:59 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def force_encoding(text, encoding):
|
|
|
|
|
"""Make sure a given text is encodable with the given encoding.
|
|
|
|
|
|
|
|
|
|
This replaces all chars not encodable with question marks.
|
|
|
|
|
"""
|
|
|
|
|
return text.encode(encoding, errors='replace').decode(encoding)
|
2015-03-13 19:25:48 +01:00
|
|
|
|
|
|
|
|
|
|
2015-10-30 21:08:37 +01:00
|
|
|
|
def sanitize_filename(name, replacement='_'):
|
|
|
|
|
"""Replace invalid filename characters.
|
|
|
|
|
|
|
|
|
|
Note: This should be used for the basename, as it also removes the path
|
|
|
|
|
separator.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
name: The filename.
|
|
|
|
|
replacement: The replacement character (or None).
|
|
|
|
|
"""
|
|
|
|
|
if replacement is None:
|
|
|
|
|
replacement = ''
|
|
|
|
|
# Bad characters taken from Windows, there are even fewer on Linux
|
|
|
|
|
# See also
|
|
|
|
|
# https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words
|
|
|
|
|
bad_chars = '\\/:*?"<>|'
|
|
|
|
|
for bad_char in bad_chars:
|
|
|
|
|
name = name.replace(bad_char, replacement)
|
|
|
|
|
return name
|
|
|
|
|
|
|
|
|
|
|
2015-03-13 19:25:48 +01:00
|
|
|
|
def newest_slice(iterable, count):
|
|
|
|
|
"""Get an iterable for the n newest items of the given iterable.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
count: How many elements to get.
|
|
|
|
|
0: get no items:
|
|
|
|
|
n: get the n newest items
|
|
|
|
|
-1: get all items
|
|
|
|
|
"""
|
|
|
|
|
if count < -1:
|
|
|
|
|
raise ValueError("count can't be smaller than -1!")
|
|
|
|
|
elif count == 0:
|
|
|
|
|
return []
|
|
|
|
|
elif count == -1 or len(iterable) < count:
|
|
|
|
|
return iterable
|
|
|
|
|
else:
|
|
|
|
|
return itertools.islice(iterable, len(iterable) - count, len(iterable))
|
2016-02-03 20:27:11 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def set_clipboard(data, selection=False):
|
|
|
|
|
"""Set the clipboard to some given data."""
|
|
|
|
|
clipboard = QApplication.clipboard()
|
|
|
|
|
if selection and not clipboard.supportsSelection():
|
|
|
|
|
raise SelectionUnsupportedError
|
|
|
|
|
if log_clipboard:
|
|
|
|
|
what = 'primary selection' if selection else 'clipboard'
|
2016-02-03 20:52:45 +01:00
|
|
|
|
log.misc.debug("Setting fake {}: {}".format(what, json.dumps(data)))
|
2016-02-03 20:27:11 +01:00
|
|
|
|
else:
|
|
|
|
|
mode = QClipboard.Selection if selection else QClipboard.Clipboard
|
|
|
|
|
clipboard.setText(data, mode=mode)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_clipboard(selection=False):
|
|
|
|
|
"""Get data from the clipboard."""
|
|
|
|
|
global fake_clipboard
|
|
|
|
|
clipboard = QApplication.clipboard()
|
|
|
|
|
if selection and not clipboard.supportsSelection():
|
|
|
|
|
raise SelectionUnsupportedError
|
|
|
|
|
|
|
|
|
|
if fake_clipboard is not None:
|
|
|
|
|
data = fake_clipboard
|
|
|
|
|
fake_clipboard = None
|
|
|
|
|
else:
|
|
|
|
|
mode = QClipboard.Selection if selection else QClipboard.Clipboard
|
|
|
|
|
data = clipboard.text(mode=mode)
|
|
|
|
|
|
|
|
|
|
return data
|