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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ and `~/.local/state/omacast`.
omacast needs only what Omarchy ships: `omarchy-shell` (Quickshell), Hyprland, bash, `jq` and `socat`.
It adds no packages and no background services.

- **Superwhisper recording indicator.** When `python3` with PyGObject is installed, the
plugin runs `services/superwhisper-frame.py` alongside the shell. While the launcher is open it tells
[Superwhisper](https://superwhisper.com) where the card is over the session bus
(`com.superwhisper.ShellOverlay`), so Superwhisper's recording glow or pill draws on the launcher instead
of the window under it. It makes no network requests, and does nothing without Superwhisper.
- **Currency rates.** When you open the launcher and its rates are over 12 hours old, the calculator downloads
ECB exchange rates from `api.frankfurter.dev` (retrying 30 minutes after a failure) and caches them in
`~/.cache/omacast/currencies.json`. This is the only network request omacast makes. Turn it off with
Expand Down
77 changes: 77 additions & 0 deletions services/superwhisper-frame.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#!/usr/bin/env python3
"""Tells Superwhisper where the launcher's card is, so its recording glow (or
pill) draws on the launcher rather than on the window under it.

stdin takes one line per change: "1 <monitor> <x> <y> <width> <height>" while
the launcher is open (logical pixels from the monitor's top left), "0" when it
closes. While open this owns com.superwhisper.ShellOverlay and publishes the
card as the Frame property (siiii) of com.superwhisper.ShellOverlay at
/com/superwhisper/ShellOverlay. Exits when stdin closes, releasing the name.
"""
import os
import sys

import gi

gi.require_version("Gio", "2.0")
from gi.repository import Gio, GLib # noqa: E402

NAME = INTERFACE = "com.superwhisper.ShellOverlay"
PATH = "/com/superwhisper/ShellOverlay"
XML = f"""<node><interface name="{INTERFACE}">
<property name="Frame" type="(siiii)" access="read"/>
</interface></node>"""
EMPTY = ("", 0, 0, 0, 0)

bus = Gio.bus_get_sync(Gio.BusType.SESSION, None)
loop = GLib.MainLoop()
frame = EMPTY
owner = 0
pending = b""


def value():
return GLib.Variant("(siiii)", frame)


def get_property(*_):
return value()


def apply(line):
global frame, owner
words = line.split()
if words[:1] == ["1"] and len(words) == 6:
try:
new = (words[1], *(int(float(w)) for w in words[2:]))
except ValueError:
return
else:
new = EMPTY
if new != frame:
frame = new
bus.emit_signal(None, PATH, "org.freedesktop.DBus.Properties", "PropertiesChanged",
GLib.Variant("(sa{sv}as)", (INTERFACE, {"Frame": value()}, [])))
if frame != EMPTY and not owner:
owner = Gio.bus_own_name_on_connection(bus, NAME, Gio.BusNameOwnerFlags.NONE, None, None)
elif frame == EMPTY and owner:
Gio.bus_unown_name(owner)
owner = 0


def command(fd, _condition):
global pending
data = os.read(fd, 4096)
if not data:
loop.quit()
return False
*lines, pending = (pending + data).split(b"\n")
if lines:
apply(lines[-1].decode(errors="replace"))
return True


info = Gio.DBusNodeInfo.new_for_xml(XML).interfaces[0]
bus.register_object(PATH, info, None, get_property, None)
GLib.io_add_watch(sys.stdin.fileno(), GLib.PRIORITY_DEFAULT, GLib.IO_IN | GLib.IO_HUP, command)
loop.run()
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.Io
import Quickshell.Wayland
import qs.Commons
import qs.Commons as Commons
Expand Down Expand Up @@ -141,6 +142,21 @@ PanelWindow {
else { popTimer.interval = seconds * 1000; popTimer.restart() }
}

// Where the card is while open, for Superwhisper: its recording glow (or
// pill) then draws on the launcher, as on any focused window, instead of on
// the window under it. Idle without Superwhisper or PyGObject.
readonly property string overlayFrame: window.shown && window.screen
? ["1", window.screen.name, Math.round(card.x), Math.round(card.y), Math.round(card.width), Math.round(card.height)].join(" ")
: "0"
onOverlayFrameChanged: superwhisperFrame.write(window.overlayFrame + "\n")
Process {
id: superwhisperFrame
command: ["python3", decodeURIComponent(Qt.resolvedUrl("../services/superwhisper-frame.py").toString().replace(/^file:\/\//, ""))]
running: true
stdinEnabled: true
onStarted: superwhisperFrame.write(window.overlayFrame + "\n")
}

Timer {
id: popTimer
onTriggered: window.popToRoot()
Expand Down