Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Backend.qml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Item {

property var appLibrary: null
property var summon: null
property var dismiss: null

property var config: Schema.withConfigDefaults(null)
property bool directoriesReady: false
Expand Down Expand Up @@ -210,6 +211,8 @@ Item {
}

function runShell(command) {
// Match Omarchy's menu: hide before a command can freeze/capture the screen.
if (backend.dismiss) backend.dismiss()
var summon = OmarchyMenu.summonAction(command)
if (summon) backend.summonPlugin(summon.id, summon.payload)
else Util.execDetached(command)
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ Works:
- **Alerts:** Shut Down, Restart, Log Out and Delete All Entries ask first (↵ confirms, Esc cancels). `omacast run` skips the alert.
- **Feedback:** toasts in the footer and a HUD through Omarchy's OSD when the window closes; static footer "Loading…" appears only after 500 ms of visible loading, yields to toasts and resets on hide. The header divider stays still.
- **Window behaviour:**
- Omarchy actions dismiss the launcher before executing (including Text/OCR). A runtime Hyprland layer rule disables compositor fades for `omacast`, like the stock overlays, and is reapplied after config reload. No user keybindings or config files are changed.
- The `escapeKeyBehavior` setting works both ways.
- Pop-to-root runs after `popToRootTimeout`.
- Compact mode shows only the header until you type.
Expand Down
1 change: 1 addition & 0 deletions Launcher.qml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ Item {
Backend {
id: backend
appLibrary: root.shell ? root.shell.appLibrary : null
dismiss: function() { root.hide() }
summon: function(id, payload) { return root.shell ? root.shell.summon(id, payload) : false }
}

Expand Down
84 changes: 84 additions & 0 deletions tests/capture-dismiss.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
const test = require("node:test")
const assert = require("node:assert/strict")
const fs = require("node:fs")
const path = require("node:path")
const vm = require("node:vm")
const OmarchyMenu = require("../core/OmarchyMenu.js")

const backendText = fs.readFileSync(path.join(__dirname, "../Backend.qml"), "utf8")
const windowText = fs.readFileSync(path.join(__dirname, "../ui/LauncherWindow.qml"), "utf8")
const launcherText = fs.readFileSync(path.join(__dirname, "../Launcher.qml"), "utf8")

function method(source, name, context) {
const start = source.indexOf(" function " + name + "(")
assert.ok(start >= 0, name)
const end = source.indexOf("\n }", start) + 4
return vm.runInNewContext("(" + source.slice(start, end) + ")", context)
}

function harness() {
const events = []
let visible = true
const backend = {
dismiss() { visible = false; events.push("hide") },
summonPlugin(id, payload) { assert.equal(visible, false); events.push([id, payload]) }
}
const context = {
backend, OmarchyMenu,
Util: { execDetached(command) { assert.equal(visible, false); events.push(command) } }
}
return { events, backend, run: method(backendText, "runShell", context) }
}

for (const command of ["omarchy-capture-text", "omarchy-capture-screenshot", "custom-command --literal '*' "]) {
test("dismiss before executing " + command, () => {
const h = harness()
h.run(command)
assert.deepEqual(h.events, ["hide", command])
})
}

test("shell summon actions still route natively, after dismissal", () => {
const h = harness()
h.run("omarchy-shell shell summon omarchy.clipboard")
assert.deepEqual(h.events, ["hide", ["omarchy.clipboard", "{}"]])
})

test("headless backends can still execute without a dismissal hook", () => {
const commands = []
const run = method(backendText, "runShell", {
backend: { dismiss: null }, OmarchyMenu,
Util: { execDetached: command => commands.push(command) }
})
run("omarchy-capture-text")
assert.deepEqual(commands, ["omarchy-capture-text"])
})

test("UI and IPC actions share the backend dismissal hook", () => {
assert.match(launcherText, /dismiss: function\(\) \{ root\.hide\(\) \}/)
assert.match(backendText, /case "omarchy-action":[\s\S]*?backend\.runShell\(candidate\.action\)/)
})

test("capture layer rule uses the current namespace and is reapplied on config reload", () => {
const commands = []
const window = {}
window.applyLayerRule = method(windowText, "applyLayerRule", {
Quickshell: { execDetached: args => commands.push(Array.from(args)) }
})
const startup = windowText.match(/Component\.onCompleted: (window\.applyLayerRule\(\))/)
assert.ok(startup)
vm.runInNewContext(startup[1], { window })
const handler = windowText.match(/function onRawEvent\(event\) \{([\s\S]*?)\n \}/)
assert.ok(handler)
const onRawEvent = vm.runInNewContext("(function(event) {" + handler[1] + "})", { window })
onRawEvent(null)
onRawEvent({ name: "focusedmon" })
assert.equal(commands.length, 1)
onRawEvent({ name: "configreloaded" })
assert.equal(commands.length, 2)
assert.deepEqual(commands[0], commands[1])
assert.deepEqual(commands[0].slice(0, 2), ["hyprctl", "eval"])
assert.match(commands[0][2], /namespace = "\^omacast\$"/)
assert.match(commands[0][2], /no_anim = true/)
assert.match(commands[0][2], /animation = "none"/)
})
16 changes: 16 additions & 0 deletions ui/LauncherWindow.qml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import QtQuick
import Quickshell
import Quickshell.Hyprland
import Quickshell.Wayland
import qs.Commons
import qs.Commons as Commons
Expand Down Expand Up @@ -41,6 +42,21 @@ PanelWindow {
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive

// [D] Match Omarchy's overlays: an unmapped layer must not linger in a
// compositor fade when Text/OCR freezes the screen. Runtime rules are lost
// on config reload, so reapply without changing the user's Hyprland files.
function applyLayerRule() {
Quickshell.execDetached(["hyprctl", "eval",
'hl.layer_rule({ name = "omacast", match = { namespace = "^omacast$" }, no_anim = true, animation = "none" })'])
}
Component.onCompleted: window.applyLayerRule()
Connections {
target: Hyprland
function onRawEvent(event) {
if (event && String(event.name) === "configreloaded") window.applyLayerRule()
}
}

readonly property var general: {
window.backend.storeRevision
var r = window.backend.ready ? window.backend.callSync("settings.getGeneralSettings", null) : null
Expand Down