feat: add configuration module with YAML loading and defaults
Made-with: Cursor
This commit is contained in:
BIN
src/cursor_flasher/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
src/cursor_flasher/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
src/cursor_flasher/__pycache__/config.cpython-312.pyc
Normal file
BIN
src/cursor_flasher/__pycache__/config.cpython-312.pyc
Normal file
Binary file not shown.
72
src/cursor_flasher/config.py
Normal file
72
src/cursor_flasher/config.py
Normal file
@@ -0,0 +1,72 @@
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
@dataclass
|
||||
class Config:
|
||||
pulse_color: str = "#FF9500"
|
||||
pulse_width: int = 4
|
||||
pulse_speed: float = 1.5
|
||||
pulse_opacity_min: float = 0.3
|
||||
pulse_opacity_max: float = 1.0
|
||||
|
||||
sound_enabled: bool = True
|
||||
sound_name: str = "Glass"
|
||||
sound_volume: float = 0.5
|
||||
|
||||
poll_interval: float = 0.5
|
||||
cooldown: float = 3.0
|
||||
|
||||
auto_dismiss: int = 300
|
||||
|
||||
|
||||
FIELD_MAP: dict[str, dict[str, str]] = {
|
||||
"pulse": {
|
||||
"color": "pulse_color",
|
||||
"width": "pulse_width",
|
||||
"speed": "pulse_speed",
|
||||
"opacity_min": "pulse_opacity_min",
|
||||
"opacity_max": "pulse_opacity_max",
|
||||
},
|
||||
"sound": {
|
||||
"enabled": "sound_enabled",
|
||||
"name": "sound_name",
|
||||
"volume": "sound_volume",
|
||||
},
|
||||
"detection": {
|
||||
"poll_interval": "poll_interval",
|
||||
"cooldown": "cooldown",
|
||||
},
|
||||
"timeout": {
|
||||
"auto_dismiss": "auto_dismiss",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
DEFAULT_CONFIG_PATH = Path.home() / ".cursor-flasher" / "config.yaml"
|
||||
|
||||
|
||||
def load_config(path: Path = DEFAULT_CONFIG_PATH) -> Config:
|
||||
"""Load config from YAML, falling back to defaults for missing values."""
|
||||
if not path.exists():
|
||||
return Config()
|
||||
|
||||
with open(path) as f:
|
||||
raw = yaml.safe_load(f)
|
||||
|
||||
if not raw or not isinstance(raw, dict):
|
||||
return Config()
|
||||
|
||||
overrides: dict[str, Any] = {}
|
||||
for section, mapping in FIELD_MAP.items():
|
||||
section_data = raw.get(section, {})
|
||||
if not isinstance(section_data, dict):
|
||||
continue
|
||||
for yaml_key, field_name in mapping.items():
|
||||
if yaml_key in section_data:
|
||||
overrides[field_name] = section_data[yaml_key]
|
||||
|
||||
return Config(**overrides)
|
||||
BIN
tests/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
tests/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
tests/__pycache__/conftest.cpython-312-pytest-8.3.4.pyc
Normal file
BIN
tests/__pycache__/conftest.cpython-312-pytest-8.3.4.pyc
Normal file
Binary file not shown.
BIN
tests/__pycache__/test_config.cpython-312-pytest-8.3.4.pyc
Normal file
BIN
tests/__pycache__/test_config.cpython-312-pytest-8.3.4.pyc
Normal file
Binary file not shown.
56
tests/test_config.py
Normal file
56
tests/test_config.py
Normal file
@@ -0,0 +1,56 @@
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from cursor_flasher.config import Config, load_config, DEFAULT_CONFIG_PATH
|
||||
|
||||
|
||||
class TestDefaultConfig:
|
||||
def test_has_pulse_settings(self):
|
||||
cfg = Config()
|
||||
assert cfg.pulse_color == "#FF9500"
|
||||
assert cfg.pulse_width == 4
|
||||
assert cfg.pulse_speed == 1.5
|
||||
assert cfg.pulse_opacity_min == 0.3
|
||||
assert cfg.pulse_opacity_max == 1.0
|
||||
|
||||
def test_has_sound_settings(self):
|
||||
cfg = Config()
|
||||
assert cfg.sound_enabled is True
|
||||
assert cfg.sound_name == "Glass"
|
||||
assert cfg.sound_volume == 0.5
|
||||
|
||||
def test_has_detection_settings(self):
|
||||
cfg = Config()
|
||||
assert cfg.poll_interval == 0.5
|
||||
assert cfg.cooldown == 3.0
|
||||
|
||||
def test_has_timeout_settings(self):
|
||||
cfg = Config()
|
||||
assert cfg.auto_dismiss == 300
|
||||
|
||||
|
||||
class TestLoadConfig:
|
||||
def test_loads_from_yaml(self, tmp_path):
|
||||
config_file = tmp_path / "config.yaml"
|
||||
config_file.write_text(
|
||||
"pulse:\n"
|
||||
' color: "#00FF00"\n'
|
||||
" width: 8\n"
|
||||
"sound:\n"
|
||||
" enabled: false\n"
|
||||
)
|
||||
cfg = load_config(config_file)
|
||||
assert cfg.pulse_color == "#00FF00"
|
||||
assert cfg.pulse_width == 8
|
||||
assert cfg.sound_enabled is False
|
||||
assert cfg.pulse_speed == 1.5
|
||||
assert cfg.sound_name == "Glass"
|
||||
|
||||
def test_missing_file_returns_defaults(self, tmp_path):
|
||||
cfg = load_config(tmp_path / "nonexistent.yaml")
|
||||
assert cfg.pulse_color == "#FF9500"
|
||||
|
||||
def test_empty_file_returns_defaults(self, tmp_path):
|
||||
config_file = tmp_path / "config.yaml"
|
||||
config_file.write_text("")
|
||||
cfg = load_config(config_file)
|
||||
assert cfg.pulse_color == "#FF9500"
|
||||
Reference in New Issue
Block a user