From 54cf4bc40fa9948c00081f3073760c40f2b8732a Mon Sep 17 00:00:00 2001 From: Ben Neil Date: Sun, 14 Sep 2025 10:25:05 -0600 Subject: [PATCH] feat(history): Added site specific clearing --- qutebrowser/browser/history.py | 57 ++++++++++++++++++++++++++++++ tests/unit/browser/test_history.py | 47 ++++++++++++++++++++++++ 2 files changed, 104 insertions(+) diff --git a/qutebrowser/browser/history.py b/qutebrowser/browser/history.py index ebcd26e72..0050e2176 100644 --- a/qutebrowser/browser/history.py +++ b/qutebrowser/browser/history.py @@ -452,6 +452,63 @@ def history_clear(force=False): title="Clear all browsing history?") +def _normalize_url(url: str) -> QUrl: + if not url: + return QUrl() + if '://' not in url: + url = f'https://{url}' + return QUrl(url) + + +def _find_matching_history_urls(qurl: QUrl) -> list[str]: + if not qurl.isValid(): + return [] + + target_host = qurl.host().lower() + matching_urls = [] + + for entry in web_history: + entry_qurl = QUrl(entry.url) + if entry_qurl.host().lower() == target_host: + matching_urls.append(entry.url) + + return matching_urls + + +def _delete_history_urls(urls: list[str]) -> None: + for url in urls: + web_history.delete_url(url) + + +@cmdutils.register() +def history_clear_site(url: str, force: bool = False): + """Clear browsing history for a specific site. + + Args: + url: URL or domain to clear (e.g., "example.com", "https://example.com/page") + force: Don't ask for confirmation. + """ + qurl = _normalize_url(url) + if not qurl.isValid(): + raise cmdutils.CommandError(f"Invalid URL: {url}") + + matching_urls = _find_matching_history_urls(qurl) + + if not matching_urls: + message.info(f"No history entries found for: {url}") + return + + if force: + _delete_history_urls(matching_urls) + message.info(f"Cleared {len(matching_urls)} history entries for: {url}") + else: + title = f"Clear {len(matching_urls)} history entries for {url}?" + message.confirm_async( + yes_action=lambda: _delete_history_urls(matching_urls), + title=title + ) + + @cmdutils.register(debug=True) def debug_dump_history(dest): """Dump the history to a file in the old pre-SQL format. diff --git a/tests/unit/browser/test_history.py b/tests/unit/browser/test_history.py index e46c685f4..873a166dd 100644 --- a/tests/unit/browser/test_history.py +++ b/tests/unit/browser/test_history.py @@ -326,6 +326,53 @@ class TestInit: assert default_interface is None +class TestHistoryClearSite: + + def test_normalize_url(self): + from qutebrowser.browser.history import _normalize_url + assert _normalize_url('example.com').toString() == 'https://example.com' + assert _normalize_url('https://example.com/page').toString() == 'https://example.com/page' + assert _normalize_url('').toString() == '' + + def test_find_matching_history_urls_exact(self, web_history): + from qutebrowser.browser.history import _find_matching_history_urls, _normalize_url + web_history.add_url(QUrl('https://example.com/page'), atime=12345) + qurl = _normalize_url('https://example.com/page') + matches = _find_matching_history_urls(qurl) + assert len(matches) == 1 + + def test_find_matching_history_urls_no_match(self, web_history): + from qutebrowser.browser.history import _find_matching_history_urls, _normalize_url + web_history.add_url(QUrl('https://example.com/page'), atime=12345) + qurl = _normalize_url('https://other.com') + matches = _find_matching_history_urls(qurl) + assert len(matches) == 0 + + def test_history_clear_site_force(self, web_history, mocker): + web_history.add_url(QUrl('https://example.com/page'), atime=12345) + m = mocker.patch('qutebrowser.browser.history.message.info') + history.history_clear_site('https://example.com/page', force=True) + assert m.called + assert not list(web_history) + + def test_history_clear_site_no_match(self, web_history, mocker): + web_history.add_url(QUrl('https://example.com/page'), atime=12345) + m = mocker.patch('qutebrowser.browser.history.message.info') + history.history_clear_site('https://other.com', force=True) + m.assert_called_once_with("No history entries found for: https://other.com") + assert len(web_history) == 1 + + def test_history_clear_site_invalid_url(self): + with pytest.raises(cmdutils.CommandError, match="Invalid URL"): + history.history_clear_site('not a url', force=True) + + def test_history_clear_site_confirm(self, web_history, mocker): + web_history.add_url(QUrl('https://example.com/page'), atime=12345) + m = mocker.patch('qutebrowser.browser.history.message.confirm_async') + history.history_clear_site('https://example.com/page') + assert m.called + + class TestDump: def test_debug_dump_history(self, web_history, tmpdir):