Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,21 @@
All notable changes to SearchMob Desktop are documented here. The version scheme is Ubuntu-style
`YY.MM.VV` and releases are tagged `vYY.MM.VV`.

## 26.05.12 — 2026-05-31

### Changed
- **Settings dialog redesign.** The sections moved from a row of top tabs to a left-hand navigation
column, and the API keys are now entered inline on the **Search engines** page: each key-requiring
engine has its key field directly under its checkbox, grayed out until you check that engine. The
standalone "API keys" section is gone.
- **Key-requiring engines are now unchecked by default.** The free engines stay on out of the box;
Brave / Mojeek API / Kagi start off and are turned on when you add their key (they could never run
without one anyway). Free engines are unaffected.

### Fixed
- Saving or clearing a **Kagi** key updated the right engine's status (previously it could update
Mojeek's), now that each engine owns its inline field.

## 26.05.11 — 2026-05-31

### Changed
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ follow_imports = "skip"
[tool.briefcase]
project_name = "SearchMob Desktop"
bundle = "com.flintwave"
version = "26.05.11"
version = "26.05.12"
url = "https://github.com/FlintWave/SearchMob-Desktop"
author = "FlintWave"
author_email = "flintwave@tuta.com"
Expand Down
17 changes: 12 additions & 5 deletions src/searchmob_desktop/gui/engines_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,19 @@ class EngineEntry:
)


# Default enabled-state per engine: the free engines are on out of the box, while the BYO-key
# engines start off (they cannot do anything until you add a key, so checking them by default would
# only ever be a no-op or a confusing "enabled but silent" engine).
_DEFAULT_ENABLED: dict[str, bool] = {e.id: not e.requires_api_key for e in ENGINE_CATALOG}


def is_engine_enabled(engine_id: str, engine_enabled: dict[str, bool] | None) -> bool:
"""Default-on lookup: an engine missing from the map counts as enabled.
"""Resolve an engine's enabled state, falling back to its default when the map has no entry.

Matches the Android `isEngineEnabled` so a fresh profile uses every free engine without the
user having to opt each one in.
Free engines default on (a fresh profile searches them without opt-in); key-requiring engines
default off (you turn one on when you add its key). An explicit entry in the map always wins.
"""
default = _DEFAULT_ENABLED.get(engine_id, True)
if not engine_enabled:
return True
return engine_enabled.get(engine_id, True)
return default
return engine_enabled.get(engine_id, default)
266 changes: 134 additions & 132 deletions src/searchmob_desktop/gui/settings_dialog.py

Large diffs are not rendered by default.

21 changes: 21 additions & 0 deletions src/searchmob_desktop/gui/theme.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,27 @@ def build_qss(p: Palette) -> str:
}}
QFrame#summaryCard QLabel {{ background: transparent; }}

/* Settings dialog: left-hand navigation column. */
QListWidget#settingsNav {{
background-color: {p.surface};
border: 1px solid {p.border};
border-radius: 10px;
padding: 4px;
outline: none;
}}
QListWidget#settingsNav::item {{
padding: 8px 12px;
border-radius: 7px;
color: {p.text};
}}
QListWidget#settingsNav::item:selected {{
background-color: {p.accent};
color: {p.on_accent};
}}
QListWidget#settingsNav::item:hover:!selected {{
background-color: {p.card_hover};
}}

/* Slim, modern scrollbars. */
QScrollBar:vertical {{ background: transparent; width: 10px; margin: 2px; }}
QScrollBar::handle:vertical {{ background: {p.border}; border-radius: 5px; min-height: 28px; }}
Expand Down
2 changes: 1 addition & 1 deletion src/searchmob_desktop/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
# Ubuntu-style scheme matching the Android app: YY.MM.VV (two-digit year,
# month, and per-month build). First desktop release is 26.05.00. Bump
# manually each release; hatchling reads __version__ from this file.
__version__ = "26.05.11"
__version__ = "26.05.12"
10 changes: 10 additions & 0 deletions tests/gui/test_pure_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,16 @@ def test_engine_catalog_respects_explicit_disable() -> None:
assert is_engine_enabled("mwmbl", {"mwmbl": True}) is True


def test_engine_catalog_api_engines_default_off() -> None:
# Key-requiring engines start off (they cannot run without a key); free engines start on.
assert is_engine_enabled("brave", None) is False
assert is_engine_enabled("mojeek-api", {}) is False
assert is_engine_enabled("kagi-api", None) is False
assert is_engine_enabled("wikipedia", None) is True
# An explicit enable still wins for an API engine.
assert is_engine_enabled("brave", {"brave": True}) is True


def test_engine_catalog_lists_expected_engines() -> None:
ids = {e.id for e in ENGINE_CATALOG}
# The five free engines + the two BYO-key ones must be in the catalog so the settings UI
Expand Down
38 changes: 38 additions & 0 deletions tests/gui/test_settings_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,44 @@ def test_engine_toggle_persists_engine_enabled(qapp: object, tmp_path: Path) ->
assert store.load().engine_enabled.get("duckduckgo") is True


def test_settings_uses_sidebar_nav_without_separate_api_keys_section(
qapp: object, tmp_path: Path
) -> None:
from PySide6.QtWidgets import QListWidget

store = _store(tmp_path)
dialog = _dialog(store)
nav = dialog.findChild(QListWidget, "settingsNav")
assert nav is not None
titles = [nav.item(i).text() for i in range(nav.count())]
assert "Search engines" in titles
# The keys moved inline onto the engines page, so there is no standalone API keys section.
assert "API keys" not in titles
# One stacked page per nav row.
assert dialog._stack.count() == nav.count()


def test_api_engine_key_field_grays_until_engine_checked(qapp: object, tmp_path: Path) -> None:
from searchmob_desktop.data.api_keys import BRAVE_KEY

store = _store(tmp_path)
dialog = _dialog(store)
brave = _checkbox_startswith(dialog, "Brave Search API")
field = dialog._key_inputs[BRAVE_KEY]

# API engines start unchecked, so the inline key field is disabled until you check the engine.
assert brave.isChecked() is False
assert field.isEnabled() is False

brave.setChecked(True)
assert field.isEnabled() is True
assert store.load().engine_enabled.get("brave") is True

brave.setChecked(False)
assert field.isEnabled() is False
assert store.load().engine_enabled.get("brave") is False


def test_suggestions_toggle_persists(qapp: object, tmp_path: Path) -> None:
store = _store(tmp_path)
dialog = _dialog(store)
Expand Down