mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-12 03:21:08 +00:00
Steam language: a guest helper that sets the client's language before it starts
This commit is contained in:
1 parent
9c72e3daf7
commit
7bb07d5dba
2 files changed
+319
No files matched your search
@@ -0,0 +1,181 @@
|
||||
#!/usr/bin/python3
|
||||
"""Sets the Steam client's interface language to the app's, before the client starts.
|
||||
|
||||
droiddeck-steam-language <home> <language>
|
||||
|
||||
<language> is one of the client's language names (english, spanish, latam, japanese, koreana,
|
||||
schinese, tchinese, ...). The client reads it from <home>/.steam/registry.vdf, under
|
||||
HKCU/Software/Valve/Steam and again under .../Steamsteamglobal: that second key is copied over the
|
||||
first at every start, so setting only the first is undone by the old value. Its update window
|
||||
reads the first key alone, and nothing reads LANG. exportedsettings.json is what the client writes
|
||||
beside them when its own setting changes. The session also passes -language, which the interface
|
||||
honours over the file; the file is still what the update window shows itself in.
|
||||
|
||||
Run only while the client is stopped: it keeps the registry in memory and writes it back on exit.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
LANGUAGES = {
|
||||
"english", "german", "french", "italian", "koreana", "spanish", "schinese", "tchinese",
|
||||
"russian", "thai", "japanese", "portuguese", "polish", "danish", "dutch", "finnish",
|
||||
"norwegian", "swedish", "hungarian", "czech", "romanian", "turkish", "brazilian", "bulgarian",
|
||||
"greek", "arabic", "ukrainian", "latam", "vietnamese", "indonesian", "malay",
|
||||
}
|
||||
KEYS = (("Registry", "HKCU", "Software", "Valve", "Steam"),
|
||||
("Registry", "HKCU", "Software", "Valve", "Steamsteamglobal"))
|
||||
TOKEN = re.compile(r'\s*(?:"((?:[^"\\]|\\.)*)"|([{}])|//[^\n]*)')
|
||||
|
||||
|
||||
def tokenize(text):
|
||||
tokens, pos = [], 0
|
||||
while pos < len(text):
|
||||
m = TOKEN.match(text, pos)
|
||||
if not m or m.end() == pos:
|
||||
break
|
||||
pos = m.end()
|
||||
if m.group(2):
|
||||
tokens.append(m.group(2))
|
||||
elif m.group(1) is not None:
|
||||
tokens.append('"' + m.group(1) + '"')
|
||||
return tokens
|
||||
|
||||
|
||||
def serialize(tokens):
|
||||
out, depth, i = [], 0, 0
|
||||
while i < len(tokens):
|
||||
t = tokens[i]
|
||||
if t == "}":
|
||||
depth -= 1
|
||||
out.append("\t" * depth + "}")
|
||||
i += 1
|
||||
elif i + 1 < len(tokens) and tokens[i + 1] == "{":
|
||||
out.append("\t" * depth + t)
|
||||
out.append("\t" * depth + "{")
|
||||
depth += 1
|
||||
i += 2
|
||||
else:
|
||||
out.append("\t" * depth + t + "\t\t" + (tokens[i + 1] if i + 1 < len(tokens) else '""'))
|
||||
i += 2
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
def block_end(tokens, start):
|
||||
"""Index of the '}' closing the block whose '{' is at start."""
|
||||
depth = 0
|
||||
for i in range(start, len(tokens)):
|
||||
if tokens[i] == "{":
|
||||
depth += 1
|
||||
elif tokens[i] == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return i
|
||||
return len(tokens)
|
||||
|
||||
|
||||
def child(tokens, start, end, name):
|
||||
"""Index of the '{' of the block called name directly inside tokens[start:end], or None.
|
||||
Keys are matched without case, as the client does."""
|
||||
i = start
|
||||
while i < end:
|
||||
if tokens[i] in ("{", "}"):
|
||||
i += 1
|
||||
continue
|
||||
if i + 1 < end and tokens[i + 1] == "{":
|
||||
if tokens[i].lower() == '"%s"' % name.lower():
|
||||
return i + 1
|
||||
i = block_end(tokens, i + 1) + 1
|
||||
else:
|
||||
i += 2
|
||||
return None
|
||||
|
||||
|
||||
def ensure_block(tokens, path):
|
||||
"""The '{' of the block at path, creating it and any missing parent at the end of its parent."""
|
||||
start, end = 0, len(tokens)
|
||||
for depth, name in enumerate(path):
|
||||
found = child(tokens, start, end, name)
|
||||
if found is None:
|
||||
at = end
|
||||
tokens[at:at] = ['"%s"' % name, "{", "}"]
|
||||
found = at + 1
|
||||
start, end = found + 1, block_end(tokens, found)
|
||||
return start - 1
|
||||
|
||||
|
||||
def set_value(tokens, opening, key, value):
|
||||
start, end = opening + 1, block_end(tokens, opening)
|
||||
i = start
|
||||
while i < end:
|
||||
if i + 1 < end and tokens[i + 1] == "{":
|
||||
i = block_end(tokens, i + 1) + 1
|
||||
continue
|
||||
if tokens[i].lower() == '"%s"' % key.lower() and i + 1 < end:
|
||||
tokens[i + 1] = '"%s"' % value
|
||||
return
|
||||
i += 2
|
||||
tokens[end:end] = ['"%s"' % key, '"%s"' % value]
|
||||
|
||||
|
||||
def with_language(text, language):
|
||||
tokens = tokenize(text)
|
||||
if child(tokens, 0, len(tokens), "Registry") is None:
|
||||
# Not a registry the client wrote (or none at all): start one it will read.
|
||||
tokens = []
|
||||
for path in KEYS:
|
||||
set_value(tokens, ensure_block(tokens, path), "language", language)
|
||||
return serialize(tokens)
|
||||
|
||||
|
||||
def write_changed(path, text):
|
||||
try:
|
||||
with open(path, encoding="utf-8", errors="replace") as f:
|
||||
if f.read() == text:
|
||||
return False
|
||||
except OSError:
|
||||
pass
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
tmp = path + ".droiddeck"
|
||||
with open(tmp, "w", encoding="utf-8") as f:
|
||||
f.write(text)
|
||||
os.replace(tmp, path)
|
||||
return True
|
||||
|
||||
|
||||
def apply(home, language):
|
||||
steam = os.path.join(home, ".steam")
|
||||
registry = os.path.join(steam, "registry.vdf")
|
||||
try:
|
||||
with open(registry, encoding="utf-8", errors="replace") as f:
|
||||
text = f.read()
|
||||
except OSError:
|
||||
text = ""
|
||||
changed = write_changed(registry, with_language(text, language))
|
||||
exported = os.path.join(steam, "exportedsettings.json")
|
||||
try:
|
||||
with open(exported, encoding="utf-8") as f:
|
||||
settings = json.load(f)
|
||||
if not isinstance(settings, dict):
|
||||
settings = {}
|
||||
except (OSError, ValueError):
|
||||
settings = {}
|
||||
if settings.get("language") != language:
|
||||
settings["language"] = language
|
||||
write_changed(exported, json.dumps(settings))
|
||||
changed = True
|
||||
return changed
|
||||
|
||||
|
||||
def main(argv):
|
||||
if len(argv) != 3 or argv[2] not in LANGUAGES:
|
||||
print("usage: droiddeck-steam-language <home> <steam language name>", file=sys.stderr)
|
||||
return 2
|
||||
changed = apply(argv[1], argv[2])
|
||||
print("== steam: language %s%s" % (argv[2], "" if changed else " (already set)"))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
@@ -0,0 +1,138 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
import runpy
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
|
||||
BIN = Path(__file__).resolve().parents[1] / 'linuxfs/overlay/usr/local/bin'
|
||||
MODULE = runpy.run_path(str(BIN / 'droiddeck-steam-language'))
|
||||
apply = MODULE['apply']
|
||||
with_language = MODULE['with_language']
|
||||
tokenize = MODULE['tokenize']
|
||||
child = MODULE['child']
|
||||
block_end = MODULE['block_end']
|
||||
main = MODULE['main']
|
||||
|
||||
|
||||
def value(text, *path):
|
||||
tokens = tokenize(text)
|
||||
start, end = 0, len(tokens)
|
||||
for name in path[:-1]:
|
||||
opening = child(tokens, start, end, name)
|
||||
if opening is None:
|
||||
return None
|
||||
start, end = opening + 1, block_end(tokens, opening)
|
||||
i = start
|
||||
while i < end:
|
||||
if i + 1 < end and tokens[i + 1] == '{':
|
||||
i = block_end(tokens, i + 1) + 1
|
||||
continue
|
||||
if tokens[i].lower() == '"%s"' % path[-1].lower():
|
||||
return tokens[i + 1].strip('"')
|
||||
i += 2
|
||||
return None
|
||||
|
||||
|
||||
STEAM = ('Registry', 'HKCU', 'Software', 'Valve', 'Steam', 'language')
|
||||
GLOBAL = ('Registry', 'HKCU', 'Software', 'Valve', 'Steamsteamglobal', 'language')
|
||||
|
||||
# As the client leaves it on a device (trimmed): both keys, other values beside them, and HKLM.
|
||||
CLIENT = '''"Registry"
|
||||
{
|
||||
"HKLM"
|
||||
{
|
||||
"Software"
|
||||
{
|
||||
"Valve"
|
||||
{
|
||||
"Steam"
|
||||
{
|
||||
"InstallPath" "/root/.local/share/Steam"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"HKCU"
|
||||
{
|
||||
"Software"
|
||||
{
|
||||
"Valve"
|
||||
{
|
||||
"Steam"
|
||||
{
|
||||
"language" "english"
|
||||
"AutoLoginUser" "someone"
|
||||
"Apps"
|
||||
{
|
||||
"language" "not this one"
|
||||
}
|
||||
}
|
||||
"Steamsteamglobal"
|
||||
{
|
||||
"language" "english"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
class SteamLanguageTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.temp.cleanup)
|
||||
self.home = Path(self.temp.name)
|
||||
(self.home / '.steam').mkdir()
|
||||
self.registry = self.home / '.steam/registry.vdf'
|
||||
|
||||
def test_both_keys_change_and_everything_else_stays(self):
|
||||
text = with_language(CLIENT, 'japanese')
|
||||
self.assertEqual('japanese', value(text, *STEAM))
|
||||
self.assertEqual('japanese', value(text, *GLOBAL))
|
||||
self.assertEqual('someone', value(text, 'Registry', 'HKCU', 'Software', 'Valve', 'Steam', 'AutoLoginUser'))
|
||||
self.assertEqual('not this one', value(text, 'Registry', 'HKCU', 'Software', 'Valve', 'Steam', 'Apps', 'language'))
|
||||
self.assertEqual('/root/.local/share/Steam', value(text, 'Registry', 'HKLM', 'Software', 'Valve', 'Steam', 'InstallPath'))
|
||||
self.assertEqual(1, text.count('"HKCU"'))
|
||||
|
||||
def test_a_first_run_gets_a_registry_the_client_reads(self):
|
||||
self.assertTrue(apply(str(self.home), 'koreana'))
|
||||
text = self.registry.read_text()
|
||||
self.assertTrue(text.startswith('"Registry"'))
|
||||
self.assertEqual('koreana', value(text, *STEAM))
|
||||
self.assertEqual('koreana', value(text, *GLOBAL))
|
||||
self.assertEqual({'language': 'koreana'}, json.loads((self.home / '.steam/exportedsettings.json').read_text()))
|
||||
|
||||
def test_missing_keys_are_added_whatever_their_case(self):
|
||||
text = with_language('"registry"\n{\n\t"hkcu"\n\t{\n\t\t"software"\n\t\t{\n\t\t}\n\t}\n}\n', 'schinese')
|
||||
self.assertEqual('schinese', value(text, *STEAM))
|
||||
self.assertEqual('schinese', value(text, *GLOBAL))
|
||||
self.assertEqual(1, text.lower().count('"hkcu"'))
|
||||
self.assertEqual(1, text.lower().count('"software"'))
|
||||
|
||||
def test_a_value_is_replaced_not_duplicated_whatever_its_key_case(self):
|
||||
text = with_language(CLIENT.replace('"language"\t\t"english"', '"Language"\t\t"english"'), 'latam')
|
||||
self.assertEqual(2, text.lower().count('"language"\t\t"latam"'))
|
||||
self.assertNotIn('english', text)
|
||||
|
||||
def test_an_unchanged_language_writes_nothing(self):
|
||||
self.registry.write_text(CLIENT)
|
||||
apply(str(self.home), 'spanish')
|
||||
before = self.registry.stat().st_mtime_ns
|
||||
self.assertFalse(apply(str(self.home), 'spanish'))
|
||||
self.assertEqual(before, self.registry.stat().st_mtime_ns)
|
||||
|
||||
def test_other_exported_settings_are_kept(self):
|
||||
(self.home / '.steam/exportedsettings.json').write_text('{"language": "english", "other": 1}')
|
||||
apply(str(self.home), 'tchinese')
|
||||
self.assertEqual({'language': 'tchinese', 'other': 1}, json.loads((self.home / '.steam/exportedsettings.json').read_text()))
|
||||
|
||||
def test_only_the_clients_names_are_accepted(self):
|
||||
self.assertEqual(2, main(['x', str(self.home), 'korean']))
|
||||
self.assertEqual(2, main(['x', str(self.home), 'Spanish']))
|
||||
self.assertFalse(self.registry.exists())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user