diff --git a/config/config.qml b/config/config.qml index 140609a..6a9f4b5 100644 --- a/config/config.qml +++ b/config/config.qml @@ -18,7 +18,7 @@ QtObject { // ──────────────────────────────────────────────────────────────────────── // ── Position & Dimensions ───────────────────────────────────────────────── - property string barPosition: "bottom" + property string barPosition: "top" property int barHeight: 35 property int margin: 15 property int radius: 26 diff --git a/launcher/Launcher.qml b/launcher/Launcher.qml index 14f6a26..7d58782 100644 --- a/launcher/Launcher.qml +++ b/launcher/Launcher.qml @@ -31,18 +31,8 @@ PanelWindow { property bool _keyboardNav: false property var allItems: [] property bool loading: false - property bool proxyMode: false - property string proxyUrl: "" - Component.onCompleted: readProxyUrl() - function readProxyUrl() { - proxyUrlProc.command = ["cat", Cfg.Config.launcherCacheDir + "/proxy.conf"] - proxyUrlProc.running = true - } - - // When launcherSelectFirst is true the first result is pre-selected; - // otherwise the list starts with no selection (-1). readonly property int defaultIndex: Cfg.Config.launcherSelectFirst ? 0 : -1 readonly property var filteredItems: { @@ -132,45 +122,13 @@ PanelWindow { let cleanExec = item.exec.replace(/%[fFuUik]/g, "").trim() const logPath = "/tmp/qml_launch.log" - const lowerExec = cleanExec.toLowerCase() - const isBrowser = lowerExec.includes("chromium") || lowerExec.includes("chrome") || lowerExec.includes("firefox") - - let extraArgs = [] - let envVars = [] - let prefix = "" - let useProxyChains = false - - if (root.proxyMode) { - if (isBrowser && root.proxyUrl) { - // Browsers: use native proxy flags instead of proxychains4 - // to avoid LD_PRELOAD sandbox conflicts - if (lowerExec.includes("chromium") || lowerExec.includes("chrome")) { - extraArgs.push("--proxy-server=" + root.proxyUrl) - } - if (lowerExec.includes("firefox")) { - envVars.push("all_proxy=" + root.proxyUrl) - } - } else { - // Non-browsers: use proxychains4 normally - useProxyChains = true - } - } - - const envPrefix = envVars.length > 0 ? "env " + envVars.join(" ") + " " : "" - if (useProxyChains) { - prefix = envPrefix + "/usr/bin/proxychains4 " - } else { - prefix = envPrefix - } - - const args = parseExec(cleanExec) - const fullArgs = args.concat(extraArgs) - const cmd = prefix + fullArgs.map(a => shellQuote(a)).join(" ") + " > " + shellQuote(logPath) + " 2>&1 < /dev/null &" + const args = parseExec(cleanExec) + const cmd = args.map(a => shellQuote(a)).join(" ") + " > " + shellQuote(logPath) + " 2>&1 < /dev/null &" launchProc.command = ["sh", "-c", cmd] launchProc.running = true - console.log("Launching: " + prefix + cleanExec + (extraArgs.length ? " " + extraArgs.join(" ") : "")) + console.log("Launching: " + cleanExec) if (typeof close === "function") close() } @@ -186,8 +144,7 @@ PanelWindow { WlrLayershell.keyboardFocus: _isOpen ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None mask: Region { item: (root._isOpen && Cfg.Config.launcherCloseOnClickOutside) ? fullMask : clipContainer } - // Full-screen item used as input mask when click-outside-to-close is enabled. - // Lets the background MouseArea receive events outside the bubble. + Item { id: fullMask; anchors.fill: parent } Timer { @@ -225,20 +182,7 @@ PanelWindow { Process { id: launchProc } - Process { - id: proxyUrlProc - property string rawOutput: "" - stdout: SplitParser { onRead: data => proxyUrlProc.rawOutput = data.trim() } - onRunningChanged: { - if (!running && rawOutput) { - root.proxyUrl = rawOutput - } - } - } - // Transparent overlay — captures clicks outside the bubble and closes the - // launcher. Sits below clipContainer in stacking order (declared first), - // so clicks inside the bubble still reach the bubble's own MouseAreas. MouseArea { anchors.fill: parent enabled: root._isOpen && Cfg.Config.launcherCloseOnClickOutside @@ -263,6 +207,10 @@ PanelWindow { width: parent.width property int targetHeight: Math.min(layout.implicitHeight + 28, 540) height: targetHeight + Behavior on height { + enabled: root._isOpen + NumberAnimation { duration: 154; easing.type: Easing.OutCubic } + } anchors.top: root.isTop ? parent.top : undefined anchors.bottom: root.isTop ? undefined : parent.bottom @@ -292,11 +240,12 @@ PanelWindow { ColumnLayout { id: layout - anchors.fill: parent + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right anchors.margins: 14 spacing: 10 - // ── Search input ────────────────────────────────────────── Rectangle { Layout.fillWidth: true height: 44 @@ -389,51 +338,19 @@ PanelWindow { } } } - - // ── Proxychains toggle ──────────────────────────── - Rectangle { - id: proxyBtn - height: 28 - width: 110 - radius: 8 - color: root.proxyMode ? Qt.rgba(1, 0.35, 0.35, 0.22) : t.launcherPill - border.color: root.proxyMode ? Qt.rgba(1, 0.35, 0.35, 0.65) : t.launcherBorder - border.width: Cfg.Config.popupBorderWidth - Behavior on color { ColorAnimation { duration: 150 } } - Behavior on border.color { ColorAnimation { duration: 150 } } - - RowLayout { - id: proxyBtnRow - anchors.centerIn: parent - spacing: 5 - Text { - text: "󰒃" - font.pixelSize: 11 - color: root.proxyMode ? Qt.rgba(1, 0.45, 0.45, 1) : t.launcherDim - Behavior on color { ColorAnimation { duration: 150 } } - } - Text { - text: "proxychains4" - font.pixelSize: 10 - font.bold: root.proxyMode - color: root.proxyMode ? Qt.rgba(1, 0.45, 0.45, 1) : t.launcherDim - Behavior on color { ColorAnimation { duration: 150 } } - } - } - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: root.proxyMode = !root.proxyMode - } - } } } ListView { id: resultsList Layout.fillWidth: true - implicitHeight: Math.min(contentHeight, 400) + Layout.preferredHeight: listHeight + + property int listHeight: Math.min(contentHeight, 400) + Behavior on listHeight { + NumberAnimation { duration: 154; easing.type: Easing.OutCubic } + } + clip: true model: root.filteredItems currentIndex: root.defaultIndex @@ -442,6 +359,10 @@ PanelWindow { onModelChanged: currentIndex = root.defaultIndex ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } + add: Transition { + NumberAnimation { property: "opacity"; from: 0; to: 1; duration: 100; easing.type: Easing.InQuad } + } + delegate: Rectangle { width: resultsList.width height: 38 diff --git a/launcher/Launcher.sh b/launcher/Launcher.sh new file mode 100644 index 0000000..fdbdc07 --- /dev/null +++ b/launcher/Launcher.sh @@ -0,0 +1,104 @@ +#!/usr/bin/env bash + +CACHE_DIR="/tmp/qs_launcher" + +DRUN_CACHE="$CACHE_DIR/drun.txt" +RUN_CACHE="$CACHE_DIR/run.txt" + +mkdir -p "$CACHE_DIR" + + +get_desktop_files() { + IFS=':' read -ra DIRS <<< \ + "${XDG_DATA_DIRS:-/usr/local/share:/usr/share}" + + for dir in "${DIRS[@]}"; do + APP_DIR="$dir/applications" + + [ -d "$APP_DIR" ] || continue + + find "$APP_DIR" \ + -maxdepth 1 \ + -name "*.desktop" \ + 2>/dev/null + done | while read -r f; do + + name=$( + grep -m1 '^Name=' "$f" \ + | cut -d= -f2- + ) + + exec=$( + grep -m1 '^Exec=' "$f" \ + | cut -d= -f2- \ + | sed 's/%[fFuUik]//g' + ) + + type=$( + grep -m1 '^Type=' "$f" \ + | cut -d= -f2- + ) + + nodisplay=$( + grep -m1 '^NoDisplay=' "$f" \ + | cut -d= -f2- \ + | tr '[:upper:]' '[:lower:]' + ) + + icon=$( + grep -m1 '^Icon=' "$f" \ + | cut -d= -f2- + ) + + [[ "$type" == "Application" ]] || continue + [[ "$nodisplay" != "true" ]] || continue + [[ -n "$name" ]] || continue + + printf "%s\t%s\t%s\t%s\n" \ + "$name" \ + "$exec" \ + "$(basename "$f" .desktop)" \ + "$icon" + done +} + + +get_binaries() { + compgen -c \ + | sort -u \ + | head -600 \ + | awk '{ + print $1 "\t" $1 "\t" $1 + }' +} + + +build_drun() { + get_desktop_files \ + | sort -f -t$'\t' -k1,1 \ + | awk -F'\t' '!seen[$1]++' +} + +build_run() { + { + get_desktop_files + get_binaries + } \ + | sort -f -t$'\t' -k1,1 \ + | awk -F'\t' '!seen[$1]++' +} + + +update_cache() { + build_drun > "$DRUN_CACHE.tmp" + build_run > "$RUN_CACHE.tmp" + + mv "$DRUN_CACHE.tmp" "$DRUN_CACHE" + mv "$RUN_CACHE.tmp" "$RUN_CACHE" +} + + +if [ "$1" == "--update-only" ]; then + update_cache + exit 0 +fi