From 987588f512addac10add0b70b68403bf7ccbfd2d Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 29 Jun 2026 11:21:07 -0500 Subject: [PATCH] Launch shell commands on active workspace --- bin/omarchy-hyprland-launch | 29 +++++++++ ...launch-floating-terminal-with-presentation | 27 +------- shell/Commons/Util.qml | 12 ++-- shell/plugins/launcher/Launcher.qml | 19 ++---- test/shell.d/floating-terminal-test.sh | 7 --- test/shell.d/launcher-search-test.sh | 15 +++-- test/shell.d/shell-launch-test.sh | 63 +++++++++++++++++++ 7 files changed, 114 insertions(+), 58 deletions(-) create mode 100755 bin/omarchy-hyprland-launch create mode 100644 test/shell.d/shell-launch-test.sh diff --git a/bin/omarchy-hyprland-launch b/bin/omarchy-hyprland-launch new file mode 100755 index 00000000..5dcb45bf --- /dev/null +++ b/bin/omarchy-hyprland-launch @@ -0,0 +1,29 @@ +#!/bin/bash + +# omarchy:summary=Launch a command on the active Hyprland workspace +# omarchy:args= + +cmd="$*" +[[ -n $cmd ]] || exit 0 + +lua_quote() { + local value=$1 + + value=${value//\\/\\\\} + value=${value//\"/\\\"} + value=${value//$'\n'/\\n} + printf '"%s"' "$value" +} + +active_workspace() { + hyprctl activeworkspace -j 2>/dev/null | jq -r '.name // .id // empty' 2>/dev/null +} + +if [[ -n ${HYPRLAND_INSTANCE_SIGNATURE:-} ]] && omarchy-cmd-present hyprctl && omarchy-cmd-present jq; then + workspace="$(active_workspace)" + if [[ -n $workspace ]] && hyprctl dispatch "function() hl.exec_cmd($(lua_quote "$cmd"), { workspace = $(lua_quote "$workspace") }) end" >/dev/null 2>&1; then + exit 0 + fi +fi + +exec bash -lc "$cmd" diff --git a/bin/omarchy-launch-floating-terminal-with-presentation b/bin/omarchy-launch-floating-terminal-with-presentation index d280d5b4..1d81ac34 100755 --- a/bin/omarchy-launch-floating-terminal-with-presentation +++ b/bin/omarchy-launch-floating-terminal-with-presentation @@ -14,30 +14,5 @@ shell_quote() { printf "'" } -lua_quote() { - local value=$1 - - value=${value//\\/\\\\} - value=${value//\"/\\\"} - value=${value//$'\n'/\\n} - printf '"%s"' "$value" -} - -active_workspace() { - hyprctl activeworkspace -j 2>/dev/null | jq -r '.name // .id // empty' 2>/dev/null -} - terminal_command="setsid uwsm-app -- xdg-terminal-exec --app-id=org.omarchy.terminal --title=Omarchy -e bash -c $(shell_quote "$presentation_script")" -workspace="${OMARCHY_LAUNCH_WORKSPACE:-}" - -if [[ -z $workspace ]] && [[ -n ${HYPRLAND_INSTANCE_SIGNATURE:-} ]] && omarchy-cmd-present hyprctl && omarchy-cmd-present jq; then - workspace="$(active_workspace)" -fi - -if [[ -n $workspace ]] && [[ -n ${HYPRLAND_INSTANCE_SIGNATURE:-} ]] && omarchy-cmd-present hyprctl; then - if hyprctl dispatch "function() hl.exec_cmd($(lua_quote "$terminal_command"), { workspace = $(lua_quote "$workspace") }) end" >/dev/null 2>&1; then - exit 0 - fi -fi - -exec setsid uwsm-app -- xdg-terminal-exec --app-id=org.omarchy.terminal --title=Omarchy -e bash -c "$presentation_script" +exec omarchy-hyprland-launch "$terminal_command" diff --git a/shell/Commons/Util.qml b/shell/Commons/Util.qml index 972154ee..2fb554a6 100644 --- a/shell/Commons/Util.qml +++ b/shell/Commons/Util.qml @@ -1,4 +1,5 @@ pragma Singleton +import Quickshell import QtQuick // Shared utility helpers used across plugins. Pure functions only — no @@ -6,6 +7,8 @@ import QtQuick QtObject { id: root + readonly property string omarchyPath: Quickshell.env("OMARCHY_PATH") + function clamp(value, min, max) { var n = Number(value) if (!isFinite(n)) return min @@ -38,15 +41,8 @@ QtObject { return "'" + String(value || "").replace(/'/g, "'\\''") + "'" } - function luaQuote(value) { - return "\"" + String(value || "") - .replace(/\\/g, "\\\\") - .replace(/"/g, "\\\"") - .replace(/\n/g, "\\n") + "\"" - } - function hyprExecCommand(command) { - return ["hyprctl", "dispatch", "hl.dsp.exec_cmd(" + luaQuote(command) + ")"] + return [omarchyPath + "/bin/omarchy-hyprland-launch", command] } function isPlainObject(value) { diff --git a/shell/plugins/launcher/Launcher.qml b/shell/plugins/launcher/Launcher.qml index 8ef18a4d..ad0f07f9 100644 --- a/shell/plugins/launcher/Launcher.qml +++ b/shell/plugins/launcher/Launcher.qml @@ -1,5 +1,4 @@ import Quickshell -import Quickshell.Hyprland import Quickshell.Io import Quickshell.Wayland import Quickshell.Widgets @@ -25,7 +24,6 @@ Item { property int launchSerial: 0 property int launchToplevelCount: 0 property var launchActiveToplevel: null - property string launchWorkspace: "" property bool launchOsdOpen: false property string launchOsdMessage: "" property var configuredHiddenEntryIds: ({}) @@ -77,7 +75,6 @@ Item { ? root.contentMargin * 2 + root.searchHeight + root.contentSpacing + requestedListHeight : 400 - root.launchWorkspace = String(payload.workspace || "") || root.focusedWorkspaceName() root.opened = true root.filterText = payload.query || "" root.selectedIndex = 0 @@ -132,12 +129,6 @@ Item { return LauncherSearch.entrySearchText(entry) } - function focusedWorkspaceName() { - var workspace = Hyprland.focusedWorkspace - if (!workspace) return "" - return String(workspace.name || workspace.id || "") - } - function isHiddenEntry(entry) { var id = String((entry && entry.id) || "") return root.configuredHiddenEntryIds[id] === true || root.desktopHiddenEntryIds[id] === true @@ -259,9 +250,12 @@ Item { if (index < 0 || index >= root.filteredEntries.length) return var entry = root.filteredEntries[index] if (!entry) return + var desktopId = String(entry.id || "") + if (!desktopId) return + root.beginLaunchFeedback(entry) root.dismiss() - entry.execute() + Quickshell.execDetached(Util.hyprExecCommand("gtk-launch " + Util.shellQuote(desktopId))) } function requestDeleteIndex(index) { @@ -286,10 +280,9 @@ Item { var desktopId = String(entry.id || "") var name = root.entryName(entry) - var command = [root.omarchyPath + "/bin/omarchy-remove-launcher-entry", desktopId, name] - if (root.launchWorkspace !== "") command = ["env", "OMARCHY_LAUNCH_WORKSPACE=" + root.launchWorkspace].concat(command) + var command = Util.shellQuote(root.omarchyPath + "/bin/omarchy-remove-launcher-entry") + " " + Util.shellQuote(desktopId) + " " + Util.shellQuote(name) root.cancelDelete() - Quickshell.execDetached(command) + Quickshell.execDetached(Util.hyprExecCommand(command)) } function beginLaunchFeedback(entry) { diff --git a/test/shell.d/floating-terminal-test.sh b/test/shell.d/floating-terminal-test.sh index a563ee40..578bb178 100644 --- a/test/shell.d/floating-terminal-test.sh +++ b/test/shell.d/floating-terminal-test.sh @@ -37,10 +37,3 @@ dispatch=$(<"$TEST_LOG") [[ $dispatch == *'workspace = "2"'* ]] || fail "floating terminal targets active workspace" "$dispatch" [[ $dispatch == *"xdg-terminal-exec --app-id=org.omarchy.terminal"* ]] || fail "floating terminal dispatch launches Omarchy terminal" "$dispatch" pass "floating terminal targets active workspace" - -: >"$TEST_LOG" -OMARCHY_LAUNCH_WORKSPACE=7 "$ROOT/bin/omarchy-launch-floating-terminal-with-presentation" "echo hello" - -dispatch=$(<"$TEST_LOG") -[[ $dispatch == *'workspace = "7"'* ]] || fail "floating terminal honors explicit launch workspace" "$dispatch" -pass "floating terminal honors explicit launch workspace" diff --git a/test/shell.d/launcher-search-test.sh b/test/shell.d/launcher-search-test.sh index 7e74f40c..d2ebd280 100644 --- a/test/shell.d/launcher-search-test.sh +++ b/test/shell.d/launcher-search-test.sh @@ -80,11 +80,18 @@ assert( 'launcher delete keeps launcher open after confirmation' ) assert( - confirmDeleteMatch[1].includes('OMARCHY_LAUNCH_WORKSPACE='), - 'launcher delete passes launch workspace to remover' + confirmDeleteMatch[1].includes('Util.hyprExecCommand(command)'), + 'launcher delete routes remover through Hyprland' +) + +const activateMatch = launcherQml.match(/function activateIndex\(index\) \{([\s\S]*?)\n \}/) +assert(activateMatch, 'launcher activateIndex function exists') +assert( + !activateMatch[1].includes('entry.execute()'), + 'launcher does not execute desktop entries directly' ) assert( - launcherQml.includes('function focusedWorkspaceName()'), - 'launcher captures focused workspace' + activateMatch[1].includes('gtk-launch') && activateMatch[1].includes('Util.hyprExecCommand'), + 'launcher routes desktop entry launch through Hyprland' ) JS diff --git a/test/shell.d/shell-launch-test.sh b/test/shell.d/shell-launch-test.sh new file mode 100644 index 00000000..ad096e0a --- /dev/null +++ b/test/shell.d/shell-launch-test.sh @@ -0,0 +1,63 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +run_node_test <<'JS' +const fs = require('fs') +const utilQml = fs.readFileSync(path.join(root, 'shell/Commons/Util.qml'), 'utf8') + +assert( + utilQml.includes('import Quickshell'), + 'shell launch helper can read Omarchy path' +) +assert( + utilQml.includes('Quickshell.env("OMARCHY_PATH")'), + 'shell launch helper uses Omarchy path' +) +assert( + /function hyprExecCommand\(command\)[\s\S]*omarchy-hyprland-launch/.test(utilQml), + 'shell launch helper routes through Hyprland launch command' +) + +JS + +tmp_dir="$(mktemp -d)" +trap 'rm -rf "$tmp_dir"' EXIT + +cat >"$tmp_dir/hyprctl" <<'SCRIPT' +#!/bin/bash +case "$1" in + activeworkspace) + printf '{"id":2,"name":"2"}\n' + ;; + dispatch) + printf '%s\n' "$2" >>"$TEST_LOG" + ;; +esac +SCRIPT +chmod +x "$tmp_dir/hyprctl" + +cat >"$tmp_dir/jq" <<'SCRIPT' +#!/bin/bash +cat >/dev/null +printf '2\n' +SCRIPT +chmod +x "$tmp_dir/jq" + +export TEST_LOG="$tmp_dir/log" +export PATH="$tmp_dir:$ROOT/bin:$PATH" +export HYPRLAND_INSTANCE_SIGNATURE=test + +"$ROOT/bin/omarchy-hyprland-launch" "echo hello" + +dispatch=$(<"$TEST_LOG") +[[ $dispatch == *'workspace = "2"'* ]] || fail "hyprland launch targets active workspace" "$dispatch" +[[ $dispatch == *'hl.exec_cmd("echo hello"'* ]] || fail "hyprland launch dispatches command" "$dispatch" +pass "hyprland launch targets active workspace" + +unset HYPRLAND_INSTANCE_SIGNATURE +"$ROOT/bin/omarchy-hyprland-launch" "printf fallback > $(printf '%q' "$tmp_dir/fallback")" +[[ $(<"$tmp_dir/fallback") == "fallback" ]] || fail "hyprland launch falls back outside Hyprland" +pass "hyprland launch falls back outside Hyprland"