feat: add server-side settings module
This commit is contained in:
@@ -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
|
||||||
@@ -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"
|
||||||
Reference in New Issue
Block a user