diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index 1d25727aa..2e2e585b0 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -33,6 +33,7 @@ It is possible to run or bind multiple commands by separating them with `;;`. |<>|Close the current window. |<>|Set all settings back to their default. |<>|Cycle an option between multiple values. +|<>|Open the config.py file in the editor. |<>|Read a config.py file. |<>|Unset an option. |<>|Download a given URL, or current page if no URL given. @@ -229,6 +230,15 @@ Cycle an option between multiple values. * +*-t*+, +*--temp*+: Set value temporarily until qutebrowser is closed. * +*-p*+, +*--print*+: Print the value after setting. +[[config-edit]] +=== config-edit +Syntax: +:config-edit [*--no-source*]+ + +Open the config.py file in the editor. + +==== optional arguments +* +*-n*+, +*--no-source*+: Don't re-source the config file after editing. + [[config-source]] === config-source Syntax: +:config-source [*--clear*] ['filename']+ diff --git a/qutebrowser/config/configcommands.py b/qutebrowser/config/configcommands.py index b63a959b7..12611cd27 100644 --- a/qutebrowser/config/configcommands.py +++ b/qutebrowser/config/configcommands.py @@ -28,6 +28,7 @@ from qutebrowser.commands import cmdexc, cmdutils from qutebrowser.completion.models import configmodel from qutebrowser.utils import objreg, utils, message, standarddir from qutebrowser.config import configtypes, configexc, configfiles +from qutebrowser.misc import editor class ConfigCommands: @@ -226,3 +227,27 @@ class ConfigCommands: configfiles.read_config_py(filename) except configexc.ConfigFileErrors as e: raise cmdexc.CommandError(e) + + @cmdutils.register(instance='config-commands') + def config_edit(self, no_source=False): + """Open the config.py file in the editor. + + Args: + no_source: Don't re-source the config file after editing. + """ + def on_editing_finished(): + """Source the new config when editing finished. + + This can't use cmdexc.CommandError as it's run async. + """ + try: + configfiles.read_config_py(filename) + except configexc.ConfigFileErrors as e: + message.error(str(e)) + + ed = editor.ExternalEditor(self._config) + if not no_source: + ed.editing_finished.connect(on_editing_finished) + + filename = os.path.join(standarddir.config(), 'config.py') + ed.edit_file(filename) diff --git a/tests/unit/config/test_configcommands.py b/tests/unit/config/test_configcommands.py index 33e68cf7e..b3836f634 100644 --- a/tests/unit/config/test_configcommands.py +++ b/tests/unit/config/test_configcommands.py @@ -18,8 +18,10 @@ """Tests for qutebrowser.config.configcommands.""" +import logging + import pytest -from PyQt5.QtCore import QUrl +from PyQt5.QtCore import QUrl, QProcess from qutebrowser.config import configcommands from qutebrowser.commands import cmdexc @@ -302,6 +304,53 @@ class TestSource: assert str(excinfo.value) == expected +class TestEdit: + + """Tests for :config-edit.""" + + def test_no_source(self, commands, mocker, config_tmpdir): + mock = mocker.patch('qutebrowser.config.configcommands.editor.' + 'ExternalEditor._start_editor', autospec=True) + commands.config_edit(no_source=True) + mock.assert_called_once() + + @pytest.fixture + def patch_editor(self, mocker, config_tmpdir, data_tmpdir): + """Write a config.py file.""" + def do_patch(text): + def _write_file(editor_self): + with open(editor_self._filename, 'w', encoding='utf-8') as f: + f.write(text) + editor_self.on_proc_closed(0, QProcess.NormalExit) + + return mocker.patch('qutebrowser.config.configcommands.editor.' + 'ExternalEditor._start_editor', autospec=True, + side_effect=_write_file) + + return do_patch + + def test_with_sourcing(self, commands, config_stub, patch_editor): + assert config_stub.val.content.javascript.enabled + mock = patch_editor('c.content.javascript.enabled = False') + + commands.config_edit() + + mock.assert_called_once() + assert not config_stub.val.content.javascript.enabled + + def test_error(self, commands, config_stub, patch_editor, message_mock, + caplog): + patch_editor('c.foo = 42') + + with caplog.at_level(logging.ERROR): + commands.config_edit() + + msg = message_mock.getmsg() + expected = ("Errors occurred while reading config.py:\n" + " While setting 'foo': No option 'foo'") + assert msg.text == expected + + class TestBind: """Tests for :bind and :unbind.""" diff --git a/tests/unit/misc/test_editor.py b/tests/unit/misc/test_editor.py index e6902852e..26a92476a 100644 --- a/tests/unit/misc/test_editor.py +++ b/tests/unit/misc/test_editor.py @@ -43,6 +43,7 @@ def editor(caplog): ed.editing_finished = mock.Mock() yield ed with caplog.at_level(logging.ERROR): + ed._remove_file = True ed._cleanup()