From 5480ce5fb24b903cbbde4a5531c8373e2665cfce Mon Sep 17 00:00:00 2001 From: Teodor Potancok Date: Mon, 31 Jul 2023 12:51:22 +0200 Subject: [PATCH] frontend: runExe and arguments in uri --- bottles/backend/globals.py | 1 + bottles/backend/managers/exepath.py | 108 ++++++++++++++++++++++ bottles/backend/managers/meson.build | 1 + bottles/frontend/main.py | 54 +++++++++-- bottles/frontend/windows/meson.build | 1 + bottles/frontend/windows/runfiledialog.py | 64 +++++++++++++ 6 files changed, 222 insertions(+), 7 deletions(-) create mode 100644 bottles/backend/managers/exepath.py create mode 100644 bottles/frontend/windows/runfiledialog.py diff --git a/bottles/backend/globals.py b/bottles/backend/globals.py index 152afd4194..fb7c37ec71 100644 --- a/bottles/backend/globals.py +++ b/bottles/backend/globals.py @@ -47,6 +47,7 @@ class Paths: latencyflex = f"{base}/latencyflex" templates = f"{base}/templates" library = f"{base}/library.yml" + exe_paths = f"{base}/exe_paths.json" @staticmethod def is_vkbasalt_available(): diff --git a/bottles/backend/managers/exepath.py b/bottles/backend/managers/exepath.py new file mode 100644 index 0000000000..53d8590692 --- /dev/null +++ b/bottles/backend/managers/exepath.py @@ -0,0 +1,108 @@ +# library.py +# +# Copyright 2022 brombinmirko +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, in version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +import os + +from bottles.backend.utils import json + +from bottles.backend.logger import Logger +from bottles.backend.globals import Paths + +logging = Logger() + + +class ExePathManager: + """ + The ExePathManager class is used to read and write the mapping + between real and mangled paths from the user exe_paths.json file. + """ + + json_path: str = Paths.exe_paths + __paths: dict = {} + + def __init__(self): + self.load_paths(silent=True) + + def load_paths(self, silent=False): + """ + Loads data from the exe_paths file. + """ + if not os.path.exists(self.json_path): + logging.warning('exe_paths file not found, creating new one') + self.__paths = {} + self.save_paths() + else: + with open(self.json_path, 'r') as paths_file: + self.__paths = json.load(paths_file) + + if self.__paths is None: + self.__paths = {} + + self.save_paths(silent=silent) + + def add_path(self, real_path: str, mangled_path: str): + """ + Adds a new entry to the exe_paths.json file. + """ + if self.__already_exists(real_path): + logging.warning(f'Exe path already exists, nothing to add: {real_path}, {mangled_path}') + return + + logging.info(f'Adding new entry to exe paths: {real_path}') + + self.__paths[real_path] = mangled_path + self.save_paths() + + def __already_exists(self, real_path: str): + """ + Checks if the real path is already in the exe_paths.json file. + """ + for k, _ in self.__paths.items(): + if k == real_path: + return True + + return False + + def remove_path(self, _real_path: str): + """ + Removes an entry from the exe_paths.json file. + """ + if self.__paths.get(_real_path): + logging.info(f'Removing entry from exe paths: {_real_path}') + del self.__paths[_real_path] + self.save_paths() + return + logging.warning(f'Entry not found in exe paths, nothing to remove: {_real_path}') + + def save_paths(self, silent=False): + """ + Saves the exe_paths.json file. + """ + with open(self.json_path, 'w') as json_file: + json.dump(self.__paths, json_file) + + if not silent: + logging.info(f'Exe paths saved') + + def get_mangled_path(self, real_path: str): + return self.__paths.get(real_path) + + def get_paths(self): + """ + Returns the exe_paths.json file. + """ + return self.__paths diff --git a/bottles/backend/managers/meson.build b/bottles/backend/managers/meson.build index e682c0c3b4..5c4dcf30ae 100644 --- a/bottles/backend/managers/meson.build +++ b/bottles/backend/managers/meson.build @@ -6,6 +6,7 @@ bottles_sources = [ 'backup.py', 'component.py', 'dependency.py', + 'exepath.py', 'installer.py', 'library.py', 'manager.py', diff --git a/bottles/frontend/main.py b/bottles/frontend/main.py index ab4d95c65f..6fe1d04772 100644 --- a/bottles/frontend/main.py +++ b/bottles/frontend/main.py @@ -193,22 +193,62 @@ def __process_uri(self, uri): e.g. xdg-open bottles:run// """ uri = uri[0] + + import urllib.parse + uri = urllib.parse.unquote(uri) if os.path.exists(uri): from bottles.frontend.windows.bottlepicker import BottlePickerDialog dialog = BottlePickerDialog(application=self, arg_exe=uri) dialog.present() return 0 - _wrong_uri_error = _("Invalid URI (syntax: bottles:run//)") - if not len(uri) > 0 or not uri.startswith('bottles:run/') or len(uri.split('/')) != 3: + _wrong_uri_error = _("Invalid URI (syntax: bottles:run/// or bottles:runExe/////)") + if not len(uri) > 0 or not (uri.startswith('bottles:run/') or uri.startswith('bottles:runExe/')) or len(uri.split('/')) < 3: print(_wrong_uri_error) return False - uri = uri.replace('bottles:run/', '') - bottle, program = uri.split('/') - - import subprocess - subprocess.Popen(['bottles-cli', 'run', '-b', bottle, '-p', program]) + if uri.startswith('bottles:run/'): + uri = uri.replace('bottles:run/', '') + if len(uri.split('/')) == 2: + bottle, program = uri.split('/') + + import subprocess + subprocess.Popen(['bottles-cli', 'run', '-b', bottle, '-p', program]) + else: + split = uri.split('/') + bottle, program, arguments = split[0], split[1], split[2:] + + import subprocess + subprocess.Popen(['bottles-cli', 'run', '-b', bottle, '-p', program, '--', '/'.join(arguments)]) + elif uri.startswith('bottles:runExe/'): + uri = uri.replace('bottles:runExe/', '') + if len(uri.split('//')) == 2: + bottle, exe = uri.split('//') + can_access_exe = True + + from bottles.backend.managers.exepath import ExePathManager + exe_path_manager = ExePathManager() + mangled_path = exe_path_manager.get_mangled_path(exe) + + if not mangled_path or not os.path.exists(mangled_path): + can_access_exe = False + + if can_access_exe: + import subprocess + subprocess.Popen(['bottles-cli', 'run', '-b', bottle, '-e', mangled_path]) + else: + from bottles.frontend.windows.runfiledialog import RunFileDialog + dialog = RunFileDialog(bottle, exe, application=self) + dialog.present() + dialog.hide() + return 0 + + else: + split = uri.split('//') + bottle, exe, arguments = split[0], split[1], split[2:] + + import subprocess + subprocess.Popen(['bottles-cli', 'run', '-b', bottle, '-e', exe, '--', '//'.join(arguments)]) def do_startup(self): """ diff --git a/bottles/frontend/windows/meson.build b/bottles/frontend/windows/meson.build index 818989e415..56e6288f51 100644 --- a/bottles/frontend/windows/meson.build +++ b/bottles/frontend/windows/meson.build @@ -27,6 +27,7 @@ bottles_sources = [ 'upgradeversioning.py', 'vmtouch.py', 'main_window.py', + 'runfiledialog.py', ] install_data(bottles_sources, install_dir: dialogsdir) diff --git a/bottles/frontend/windows/runfiledialog.py b/bottles/frontend/windows/runfiledialog.py new file mode 100644 index 0000000000..4f446c34d5 --- /dev/null +++ b/bottles/frontend/windows/runfiledialog.py @@ -0,0 +1,64 @@ +# runfiledialog.py +# +# Copyright 2022 brombinmirko +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, in version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +from gettext import gettext as _ + +from gi.repository import Gtk, Adw, Gio +from bottles.backend.managers.exepath import ExePathManager + +from bottles.frontend.utils.filters import add_all_filters, add_executable_filters +from bottles.frontend.params import APP_ID + +class RunFileDialog(Adw.ApplicationWindow): + """This class should not be called from the application GUI, only from CLI.""" + __gtype_name__ = 'RunFileDialog' + settings = Gio.Settings.new(APP_ID) + Adw.init() + + def __init__(self, bottle: str, exe: str, **kwargs): + super().__init__(**kwargs) + def execute(_dialog, response): + if response != Gtk.ResponseType.ACCEPT: + self._exit() + return + + exe_path_manager = ExePathManager() + # map the flatpak mangled path to `exe` + exe_path_manager.add_path(exe, dialog.get_file().get_path()) + import subprocess + subprocess.Popen(['bottles-cli', 'run', '-b', bottle, '-e', dialog.get_file().get_path()]) + self._exit() + + fd = Gio.File.new_for_path(exe) + + dialog = Gtk.FileChooserNative.new( + _("Select Executable"), + self, + Gtk.FileChooserAction.OPEN, + _("Run"), + None + ) + import os.path + if os.path.exists(fd.get_path()): + dialog.set_file(fd) + add_executable_filters(dialog) + add_all_filters(dialog) + dialog.connect("response", execute) + dialog.show() + + def _exit(self): + self.close() \ No newline at end of file