diff --git a/doc/TODO b/doc/TODO index 27eefdc4a..21b65d501 100644 --- a/doc/TODO +++ b/doc/TODO @@ -55,7 +55,6 @@ Downloads Improvements / minor features ============================= -- Make editor encoding configurable - Sane default for editor (maybe via QDesktopServices or so?) - We should have something like utils.debug.qenum_key for QFlags. - Honour icognito mode for cookies etc. diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 3d64653f3..2f8174a4f 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -194,6 +194,10 @@ DATA = OrderedDict([ "Use `{}` for the filename. The value gets split like in a shell, so " "you can use `\"` or `'` to quote arguments."), + ('editor-encoding', + SettingValue(types.Encoding(), 'utf-8'), + "Encoding to use for editor."), + ('private-browsing', SettingValue(types.Bool(), 'false'), "Do not record visited pages in the history or store web page " diff --git a/qutebrowser/config/conftypes.py b/qutebrowser/config/conftypes.py index 295b5de2c..d1509324f 100644 --- a/qutebrowser/config/conftypes.py +++ b/qutebrowser/config/conftypes.py @@ -21,6 +21,7 @@ import re import shlex +import codecs import os.path from sre_constants import error as RegexError @@ -1060,6 +1061,24 @@ class KeyBinding(Command): pass +class Encoding(BaseType): + + """Setting for a python encoding.""" + + typestr = 'encoding' + + def validate(self, value): + if not value: + if self.none_ok: + return + else: + raise ValidationError(value, "may not be empty!") + try: + codecs.lookup(value) + except LookupError: + raise ValidationError(value, "is not a valid encoding!") + + class WebSettingsFile(File): """QWebSettings file.""" diff --git a/qutebrowser/test/config/test_conftypes.py b/qutebrowser/test/config/test_conftypes.py index 3b9cbee34..2d017b05d 100644 --- a/qutebrowser/test/config/test_conftypes.py +++ b/qutebrowser/test/config/test_conftypes.py @@ -1826,7 +1826,7 @@ class AutoSearchTests(unittest.TestCase): self.assertIsNone(self.t.transform('')) -class IgnoreCase(unittest.TestCase): +class IgnoreCaseTests(unittest.TestCase): """Test IgnoreCase.""" @@ -1876,5 +1876,42 @@ class IgnoreCase(unittest.TestCase): self.assertIsNone(self.t.transform('')) +class EncodingTests(unittest.TestCase): + + """Test Encoding.""" + + def setUp(self): + self.t = conftypes.Encoding() + + def test_validate_empty(self): + """Test validate with empty string and none_ok = False.""" + with self.assertRaises(conftypes.ValidationError): + self.t.validate('') + + def test_validate_empty_none_ok(self): + """Test validate with empty string and none_ok = True.""" + t = conftypes.Encoding(none_ok=True) + t.validate('') + + def test_validate_valid(self): + """Test validate with valid values.""" + for val in ('utf-8', 'UTF-8', 'iso8859-1'): + with self.subTest(val=val): + self.t.validate(val) + + def test_validate_invalid(self): + """Test validate with an invalid value.""" + with self.assertRaises(conftypes.ValidationError): + self.t.validate('blubber') + + def test_transform(self): + """Test if transform doesn't alter the value.""" + self.assertEqual(self.t.transform('utf-8'), 'utf-8') + + def test_transform_empty(self): + """Test transform with none_ok = False and an empty value.""" + self.assertIsNone(self.t.transform('')) + + if __name__ == '__main__': unittest.main() diff --git a/qutebrowser/test/stubs.py b/qutebrowser/test/stubs.py index 01b0ee118..d8cce8d92 100644 --- a/qutebrowser/test/stubs.py +++ b/qutebrowser/test/stubs.py @@ -61,11 +61,11 @@ class ConfigStub: def get(self, sect, opt): """Get a value from the config.""" - sect = self.data[sect] + data = self.data[sect] try: - return sect[opt] + return data[opt] except KeyError: - raise self.NoOptionError + raise self.NoOptionError('{} -> {}'.format(sect, opt)) class FakeKeyEvent: diff --git a/qutebrowser/test/utils/test_editor.py b/qutebrowser/test/utils/test_editor.py index 9f3cedb84..15c3ea54e 100644 --- a/qutebrowser/test/utils/test_editor.py +++ b/qutebrowser/test/utils/test_editor.py @@ -56,21 +56,24 @@ class ArgTests(unittest.TestCase): def test_simple_start_args(self): """Test starting editor without arguments.""" - editorutils.config = ConfigStub({'general': {'editor': ['bin']}}) + editorutils.config = ConfigStub( + {'general': {'editor': ['bin'], 'editor-encoding': 'utf-8'}}) self.editor.edit("") self.editor.proc.start.assert_called_with("bin", []) def test_start_args(self): """Test starting editor with static arguments.""" editorutils.config = ConfigStub( - {'general': {'editor': ['bin', 'foo', 'bar']}}) + {'general': {'editor': ['bin', 'foo', 'bar'], + 'editor-encoding': 'utf-8'}}) self.editor.edit("") self.editor.proc.start.assert_called_with("bin", ["foo", "bar"]) def test_placeholder(self): """Test starting editor with placeholder argument.""" editorutils.config = ConfigStub( - {'general': {'editor': ['bin', 'foo', '{}', 'bar']}}) + {'general': {'editor': ['bin', 'foo', '{}', 'bar'], + 'editor-encoding': 'utf-8'}}) self.editor.edit("") filename = self.editor.filename self.editor.proc.start.assert_called_with("bin", @@ -79,7 +82,8 @@ class ArgTests(unittest.TestCase): def test_in_arg_placeholder(self): """Test starting editor with placeholder argument inside argument.""" editorutils.config = ConfigStub( - {'general': {'editor': ['bin', 'foo{}bar']}}) + {'general': {'editor': ['bin', 'foo{}bar'], + 'editor-encoding': 'utf-8'}}) self.editor.edit("") self.editor.proc.start.assert_called_with("bin", ["foo{}bar"]) @@ -97,7 +101,8 @@ class FileHandlingTests(unittest.TestCase): def setUp(self): self.editor = editorutils.ExternalEditor() - editorutils.config = ConfigStub({'general': {'editor': ['']}}) + editorutils.config = ConfigStub( + {'general': {'editor': [''], 'editor-encoding': 'utf-8'}}) def test_file_handling_closed_ok(self): """Test file handling when closing with an exitstatus == 0.""" @@ -136,7 +141,8 @@ class TextModifyTests(unittest.TestCase): def setUp(self): self.editor = editorutils.ExternalEditor() self.editor.editing_finished = Mock() - editorutils.config = ConfigStub({'general': {'editor': ['']}}) + editorutils.config = ConfigStub( + {'general': {'editor': [''], 'editor-encoding': 'utf-8'}}) def _write(self, text): """Write a text to the file opened in the fake editor. @@ -204,7 +210,8 @@ class ErrorMessageTests(unittest.TestCase): def setUp(self): self.editor = editorutils.ExternalEditor() - editorutils.config = ConfigStub({'general': {'editor': ['']}}) + editorutils.config = ConfigStub( + {'general': {'editor': [''], 'editor-encoding': 'utf-8'}}) def test_proc_error(self): """Test on_proc_error.""" diff --git a/qutebrowser/utils/editor.py b/qutebrowser/utils/editor.py index 29357c8b1..bd6591951 100644 --- a/qutebrowser/utils/editor.py +++ b/qutebrowser/utils/editor.py @@ -72,7 +72,8 @@ class ExternalEditor(QObject): message.error("Editor did quit abnormally (status {})!".format( exitcode)) return - with open(self.filename, 'r', encoding='utf-8') as f: + encoding = config.get('general', 'editor-encoding') + with open(self.filename, 'r', encoding=encoding) as f: text = ''.join(f.readlines()) logger.debug("Read back: {}".format(text)) self.editing_finished.emit(text) @@ -110,7 +111,8 @@ class ExternalEditor(QObject): self.text = text self.oshandle, self.filename = mkstemp(text=True) if text: - with open(self.filename, 'w', encoding='utf-8') as f: + encoding = config.get('general', 'editor-encoding') + with open(self.filename, 'w', encoding=encoding) as f: f.write(text) self.proc = QProcess(self) self.proc.finished.connect(self.on_proc_closed)