diff --git a/quickshell/services/TaskService.qml b/quickshell/services/TaskService.qml index 2e6a2ee..eb289d9 100644 --- a/quickshell/services/TaskService.qml +++ b/quickshell/services/TaskService.qml @@ -139,51 +139,25 @@ Item { } } + property real lastLaunchTime: 0 + property string lastLaunchCmd: "" + function launchApp(cmd) { if (!cmd) return; let cleanCmd = cmd.trim() if (cleanCmd === "") return; + let now = Date.now() + if (cleanCmd === lastLaunchCmd && (now - lastLaunchTime) < 600) { + return; // Ignore rapid double-clicks within 600ms + } + lastLaunchTime = now + lastLaunchCmd = cleanCmd + let execCmd = cleanCmd.replace(/%[a-zA-Z]/g, "").trim() root.activeAppId = execCmd.toLowerCase() - if (execCmd.startsWith("flatpak run ") || execCmd.includes("flatpak ")) { - Quickshell.execDetached(["sh", "-c", execCmd + " &"]) - return; - } - - let baseName = execCmd.replace(/\.desktop$/, "").trim() - - // Build universal Flatpak + desktop launcher + native binary fallback chain - let shellCmd = "" - if (baseName.startsWith("org.") || baseName.startsWith("io.") || baseName.startsWith("com.") || baseName.startsWith("app.")) { - let shortName = baseName.split(".").pop() - shellCmd = "flatpak run " + baseName + " 2>/dev/null || flatpak run " + shortName + " 2>/dev/null || gtk-launch " + baseName + " 2>/dev/null || gtk-launch " + shortName + " 2>/dev/null || " + shortName + " 2>/dev/null || " + execCmd + " &" - } else { - // Known reverse domain mappings for common Flatpak apps like spotify, feishin, zen, vesktop, discord, etc. - let flatpakMapping = { - "spotify": "com.spotify.Client", - "feishin": "io.github.jeffvli.feishin", - "zen": "io.github.zen_browser.zen", - "zen-browser": "io.github.zen_browser.zen", - "vesktop": "dev.vencord.Vesktop", - "discord": "com.discordapp.Discord", - "code": "com.visualstudio.code", - "vscodium": "com.vscodium.codium", - "obs": "com.obsproject.Studio", - "steam": "com.valvesoftware.Steam", - "heroic": "com.heroicgameslauncher.hgl" - } - - let mappedFlatpak = flatpakMapping[baseName.toLowerCase()] || "" - if (mappedFlatpak !== "") { - shellCmd = "flatpak run " + mappedFlatpak + " 2>/dev/null || flatpak run " + baseName + " 2>/dev/null || gtk-launch " + mappedFlatpak + " 2>/dev/null || gtk-launch " + baseName + " 2>/dev/null || " + execCmd + " &" - } else { - shellCmd = "flatpak run " + baseName + " 2>/dev/null || gtk-launch " + baseName + " 2>/dev/null || " + execCmd + " &" - } - } - - Quickshell.execDetached(["sh", "-c", shellCmd]) + Quickshell.execDetached(["python3", Quickshell.env("HOME") + "/.config/quickshell/services/python/launch_app.py", cleanCmd]) } function closeApp(appId) { diff --git a/quickshell/services/python/launch_app.py b/quickshell/services/python/launch_app.py new file mode 100755 index 0000000..d999a6b --- /dev/null +++ b/quickshell/services/python/launch_app.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +import sys +import os +import subprocess +import shutil +import re + +def launch(): + if len(sys.argv) < 2: + return + cmd = sys.argv[1].strip() + if not cmd: + return + + # Strip desktop field codes (%u, %U, %f, %F, etc.) + clean_cmd = re.sub(r'%[a-zA-Z]', '', cmd).strip() + base_name = clean_cmd.replace('.desktop', '').strip() + lower_name = base_name.lower() + + flatpak_mappings = { + "spotify": "com.spotify.Client", + "feishin": "io.github.jeffvli.feishin", + "zen": "io.github.zen_browser.zen", + "zen-browser": "io.github.zen_browser.zen", + "vesktop": "dev.vencord.Vesktop", + "discord": "com.discordapp.Discord", + "code": "com.visualstudio.code", + "vscodium": "com.vscodium.codium", + "obs": "com.obsproject.Studio", + "steam": "com.valvesoftware.Steam", + "heroic": "com.heroicgameslauncher.hgl" + } + + # 1. Flatpak launch check (launch exactly once if Flatpak ID is installed) + candidates_flatpak = [] + if base_name in flatpak_mappings: + candidates_flatpak.append(flatpak_mappings[base_name]) + if lower_name in flatpak_mappings: + candidates_flatpak.append(flatpak_mappings[lower_name]) + if base_name.startswith(("org.", "io.", "com.", "app.")): + candidates_flatpak.append(base_name) + + for fp_id in candidates_flatpak: + check = subprocess.run(["flatpak", "info", fp_id], capture_output=True, text=True) + if check.returncode == 0: + subprocess.Popen(["flatpak", "run", fp_id], start_new_session=True) + return + + # 2. GTK Desktop launcher check + if shutil.which("gtk-launch"): + for gtk_id in [base_name, f"{base_name}.desktop"]: + res = subprocess.run(["gtk-launch", gtk_id], capture_output=True, text=True) + if res.returncode == 0: + return + + # 3. Direct binary execution + bin_name = clean_cmd.split()[0] + if shutil.which(bin_name): + subprocess.Popen(clean_cmd, shell=True, start_new_session=True) + return + + # 4. Fallback execution + subprocess.Popen(f"{clean_cmd} &", shell=True, start_new_session=True) + +if __name__ == '__main__': + launch()