diff --git a/core/config.py b/core/config.py index b8afa870..e947dcd8 100644 --- a/core/config.py +++ b/core/config.py @@ -91,7 +91,7 @@ "settings": { "start_minimized": True, "start_at_login": False, - "hscroll_threshold": 1, + "hscroll_threshold": 0.1, "invert_hscroll": False, # swap horizontal scroll directions "invert_vscroll": False, # swap vertical scroll directions "dpi": 1000, # pointer speed / DPI setting @@ -358,6 +358,16 @@ def _merge_defaults(cfg, defaults): return cfg + +def _is_compatible_type(value, default_val): + """Return True for values that are safe despite differing exact types.""" + return ( + isinstance(default_val, float) + and isinstance(value, int) + and not isinstance(value, bool) + ) + + def _validate_types(cfg, defaults, path=""): """Reset values whose type doesn't match the defaults template.""" for key, default_val in defaults.items(): @@ -370,6 +380,8 @@ def _validate_types(cfg, defaults, path=""): print(f"[Config] Type mismatch at {path}.{key}: " f"expected dict, got {type(cfg[key]).__name__}") cfg[key] = json.loads(json.dumps(default_val)) + elif _is_compatible_type(cfg[key], default_val): + continue elif not isinstance(cfg[key], type(default_val)): print(f"[Config] Type mismatch at {path}.{key}: " f"expected {type(default_val).__name__}, " diff --git a/tests/test_config.py b/tests/test_config.py index 54cb4986..797494f7 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -102,6 +102,37 @@ def test_migrate_updates_media_player_profile_apps(self): self.assertFalse(migrated["settings"]["start_at_login"]) self.assertNotIn("start_with_windows", migrated["settings"]) + def test_default_hscroll_threshold_supports_fractional_mac_deltas(self): + self.assertEqual(config.DEFAULT_CONFIG["settings"]["hscroll_threshold"], 0.1) + + def test_load_config_preserves_integer_hscroll_threshold(self): + partial = { + "version": 9, + "active_profile": "default", + "profiles": { + "default": { + "label": "Default", + "apps": [], + "mappings": {}, + } + }, + "settings": { + "hscroll_threshold": 1, + }, + } + + with tempfile.TemporaryDirectory() as temp_dir: + config_file = Path(temp_dir) / "config.json" + config_file.write_text(json.dumps(partial), encoding="utf-8") + + with ( + patch.object(config, "CONFIG_DIR", temp_dir), + patch.object(config, "CONFIG_FILE", str(config_file)), + ): + loaded = config.load_config() + + self.assertEqual(loaded["settings"]["hscroll_threshold"], 1) + def test_load_config_merges_missing_defaults_from_disk(self): partial = { "version": 3, diff --git a/tests/test_engine.py b/tests/test_engine.py index dfbe00c6..5bad9681 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -93,11 +93,12 @@ def run_target(self): class EngineHorizontalScrollTests(unittest.TestCase): - def _make_engine(self): + def _make_engine(self, hscroll_threshold=1): from core.engine import Engine cfg = copy.deepcopy(DEFAULT_CONFIG) - cfg["settings"]["hscroll_threshold"] = 1 + if hscroll_threshold is not None: + cfg["settings"]["hscroll_threshold"] = hscroll_threshold with ( patch("core.engine.MouseHook", _FakeMouseHook), @@ -152,6 +153,19 @@ def test_hscroll_accumulates_fractional_mac_deltas(self): self.assertEqual(execute_action_mock.call_count, 1) + def test_default_hscroll_threshold_handles_m720_fractional_delta(self): + engine = self._make_engine(hscroll_threshold=None) + handler = engine._make_hscroll_handler("space_right") + + with patch("core.engine.execute_action") as execute_action_mock: + handler(SimpleNamespace( + event_type=MouseEvent.HSCROLL_RIGHT, + raw_data=0.100006103515625, + timestamp=3.00, + )) + + self.assertEqual(execute_action_mock.call_count, 1) + def test_connection_callback_receives_current_state_immediately(self): engine = self._make_engine() engine.hook.device_connected = True