diff --git a/tests/test_client_in_game_tricks.py b/tests/test_client_in_game_tricks.py index 6477571..4b19d28 100644 --- a/tests/test_client_in_game_tricks.py +++ b/tests/test_client_in_game_tricks.py @@ -51,12 +51,30 @@ class ClientInGameTricksTests(unittest.TestCase): def test_last_duplicate_value_wins_and_quotes_are_accepted(self): self.path.write_text( - 'potatoModeEnabled: false\n' - 'closeLeagueClientDuringGame: false\n' - 'potatoModeEnabled: "true"\n' - 'closeLeagueClientDuringGame: "true"\n') + 'lol-user-experience:\n' + ' data:\n' + ' potatoModeEnabled: false\n' + ' closeLeagueClientDuringGame: false\n' + ' potatoModeEnabled: "true"\n' + ' closeLeagueClientDuringGame: "true"\n') self.assertEqual(tricks.mode(self.path), 'close') + def test_keys_outside_the_named_section_are_ignored(self): + self.path.write_text( + 'lol-user-experience:\n' + ' data:\n' + ' potatoModeEnabled: true\n' + 'other-section:\n' + ' potatoModeEnabled: true\n' + ' closeLeagueClientDuringGame: true\n') + self.assertEqual(tricks.mode(self.path), 'hide') + + def test_second_read_failure_fails_closed(self): + self.write(True, False) + text = self.path.read_text() + with patch.object(Path, 'read_text', side_effect=[text, OSError()]): + self.assertEqual(tricks.mode(self.path), 'none') + def test_main_prints_mode(self): self.write(True, False) output = io.StringIO() diff --git a/tests/test_hide_league_client_in_game.py b/tests/test_hide_league_client_in_game.py index 95be689..946bbad 100644 --- a/tests/test_hide_league_client_in_game.py +++ b/tests/test_hide_league_client_in_game.py @@ -156,6 +156,49 @@ class HideClientInGameTests(unittest.TestCase): self.assertEqual(hide_in_game.target_windows(self.prefix, cache), [0x10]) self.assertEqual(classes.call_count, 1) + def test_missing_wm_class_is_not_cached_as_a_class(self): + with patch.object(hide_in_game, 'xprop_value', + return_value='WM_CLASS: not found.'): + self.assertEqual(hide_in_game.window_class('0x10'), '') + + def test_unmap_failure_removes_the_window_from_hidden(self): + class FailingX11(FakeX11): + def unmap(self, window): + raise RuntimeError('unmap failed') + + watcher = hide_in_game.Watcher( + self.prefix, FailingX11(), + window_source=lambda _prefix, _cache: [0xabc], + state_source=lambda _prefix: True, + sleep=lambda _seconds: None, + out=io.StringIO(), + ) + watcher._hide_new() + self.assertEqual(watcher.hidden, set()) + + def test_interrupted_unmap_keeps_the_window_tracked(self): + shown = [] + + class InterruptingX11: + def unmap(self, window): + raise KeyboardInterrupt + + def show(self, window): + shown.append(window) + + watcher = hide_in_game.Watcher( + self.prefix, InterruptingX11(), + window_source=lambda _prefix, _cache: [0xabc], + state_source=lambda _prefix: True, + sleep=lambda _seconds: None, + out=io.StringIO(), + ) + with self.assertRaises(KeyboardInterrupt): + watcher._hide_new() + self.assertEqual(watcher.hidden, {0xabc}) + watcher.restore() + self.assertEqual(shown, [0xabc]) + def test_hides_then_shows_on_match_end(self): states = iter([True, True, False]) x11 = FakeX11() diff --git a/tools/client-in-game-tricks.py b/tools/client-in-game-tricks.py index bdc8d6b..adf28c6 100755 --- a/tools/client-in-game-tricks.py +++ b/tools/client-in-game-tricks.py @@ -7,32 +7,45 @@ The public launcher switch is RCLOL_CLIENT_IN_GAME_TRICKS=1. It reads - potatoModeEnabled must be true (client optimizations enabled); - closeLeagueClientDuringGame true -> close the client UI while a match runs; - closeLeagueClientDuringGame false -> hide the client windows instead; -- missing/unreadable preferences -> no action (none). +- missing preferences or an unreadable file -> no action (none). -This helper only selects a mode. It does not change preferences or files. +Only the named `lol-user-experience` section is considered, so identical keys +in another section cannot select a helper. This helper only selects a mode; it +does not change preferences or files. """ -import re import sys from pathlib import Path +SECTION = 'lol-user-experience' CLOSE_KEY = 'closeLeagueClientDuringGame' OPTIMIZATION_KEY = 'potatoModeEnabled' TRUE_VALUES = {'true', 'yes', '1'} FALSE_VALUES = {'false', 'no', '0'} -def read_bool(path, key): - """Return True/False for the last value, or None when absent/unreadable.""" - try: - lines = Path(path).read_text().splitlines() - except (OSError, UnicodeError): - return None +def read_bool(path, key, section=SECTION): + """Return True/False for the last key in section, or None when absent. + + OSError/UnicodeError is raised for an unreadable file so callers can fail + closed instead of treating a read race as a missing preference. + """ + lines = Path(path).read_text().splitlines() + in_section = False value = None for line in lines: - match = re.match(r'^\s*' + re.escape(key) + r':\s*(\S+)', line) - if match: - value = match.group(1).strip('"\'').lower() + stripped = line.strip() + if not stripped or stripped.startswith('#'): + continue + if not line.startswith((' ', '\t')): + if stripped == section + ':': + in_section = True + elif in_section: + break + continue + if not in_section or not stripped.startswith(key + ':'): + continue + value = stripped.split(':', 1)[1].strip().strip('"\'').lower() if value in TRUE_VALUES: return True if value in FALSE_VALUES: @@ -41,9 +54,17 @@ def read_bool(path, key): def mode(path): - if read_bool(path, OPTIMIZATION_KEY) is not True: + try: + optimization = read_bool(path, OPTIMIZATION_KEY) + except (OSError, UnicodeError): return 'none' - return 'close' if read_bool(path, CLOSE_KEY) is True else 'hide' + if optimization is not True: + return 'none' + try: + close = read_bool(path, CLOSE_KEY) + except (OSError, UnicodeError): + return 'none' + return 'close' if close is True else 'hide' def main(argv=None): diff --git a/tools/hide-league-client-in-game.py b/tools/hide-league-client-in-game.py index 68ad0b9..bf6f212 100755 --- a/tools/hide-league-client-in-game.py +++ b/tools/hide-league-client-in-game.py @@ -129,7 +129,8 @@ def xprop_value(window, prop): def window_class(window): - return xprop_value(window, 'WM_CLASS').lower() + value = xprop_value(window, 'WM_CLASS').lower() + return value if '=' in value else '' def window_pid(window): @@ -295,11 +296,12 @@ class Watcher: for window in windows: if window in self.hidden: continue + self.hidden.add(window) try: self.x11.unmap(window) - self.hidden.add(window) print(f'hide-league-client-in-game: hidden 0x{window:x}', file=self.out, flush=True) except Exception as error: + self.hidden.discard(window) print(f'hide-league-client-in-game: cannot hide 0x{window:x}: {error}', file=sys.stderr, flush=True) @@ -372,8 +374,10 @@ def main(argv=None): for signum in (signal.SIGTERM, signal.SIGINT, signal.SIGHUP): signal.signal(signum, stop) + optout = ('RCLOL_CLIENT_IN_GAME_TRICKS=0' if tricks + else 'RCLOL_HIDE_CLIENT_IN_GAME=0') print('hide-league-client-in-game: watching %s for a running match; ' - 'opt out with RCLOL_HIDE_CLIENT_IN_GAME=0' % prefix, flush=True) + 'opt out with %s' % (prefix, optout), flush=True) try: watcher.run() except KeyboardInterrupt: