Better handling for wine based shortcuts.
This commit is contained in:
1 parent
d2c5695221
commit
fb2e1cc55c
3 files changed
+91
-9
No files matched your search
@@ -134,6 +134,8 @@ Review the suggested shortcut name, edit it if desired, then choose **Create Sho
|
||||
|
||||
Application search lists registered visible applications, including user and system application launchers, with case-insensitive name-prefix filtering like the desktop dialog. Type to search and press Down to move into the list; press Enter to select an application. The shortcut launches the original `.desktop` entry, preserving its terminal, working-directory, D-Bus, and launch options. Keep that original entry available. You can also browse to or paste a `.desktop` file directly.
|
||||
|
||||
Windows `.exe` programs run with Wine, using the program's folder as the working directory. The creator looks for an enclosing Wine prefix (bottle), including through symbolic links, then checks the existing `~/.wine` prefix. It recognizes prefixes by their `drive_c`, `dosdevices`, and `system.reg` entries. Review or change the suggested **Wine prefix (bottle) folder**; if none is found, enter or browse for the existing prefix before creating the shortcut. Wine must be installed. Existing shortcuts are not updated automatically; recreate a shortcut made before this support was added.
|
||||
|
||||
Shortcuts are ordinary `.desktop` files in the XDG desktop directory (normally `~/Desktop`). Existing items are never overwritten; repeated names get numbered filenames. Tab and Shift+Tab move between controls, and Escape closes the dialog. Deleting a created shortcut through I38 Desktop removes only the shortcut file.
|
||||
|
||||
After updating an existing installation, regenerate your I38 configuration to add the new Ratpoison binding; `./i38.sh -u` copies scripts but does not add bindings. Review your configuration preferences before regeneration. Then reload i3 with `i3-msg reload`, or enter Ratpoison Mode and press Control+semicolon, to activate the new binding in the running session.
|
||||
@@ -162,7 +164,7 @@ git config --local core.hooksPath .githooks
|
||||
The pre-commit hook rejects staged Python cache files. The I38 script installer
|
||||
also excludes and removes Python cache artifacts when copying `scripts/`.
|
||||
|
||||
Shortcut regression tests use Python's unittest, PyGObject, and `desktop-file-validate` (desktop-file-utils). Run `python3 -B -m unittest discover -s tests -v`; its page-title tests start a temporary localhost HTTP server. For native GTK and clipboard checks, also install Xvfb and xclip, then run:
|
||||
Shortcut regression tests use Python's unittest, PyGObject, and `desktop-file-validate` (desktop-file-utils). Run `python3 -B -m unittest discover -s tests -v`. These check Wine prefix detection, desktop-entry argument handling and working directory using a recording Wine substitute, and existing target types. For native GTK Wine-prefix field and validation checks, also install Xvfb, then run:
|
||||
|
||||
```bash
|
||||
xvfb-run -a dbus-run-session -- env -u AT_SPI_BUS_ADDRESS I38_PRIVATE_GUI_TEST=1 PYTHONDONTWRITEBYTECODE=1 python3 tests/native_shortcut_gui.py -v
|
||||
@@ -170,6 +172,8 @@ xvfb-run -a dbus-run-session -- env -u AT_SPI_BUS_ADDRESS I38_PRIVATE_GUI_TEST=1
|
||||
|
||||
The GUI test uses a separate display and session bus. It does not prove live speech or Braille. After reviewing the diff and regenerating your configuration, test the new Ratpoison binding, clipboard and browse sources, application search, edited names, and opening each shortcut type through I38 Desktop. Confirm that normal desktop prefix search and existing launchers still work, that repeated names preserve previous shortcuts, and that deleting a generated shortcut leaves its original target intact.
|
||||
|
||||
For Wine, create a shortcut to an `.exe` inside a non-default prefix and confirm that the suggested prefix is correct and the program opens with its settings and resources. Try an `.exe` outside any prefix: review the `~/.wine` suggestion when that prefix exists, or select the bottle manually when it does not. Test an explicit override, cancel the bottle chooser, and verify that an invalid folder prevents creation. Check that switching to a native program or document hides the Wine controls and preserves its normal launch behavior.
|
||||
|
||||
### AI-assisted contributions
|
||||
|
||||
AI-assisted contributions are welcome.
|
||||
|
||||
@@ -12,8 +12,8 @@ gi.require_version("Gdk", "3.0")
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gdk, GLib, Gtk
|
||||
|
||||
from shortcuts import (clipboard_target, create_shortcut, fetch_page_title,
|
||||
installed_applications, parse_target)
|
||||
from shortcuts import (clipboard_target, create_shortcut, detect_wine_prefix, fetch_page_title,
|
||||
installed_applications, parse_target, validate_wine_prefix)
|
||||
|
||||
|
||||
class ApplicationDialog(Gtk.Dialog):
|
||||
@@ -132,6 +132,15 @@ class ShortcutWindow(Gtk.Window):
|
||||
self.add_field(box, "_Target (path, program, or web URL):", self.targetEntry)
|
||||
self.nameEntry = Gtk.Entry()
|
||||
self.add_field(box, "Shortcut _name:", self.nameEntry)
|
||||
self.wineBox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
||||
self.wineBox.set_no_show_all(True)
|
||||
self.wineEntry = Gtk.Entry()
|
||||
self.wineEntry.set_activates_default(True)
|
||||
self.add_field(self.wineBox, "_Wine prefix (bottle) folder:", self.wineEntry)
|
||||
wineButton = Gtk.Button.new_with_mnemonic("_Browse for Wine Prefix")
|
||||
wineButton.connect("clicked", self.on_wine_prefix)
|
||||
self.wineBox.pack_start(wineButton, False, False, 0)
|
||||
box.pack_start(self.wineBox, False, False, 0)
|
||||
self.targetEntry.connect("changed", self.on_target_changed)
|
||||
self.targetEntry.connect("activate", self.on_target_activate)
|
||||
self.nameEntry.connect("changed", self.on_name_changed)
|
||||
@@ -181,6 +190,8 @@ class ShortcutWindow(Gtk.Window):
|
||||
self.createButton.set_sensitive(False)
|
||||
self.terminalCheck.set_active(False)
|
||||
self.terminalCheck.set_sensitive(False)
|
||||
self.wineBox.hide()
|
||||
self.wineEntry.set_text("")
|
||||
self.statusLabel.set_text("Press Enter to check the target, or choose a source above.")
|
||||
|
||||
def on_name_changed(self, _entry):
|
||||
@@ -191,10 +202,21 @@ class ShortcutWindow(Gtk.Window):
|
||||
self.targetEntry.set_text(target.value)
|
||||
self.target = target
|
||||
self.nameEntry.set_text(target.name)
|
||||
self.terminalCheck.set_sensitive(target.kind == "Program")
|
||||
self.terminalCheck.set_sensitive(target.kind in ("Program", "Windows program"))
|
||||
self.statusLabel.set_text("{}: {}".format(target.kind, target.value))
|
||||
self.nameEntry.grab_focus()
|
||||
self.nameEntry.select_region(0, -1)
|
||||
if target.kind == "Windows program":
|
||||
winePrefix = detect_wine_prefix(target.value)
|
||||
self.wineEntry.set_text(winePrefix or "")
|
||||
self.wineBox.set_no_show_all(False)
|
||||
self.wineBox.show_all()
|
||||
self.wineBox.set_no_show_all(True)
|
||||
if winePrefix:
|
||||
self.statusLabel.set_text("Windows program: {}. Wine prefix: {}".format(target.value, winePrefix))
|
||||
else:
|
||||
self.statusLabel.set_text("Choose the Wine prefix (bottle) folder before creating this shortcut.")
|
||||
self.wineEntry.grab_focus()
|
||||
if target.kind == "Web link":
|
||||
requestNumber = self.requestNumber
|
||||
nameVersion = self.nameVersion
|
||||
@@ -289,6 +311,18 @@ class ShortcutWindow(Gtk.Window):
|
||||
def on_folder(self, _button):
|
||||
self.choose_path(True)
|
||||
|
||||
def on_wine_prefix(self, _button):
|
||||
dialog = Gtk.FileChooserDialog(title="Choose Wine prefix (bottle) folder",
|
||||
transient_for=self, modal=True,
|
||||
action=Gtk.FileChooserAction.SELECT_FOLDER)
|
||||
dialog.add_buttons("_Cancel", Gtk.ResponseType.CANCEL, "_Select", Gtk.ResponseType.OK)
|
||||
dialog.set_local_only(True)
|
||||
dialog.set_show_hidden(True)
|
||||
if dialog.run() == Gtk.ResponseType.OK:
|
||||
self.wineEntry.set_text(dialog.get_filename())
|
||||
dialog.destroy()
|
||||
self.wineEntry.grab_focus()
|
||||
|
||||
def on_application(self, _button):
|
||||
self.requestNumber += 1
|
||||
try:
|
||||
@@ -303,9 +337,17 @@ class ShortcutWindow(Gtk.Window):
|
||||
def on_create(self, _button):
|
||||
if self.target is None:
|
||||
return
|
||||
winePrefix = None
|
||||
if self.target.kind == "Windows program":
|
||||
try:
|
||||
winePrefix = validate_wine_prefix(self.wineEntry.get_text())
|
||||
except ValueError as error:
|
||||
self.show_message(str(error), error=True)
|
||||
self.wineEntry.grab_focus()
|
||||
return
|
||||
try:
|
||||
destination = create_shortcut(self.target, self.nameEntry.get_text(), self.fileManager,
|
||||
self.terminalCheck.get_active())
|
||||
self.terminalCheck.get_active(), winePrefix=winePrefix)
|
||||
except (ValueError, OSError, GLib.Error) as error:
|
||||
self.show_message(str(error), error=True)
|
||||
return
|
||||
|
||||
+40
-4
@@ -143,11 +143,39 @@ def parse_target(value):
|
||||
icon = app.get_icon()
|
||||
return Target(str(targetPath), app.get_display_name(), "Application",
|
||||
icon.to_string() if icon else "application-x-executable")
|
||||
if targetPath.suffix.lower() == ".exe":
|
||||
return Target(str(targetPath), targetPath.stem, "Windows program", "application-x-executable")
|
||||
if os.access(str(targetPath), os.X_OK):
|
||||
return Target(str(targetPath), targetPath.name, "Program", "application-x-executable")
|
||||
return Target(str(targetPath), targetPath.name, "File", "text-x-generic")
|
||||
|
||||
|
||||
def is_wine_prefix(path):
|
||||
return ((path / "drive_c").is_dir() and (path / "dosdevices").is_dir()
|
||||
and (path / "system.reg").is_file())
|
||||
|
||||
|
||||
def detect_wine_prefix(value):
|
||||
targetPath = Path(value).expanduser()
|
||||
# Check the supplied path first: drive_c itself can be a symlink. Then
|
||||
# resolve links so an executable linked onto the desktop also works.
|
||||
for programPath in (targetPath, targetPath.resolve()):
|
||||
for parent in programPath.parents:
|
||||
if is_wine_prefix(parent):
|
||||
return str(parent)
|
||||
defaultPrefix = Path.home() / ".wine"
|
||||
return str(defaultPrefix) if is_wine_prefix(defaultPrefix) else None
|
||||
|
||||
|
||||
def validate_wine_prefix(value):
|
||||
if not value or any(ord(character) < 32 or ord(character) == 127 for character in value):
|
||||
raise ValueError("Choose the Wine prefix (bottle) folder for this Windows program.")
|
||||
prefixPath = Path(value).expanduser()
|
||||
if not prefixPath.is_absolute() or not is_wine_prefix(prefixPath):
|
||||
raise ValueError("Choose an existing Wine prefix containing drive_c, dosdevices, and system.reg.")
|
||||
return str(prefixPath)
|
||||
|
||||
|
||||
def clipboard_target(data, targetType):
|
||||
if isinstance(data, bytes):
|
||||
data = data.decode("utf-8")
|
||||
@@ -178,7 +206,7 @@ def exec_argument(value):
|
||||
return '"' + escaped + '"'
|
||||
|
||||
|
||||
def shortcut_data(target, name, fileManager="", terminal=False):
|
||||
def shortcut_data(target, name, fileManager="", terminal=False, winePrefix=None):
|
||||
name = name.strip()
|
||||
if not name or any(ord(character) < 32 or ord(character) == 127 for character in name):
|
||||
raise ValueError("Enter a shortcut name without line breaks or control characters.")
|
||||
@@ -192,6 +220,12 @@ def shortcut_data(target, name, fileManager="", terminal=False):
|
||||
if "=" in target.value:
|
||||
raise ValueError("Desktop launchers cannot run a program with '=' in its path.")
|
||||
arguments = [target.value]
|
||||
elif target.kind == "Windows program":
|
||||
winePrefix = validate_wine_prefix(winePrefix if winePrefix is not None else
|
||||
detect_wine_prefix(target.value))
|
||||
if shutil.which("wine") is None:
|
||||
raise ValueError("Required program is unavailable: wine")
|
||||
arguments = ["env", "WINEPREFIX=" + winePrefix, "wine", target.value]
|
||||
elif target.kind == "Application":
|
||||
# Launch the original entry so D-Bus identity, field codes, terminal and
|
||||
# working-directory settings survive a renamed desktop shortcut.
|
||||
@@ -204,12 +238,14 @@ def shortcut_data(target, name, fileManager="", terminal=False):
|
||||
for key, value in (("Type", "Application"), ("Name", name), ("Icon", target.icon),
|
||||
("Exec", " ".join(exec_argument(argument) for argument in arguments))):
|
||||
keyFile.set_string("Desktop Entry", key, value)
|
||||
keyFile.set_boolean("Desktop Entry", "Terminal", terminal and target.kind == "Program")
|
||||
keyFile.set_boolean("Desktop Entry", "Terminal", terminal and target.kind in ("Program", "Windows program"))
|
||||
if target.kind == "Windows program":
|
||||
keyFile.set_string("Desktop Entry", "Path", str(Path(target.value).resolve().parent))
|
||||
return keyFile.to_data()[0]
|
||||
|
||||
|
||||
def create_shortcut(target, name, fileManager="", terminal=False, desktopPath=None):
|
||||
data = shortcut_data(target, name, fileManager, terminal)
|
||||
def create_shortcut(target, name, fileManager="", terminal=False, desktopPath=None, winePrefix=None):
|
||||
data = shortcut_data(target, name, fileManager, terminal, winePrefix)
|
||||
desktopPath = Path(desktopPath) if desktopPath is not None else desktop_directory()
|
||||
desktopPath.mkdir(parents=True, exist_ok=True)
|
||||
# Keep long names below common NAME_MAX limits, including numbered suffixes.
|
||||
|
||||
Reference in new issue
Block a user