From 8d66570d334094ba2dfb2fdf735f5d286abf6980 Mon Sep 17 00:00:00 2001 From: maxjivi05 Date: Sat, 24 Oct 2026 12:54:03 -0400 Subject: [PATCH] Steam file dialog: keep spaces in the chosen path, cancel when GTK 3 is missing or the bridge reconnects --- .../usr/local/bin/droiddeck-steam-compat | 29 +++++++++++++++---- tools/tests/test_steam_file_dialog.py | 24 ++++++++++----- 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-compat b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-compat index 3127ace34..73bf29c3e 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-compat +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-compat @@ -199,9 +199,12 @@ TOUR_JS = r""" FILE_BINDING = "droiddeckOpenFileDialog" FILE_JS = r""" (() => { - if (window.__droiddeckFiles) return "present"; - const self = { next: 0, pending: new Map() }; const cancelled = () => ({ result: 2, message: "" }); + if (window.__droiddeckFiles) { + window.__droiddeckFiles.cancelAll(); + return "present"; + } + const self = { next: 0, pending: new Map() }; const open = function (options) { return new Promise((resolve, reject) => { if (typeof window.__BINDING__ !== "function") { reject(cancelled()); return; } @@ -218,6 +221,11 @@ FILE_JS = r""" if (typeof path === "string" && path) item.resolve(path); else item.reject(cancelled()); return true; }; + self.cancelAll = () => { + const items = Array.from(self.pending.values()); + self.pending.clear(); + for (const item of items) item.reject(cancelled()); + }; const patch = (w) => { const system = w?.SteamClient?.System; if (system && system.OpenFileDialog !== open) system.OpenFileDialog = open; @@ -657,10 +665,16 @@ class Helper: def chooser_done(self): ident, proc = self.chooser self.chooser = None - path = proc.stdout.read().decode("utf-8", "replace").strip() + out = proc.stdout.read().decode("utf-8", "replace") proc.stdout.close() proc.wait() - self.finish_chooser(ident, path if proc.returncode == 0 and path else None) + if proc.returncode == 2: + say("file dialog needs GTK 3 and PyGObject, which come with the desktop") + try: + path = json.loads(out) if proc.returncode == 0 else None + except ValueError: + path = None + self.finish_chooser(ident, path if isinstance(path, str) and path else None) def finish_chooser(self, ident, path): say("file dialog %s" % ("chose " + path if path else "cancelled")) @@ -878,10 +892,13 @@ def chooser_main(): options = json.load(sys.stdin) except ValueError: return 1 - path = choose_file(options if isinstance(options, dict) else {}) + try: + path = choose_file(options if isinstance(options, dict) else {}) + except (ImportError, ValueError): + return 2 if not path: return 1 - print(path) + print(json.dumps(path)) return 0 diff --git a/tools/tests/test_steam_file_dialog.py b/tools/tests/test_steam_file_dialog.py index baff8914f..f557b3557 100644 --- a/tools/tests/test_steam_file_dialog.py +++ b/tools/tests/test_steam_file_dialog.py @@ -26,7 +26,11 @@ window.droiddeckOpenFileDialog = payload => asked.push(JSON.parse(payload)); const script = require('fs').readFileSync(process.argv[2], 'utf8'); (async () => { assert.strictEqual(eval(script), 'installed'); + const orphan = SteamClient.System.OpenFileDialog({ strTitle: 'Before reconnect' }); assert.strictEqual(eval(script), 'present'); + await assert.rejects(orphan, e => e.result === 2); + assert.strictEqual(window.__droiddeckFiles.done(1, '/late'), false); + asked.length = 0; assert.notStrictEqual(SteamClient.System.OpenFileDialog, native); assert.strictEqual(popup.window.SteamClient.System.OpenFileDialog, SteamClient.System.OpenFileDialog); popupCallbacks.forEach(cb => cb(later)); @@ -34,11 +38,11 @@ const script = require('fs').readFileSync(process.argv[2], 'utf8'); const options = { strTitle: 'Pick', bChooseDirectory: true }; const chosen = SteamClient.System.OpenFileDialog(options); const cancelled = later.window.SteamClient.System.OpenFileDialog({ strTitle: 'Other' }); - assert.deepStrictEqual(asked, [{ id: 1, options }, { id: 2, options: { strTitle: 'Other' } }]); - assert.strictEqual(window.__droiddeckFiles.done(1, '/root/Storage/Games'), true); - assert.strictEqual(window.__droiddeckFiles.done(1, '/again'), false); + assert.deepStrictEqual(asked, [{ id: 2, options }, { id: 3, options: { strTitle: 'Other' } }]); + assert.strictEqual(window.__droiddeckFiles.done(2, '/root/Storage/Games'), true); + assert.strictEqual(window.__droiddeckFiles.done(2, '/again'), false); assert.strictEqual(await chosen, '/root/Storage/Games'); - window.__droiddeckFiles.done(2, null); + window.__droiddeckFiles.done(3, null); await assert.rejects(cancelled, e => e.result === 2); delete window.droiddeckOpenFileDialog; await assert.rejects(SteamClient.System.OpenFileDialog({}), e => e.result === 2); @@ -97,8 +101,9 @@ class FileDialogBridgeTest(unittest.TestCase): self.assertEqual(self.helper.chooser, (1, first)) def test_the_chosen_path_or_a_cancel_goes_back_to_the_page(self): - for code, out, expected in ((0, b"/root/Storage/My Game/game.sh\n", '"/root/Storage/My Game/game.sh"'), - (1, b"", "null"), (0, b"\n", "null")): + for code, out, expected in ((0, b'"/root/Storage/My Game/game.sh"\n', '"/root/Storage/My Game/game.sh"'), + (0, b'" spaced name "\n', '" spaced name "'), + (1, b"", "null"), (2, b"", "null"), (0, b"\n", "null"), (0, b"not json\n", "null")): proc = mock.Mock(stdout=io.BytesIO(out), returncode=code) self.helper.chooser = (7, proc) self.helper.chooser_done() @@ -113,8 +118,11 @@ class FileDialogBridgeTest(unittest.TestCase): self.helper.page.evaluate.assert_not_called() def test_choose_file_mode_prints_the_path_and_fails_on_cancel(self): - for chosen, code, printed in (("/root/Storage/a.exe", 0, "/root/Storage/a.exe\n"), (None, 1, "")): - with mock.patch.dict(LIVE["chooser_main"].__globals__, {"choose_file": mock.Mock(return_value=chosen)}), \ + for chooser, code, printed in ((mock.Mock(return_value="/root/Storage/a.exe "), 0, '"/root/Storage/a.exe "\n'), + (mock.Mock(return_value=None), 1, ""), + (mock.Mock(side_effect=ValueError("Namespace Gtk not available for version 3.0")), 2, ""), + (mock.Mock(side_effect=ImportError("No module named 'gi'")), 2, "")): + with mock.patch.dict(LIVE["chooser_main"].__globals__, {"choose_file": chooser}), \ mock.patch.object(sys, "stdin", io.StringIO('{"strTitle": "Pick"}')), \ mock.patch.object(sys, "stdout", io.StringIO()) as out: self.assertEqual(LIVE["chooser_main"](), code)