mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-27 00:02:12 +01:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4967b13dca |
No files matched your search
@@ -14,6 +14,12 @@ action calls the backend, so it reads the persisted setting rather than a
|
||||
browser cache. A missing, damaged, or old non-boolean value reads as the
|
||||
caller's default;
|
||||
the next save atomically replaces the sample's settings file.
|
||||
The current file is `{"schema": 1, "enabled": true|false}`. An unversioned
|
||||
`{"enabled": true|false}` file from the first sample release still reads, and
|
||||
the next save writes schema 1. A file with another explicit schema returns a
|
||||
visible read or save error and remains untouched, so downgrading the sample
|
||||
cannot erase settings written by a newer version. Add a deliberate migration
|
||||
for each future schema instead of changing this guard silently.
|
||||
The panel also guards its own read and write calls synchronously. If two
|
||||
controller events arrive before React redraws a disabled control, the second
|
||||
event is ignored; after the request settles, a new change can be saved.
|
||||
|
||||
@@ -5,26 +5,41 @@ from pathlib import Path
|
||||
import tempfile
|
||||
import decky
|
||||
|
||||
SETTINGS_SCHEMA = 1
|
||||
|
||||
|
||||
def settings_file():
|
||||
# Decky provides this per-plugin path through its official Python module.
|
||||
return Path(decky.DECKY_PLUGIN_SETTINGS_DIR) / 'settings.json'
|
||||
|
||||
|
||||
def read_settings():
|
||||
try:
|
||||
stored = json.loads(settings_file().read_text(encoding='utf-8'))
|
||||
except (FileNotFoundError, json.JSONDecodeError, UnicodeDecodeError):
|
||||
return {}
|
||||
if not isinstance(stored, dict):
|
||||
return {}
|
||||
# The first sample release wrote {"enabled": bool} without a schema.
|
||||
# Keep reading it; the next save writes the current format.
|
||||
schema = stored.get('schema', SETTINGS_SCHEMA)
|
||||
if type(schema) is not int or schema != SETTINGS_SCHEMA:
|
||||
raise ValueError('Settings were written by an unsupported sample version')
|
||||
return stored
|
||||
|
||||
|
||||
class Plugin:
|
||||
async def get_setting(self, key: str, default=False):
|
||||
if key != 'enabled':
|
||||
raise ValueError('Unknown setting')
|
||||
try:
|
||||
stored = json.loads(settings_file().read_text(encoding='utf-8'))
|
||||
except (FileNotFoundError, json.JSONDecodeError, UnicodeDecodeError):
|
||||
return default
|
||||
value = stored.get(key) if isinstance(stored, dict) else None
|
||||
value = read_settings().get(key)
|
||||
return value if type(value) is bool else default
|
||||
|
||||
async def set_setting(self, key: str, value: bool):
|
||||
if key != 'enabled' or type(value) is not bool:
|
||||
raise ValueError('Expected an enabled boolean')
|
||||
# Refuse to erase settings from a newer plugin after a downgrade.
|
||||
read_settings()
|
||||
target = settings_file()
|
||||
target.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
temporary = None
|
||||
@@ -32,7 +47,7 @@ class Plugin:
|
||||
with tempfile.NamedTemporaryFile('w', encoding='utf-8', dir=target.parent,
|
||||
prefix='.settings-', delete=False) as output:
|
||||
temporary = Path(output.name)
|
||||
json.dump({'enabled': value}, output)
|
||||
json.dump({'schema': SETTINGS_SCHEMA, 'enabled': value}, output)
|
||||
output.write('\n')
|
||||
temporary.chmod(0o600)
|
||||
os.replace(temporary, target)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""The author example stores settings as a normal Decky user plugin."""
|
||||
import asyncio
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -36,7 +37,8 @@ class SamplePlugin(unittest.TestCase):
|
||||
self.assertTrue(asyncio.run(first.set_setting('enabled', True)))
|
||||
second = backend(folder)
|
||||
self.assertTrue(asyncio.run(second.get_setting('enabled', False)))
|
||||
self.assertTrue((Path(folder) / 'settings.json').is_file())
|
||||
self.assertEqual(json.loads((Path(folder) / 'settings.json').read_text()),
|
||||
{'schema': 1, 'enabled': True})
|
||||
|
||||
def test_backend_rejects_unknown_keys_and_non_boolean_values(self):
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
@@ -57,6 +59,43 @@ class SamplePlugin(unittest.TestCase):
|
||||
self.assertTrue(asyncio.run(plugin.set_setting('enabled', True)))
|
||||
self.assertTrue(asyncio.run(backend(folder).get_setting('enabled', False)))
|
||||
|
||||
def test_legacy_boolean_survives_restart_and_migrates_on_save(self):
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
path = Path(folder) / 'settings.json'
|
||||
path.write_text('{"enabled": true}\n')
|
||||
self.assertTrue(asyncio.run(backend(folder).get_setting('enabled', False)))
|
||||
self.assertEqual(json.loads(path.read_text()), {'enabled': True},
|
||||
'Reading must not rewrite a user setting')
|
||||
self.assertTrue(asyncio.run(backend(folder).set_setting('enabled', False)))
|
||||
self.assertEqual(json.loads(path.read_text()), {'schema': 1, 'enabled': False})
|
||||
self.assertFalse(asyncio.run(backend(folder).get_setting('enabled', True)))
|
||||
|
||||
def test_newer_schema_is_preserved_on_read_and_save(self):
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
path = Path(folder) / 'settings.json'
|
||||
original = '{"schema": 2, "enabled": true, "future": "keep"}\n'
|
||||
path.write_text(original)
|
||||
plugin = backend(folder)
|
||||
with self.assertRaisesRegex(ValueError, 'unsupported sample version'):
|
||||
asyncio.run(plugin.get_setting('enabled', False))
|
||||
with self.assertRaisesRegex(ValueError, 'unsupported sample version'):
|
||||
asyncio.run(plugin.set_setting('enabled', False))
|
||||
self.assertEqual(path.read_text(), original)
|
||||
self.assertEqual(list(Path(folder).glob('.settings-*')), [])
|
||||
|
||||
def test_failed_atomic_replace_keeps_previous_setting(self):
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
path = Path(folder) / 'settings.json'
|
||||
plugin = backend(folder)
|
||||
self.assertTrue(asyncio.run(plugin.set_setting('enabled', True)))
|
||||
previous = path.read_bytes()
|
||||
with patch('os.replace', side_effect=OSError('storage unavailable')):
|
||||
with self.assertRaisesRegex(OSError, 'storage unavailable'):
|
||||
asyncio.run(plugin.set_setting('enabled', False))
|
||||
self.assertEqual(path.read_bytes(), previous)
|
||||
self.assertEqual(list(Path(folder).glob('.settings-*')), [])
|
||||
self.assertTrue(asyncio.run(backend(folder).get_setting('enabled', False)))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user