diff --git a/muxplex/settings.py b/muxplex/settings.py new file mode 100644 index 0000000..eceae8a --- /dev/null +++ b/muxplex/settings.py @@ -0,0 +1,66 @@ +""" +Server-side settings management for muxplex. + +Settings are stored at ~/.config/muxplex/settings.json. +""" + +import json +from pathlib import Path + +SETTINGS_PATH = Path.home() / ".config" / "muxplex" / "settings.json" + +DEFAULT_SETTINGS: dict = { + "default_session": None, + "sort_order": "manual", + "hidden_sessions": [], + "window_size_largest": False, + "auto_open_created": True, + "new_session_template": "tmux new-session -d -s {name}", +} + + +def load_settings() -> dict: + """Load settings from disk, merging saved values over defaults. + + Returns DEFAULT_SETTINGS if the file does not exist or contains corrupt JSON. + Unknown keys in the file are ignored. + """ + result = DEFAULT_SETTINGS.copy() + try: + text = SETTINGS_PATH.read_text() + data = json.loads(text) + for key in DEFAULT_SETTINGS: + if key in data: + result[key] = data[key] + except FileNotFoundError: + pass + except (json.JSONDecodeError, Exception): + pass + return result + + +def save_settings(data: dict) -> None: + """Save settings to disk, merging *data* with defaults first. + + Creates parent directories as needed. Writes JSON with indent=2 and a + trailing newline. + """ + merged = DEFAULT_SETTINGS.copy() + for key in DEFAULT_SETTINGS: + if key in data: + merged[key] = data[key] + SETTINGS_PATH.parent.mkdir(parents=True, exist_ok=True) + SETTINGS_PATH.write_text(json.dumps(merged, indent=2) + "\n") + + +def patch_settings(patch: dict) -> dict: + """Merge known keys from *patch* into the current settings, save, and return result. + + Unknown keys in *patch* are silently ignored. + """ + current = load_settings() + for key in DEFAULT_SETTINGS: + if key in patch: + current[key] = patch[key] + save_settings(current) + return current diff --git a/muxplex/tests/test_settings.py b/muxplex/tests/test_settings.py new file mode 100644 index 0000000..28d1d2b --- /dev/null +++ b/muxplex/tests/test_settings.py @@ -0,0 +1,118 @@ +""" +Tests for muxplex/settings.py — server-side settings management. +7 acceptance-criteria tests defined here. +""" + +import json + +import pytest + +import muxplex.settings as settings_mod +from muxplex.settings import ( + DEFAULT_SETTINGS, + load_settings, + patch_settings, + save_settings, +) + + +# --------------------------------------------------------------------------- +# Autouse fixture: redirect SETTINGS_PATH to tmp_path +# --------------------------------------------------------------------------- + + +@pytest.fixture(autouse=True) +def redirect_settings_path(tmp_path, monkeypatch): + """Redirect SETTINGS_PATH to a temporary file for all tests.""" + fake_path = tmp_path / "settings.json" + monkeypatch.setattr(settings_mod, "SETTINGS_PATH", fake_path) + return fake_path + + +# --------------------------------------------------------------------------- +# Tests +# --------------------------------------------------------------------------- + + +def test_load_returns_defaults_when_no_file(): + """load_settings() returns DEFAULT_SETTINGS when no file exists.""" + result = load_settings() + assert result == DEFAULT_SETTINGS + + +def test_load_returns_saved_values(tmp_path, monkeypatch): + """load_settings() merges saved values over defaults.""" + # Write partial settings file + fake_path = tmp_path / "settings.json" + monkeypatch.setattr(settings_mod, "SETTINGS_PATH", fake_path) + fake_path.write_text(json.dumps({"sort_order": "alpha"})) + + result = load_settings() + + # Overridden value + assert result["sort_order"] == "alpha" + # Default fallback for unset key + assert result["default_session"] is None + + +def test_save_creates_file_and_dirs(tmp_path, monkeypatch): + """save_settings() creates parent dirs and writes the file.""" + nested_path = tmp_path / "a" / "b" / "settings.json" + monkeypatch.setattr(settings_mod, "SETTINGS_PATH", nested_path) + + save_settings({"sort_order": "alpha"}) + + assert nested_path.exists() + + +def test_save_merges_with_defaults(tmp_path, monkeypatch): + """save_settings() merges data with defaults before writing.""" + fake_path = tmp_path / "settings.json" + monkeypatch.setattr(settings_mod, "SETTINGS_PATH", fake_path) + + save_settings({"sort_order": "alpha"}) + + written = json.loads(fake_path.read_text()) + # The override should be present + assert written["sort_order"] == "alpha" + # The default should also be present (merged) + assert "default_session" in written + assert "auto_open_created" in written + + +def test_load_handles_corrupt_json(tmp_path, monkeypatch): + """load_settings() returns defaults gracefully on corrupt JSON.""" + fake_path = tmp_path / "settings.json" + monkeypatch.setattr(settings_mod, "SETTINGS_PATH", fake_path) + fake_path.write_text("NOT VALID JSON {{{{") + + result = load_settings() + + assert result == DEFAULT_SETTINGS + + +def test_patch_settings_merges_single_field(tmp_path, monkeypatch): + """patch_settings() updates a single known field and returns result.""" + fake_path = tmp_path / "settings.json" + monkeypatch.setattr(settings_mod, "SETTINGS_PATH", fake_path) + + result = patch_settings({"sort_order": "alpha"}) + + assert result["sort_order"] == "alpha" + # Other defaults preserved + assert result["auto_open_created"] is True + + # Also verify persistence + loaded = load_settings() + assert loaded["sort_order"] == "alpha" + + +def test_patch_settings_ignores_unknown_keys(tmp_path, monkeypatch): + """patch_settings() ignores keys not in DEFAULT_SETTINGS.""" + fake_path = tmp_path / "settings.json" + monkeypatch.setattr(settings_mod, "SETTINGS_PATH", fake_path) + + result = patch_settings({"unknown_key": "should_be_ignored", "sort_order": "alpha"}) + + assert "unknown_key" not in result + assert result["sort_order"] == "alpha"