Skip to content
Merged
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: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,8 @@ Full Raycast ↔ Omarchy map: [`docs/raycast-vs-omarchy.md`](docs/raycast-vs-oma
|---|---|
| [`bin/omnicast`](bin/omnicast) | Launch / IPC toggle |
| [`src/`](src/) | Shell, views, services, backends |
| [`src/views/root/`](src/views/root/) | Calc / PathSafety / Handoffs / WindowActions modules |
| [`src/views/root/`](src/views/root/) | Calc / Handoffs / WindowActions modules |
| [`src/services/PathSafety.js`](src/services/PathSafety.js) | Client-side sensitive-path checks (aligned with `path_safety.py`) |
| [`tests/smoke_backends.sh`](tests/smoke_backends.sh) | Backend + packaging smoke |
| [`packaging/opr/`](packaging/opr/) | OPR (`omarchy-pkgs`) recipe scaffold |
| [`bin/omnicast-omarchy-setup`](bin/omnicast-omarchy-setup) | Post-plugin PATH + Alt+Space bind |
Expand Down
30 changes: 28 additions & 2 deletions bin/omnicast-open-file
Original file line number Diff line number Diff line change
@@ -1,9 +1,35 @@
#!/usr/bin/env bash
# Dismiss-friendly file open: portal picker → xdg-open.
# Dismiss-friendly file open: portal picker → path_safety → xdg-open.
# Used by Omnicast "Find Files" handoff.

set -euo pipefail

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SAFETY="$ROOT/src/backend/path_safety.py"

_danger_ext() {
case "${1,,}" in
*.sh|*.bash|*.zsh|*.fish|*.py|*.rb|*.pl|*.js|*.mjs|*.cjs|*.exe|*.bin|*.run|*.appimage)
return 0 ;;
esac
return 1
}

_safe_open() {
local path="$1"
if ! python3 "$SAFETY" check "$path" >/dev/null 2>&1; then
command -v omarchy-notification-send >/dev/null 2>&1 && \
omarchy-notification-send -g "󰈔" -u critical "Open blocked" "Sensitive or out-of-home path" || true
return 1
fi
if _danger_ext "$path"; then
command -v omarchy-notification-send >/dev/null 2>&1 && \
omarchy-notification-send -g "󰈔" -u critical "Open blocked" "Script/binary not launched" || true
return 1
fi
xdg-open "$path" >/dev/null 2>&1 &
}

picked="$(omarchy-file-select --title "Open file" 2>/dev/null)" || status=$?
status=${status:-0}

Expand All @@ -23,6 +49,6 @@ while IFS= read -r line; do
path="$(python3 -c "import sys,urllib.parse; print(urllib.parse.unquote(sys.argv[1][7:]))" "$path")"
fi
if [[ -e $path ]]; then
xdg-open "$path" >/dev/null 2>&1 &
_safe_open "$path" || true
fi
done <<<"$picked"
3 changes: 2 additions & 1 deletion context.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ omnicast/
│ ├── script-commands.md
│ └── spec/
├── bin/omnicast # Launch / toggle
├── src/views/root/ # Calc / PathSafety / Handoffs / WindowActions modules
├── src/views/root/ # Calc / Handoffs / WindowActions modules
├── src/services/PathSafety.js # Client sensitive-path checks (↔ path_safety.py)
└── src/ # QML + Python backends
```

Expand Down
13 changes: 6 additions & 7 deletions docs/work-order.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,14 @@
- [x] Docs: Omarchy integration checklist + OPR scaffold

## Now
1. [ ] Merge `koustav` → `main` (plugin-add clones default branch)
2. [ ] Dogfood: `omarchy plugin update` / re-add + setup on a clean Omarchy box
3. [ ] `omarchy plugin validate` on fresh clone from `main`
4. [ ] OPR PR to `omacom/omarchy-pkgs` (`packaging/opr/`)
1. [x] Merge `koustav` → `main` (PR #66)
2. [x] OPR PR opened: https://github.com/omacom/omarchy-pkgs/pull/1573
3. [ ] OPR merge + publish to `pkgs.omarchy.org` (awaiting maintainer build approval)

## Later
5. [ ] Index / handoff Omarchy plugin store in umbrella
6. [ ] Optional: generic Ollama/BYOK gateway
7. [ ] AUR only if targeting vanilla Arch (not Omarchy-default)
4. [ ] Index / handoff Omarchy plugin store in umbrella
5. [ ] Optional: generic Ollama/BYOK gateway
6. [ ] AUR only if targeting vanilla Arch (not Omarchy-default)

## Out of band
- Omarchy-LLM
60 changes: 32 additions & 28 deletions packaging/opr/PKGBUILD
Original file line number Diff line number Diff line change
@@ -1,47 +1,51 @@
# Maintainer: Koustav Ganguly <koustavganguly24@gmail.com>
# OPR recipe scaffold — copy into omacom/omarchy-pkgs pkgbuilds/omnicast/
# Pin _commit before opening a PR (omarchy-pkgs rejects unpinned git sources).

pkgname=omnicast
pkgver=0.2.1
pkgrel=1
pkgdesc="Alt+Space productivity umbrella for Omarchy (Hyprland / Quickshell)"
pkgdesc='Alt+Space productivity umbrella for Omarchy (Hyprland / Quickshell)'
arch=('any')
url="https://omnicast.best"
url='https://omnicast.best'
license=('MIT')
_commit='bf646385b6a02244318d57fb8ed77e1be8a1d1ed'
depends=(
'quickshell'
'python'
'wl-clipboard'
'xdg-utils'
'omarchy'
'python'
'quickshell'
'wl-clipboard'
'xdg-utils'
)
makedepends=('git')
optdepends=(
'fd: fast file name search'
'ripgrep: content: file search'
'plocate: locate-backed file search'
'ghostty: default terminal'
'imv: image preview'
'onlyoffice-desktopeditors: office document viewer'
'fd: fast file name search'
'ripgrep: content: file search'
'plocate: locate-backed file search'
'ghostty: default terminal'
'imv: image preview'
'onlyoffice-desktopeditors: office document viewer'
)
# REPLACE with the exact commit you are packaging, then run updpkgsums.
_commit=REPLACE_WITH_GIT_SHA
source=("git+https://github.com/KOUSTAV2409/omnicast.git#commit=${_commit}")
options=('!debug')
source=("omnicast::git+https://github.com/KOUSTAV2409/omnicast.git#commit=${_commit}")
sha256sums=('SKIP')

prepare() {
[[ $(git -C omnicast rev-parse HEAD) == "$_commit" ]]
}

package() {
cd "$srcdir/omnicast"
cd omnicast

install -d "$pkgdir/usr/share/omnicast"
cp -a src bin BarWidget.qml manifest.json LICENSE README.md \
"$pkgdir/usr/share/omnicast/"
install -d "$pkgdir/usr/share/omnicast"
cp -a src bin BarWidget.qml manifest.json LICENSE README.md \
"$pkgdir/usr/share/omnicast/"

# Do not ship marketing video, site, or draft packaging trees
rm -rf "$pkgdir/usr/share/omnicast/bin/__pycache__" 2>/dev/null || true
# Keep the package lean: no marketing video, site, or draft trees
find "$pkgdir/usr/share/omnicast" -type d -name '__pycache__' -exec rm -rf {} + 2>/dev/null || true
find "$pkgdir/usr/share/omnicast" -type f -name '*.pyc' -delete 2>/dev/null || true

install -d "$pkgdir/usr/bin"
ln -sf /usr/share/omnicast/bin/omnicast "$pkgdir/usr/bin/omnicast"
install -d "$pkgdir/usr/bin"
ln -sf /usr/share/omnicast/bin/omnicast "$pkgdir/usr/bin/omnicast"

install -Dm644 /dev/stdin "$pkgdir/usr/share/applications/omnicast.desktop" <<'DESKTOP'
install -Dm644 /dev/stdin "$pkgdir/usr/share/applications/omnicast.desktop" <<'DESKTOP'
[Desktop Entry]
Name=Omnicast
Comment=Alt+Space Productivity Umbrella for Omarchy
Expand All @@ -54,5 +58,5 @@ MimeType=x-scheme-handler/omnicast;
StartupNotify=false
DESKTOP

install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
}
6 changes: 6 additions & 0 deletions packaging/opr/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"source": "local",
"sync": false,
"release_ring": "fast",
"auto_merge": true
}
30 changes: 20 additions & 10 deletions src/backend/path_safety.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import json
import os
import re
import sys
from pathlib import Path

# Directory path segments that must never appear in search/preview.
Expand Down Expand Up @@ -48,6 +49,8 @@
)

# Exact / suffix filenames that are typically credentials.
# Keep this list tight: bare words like "token"/"passwd"/"hosts" overblock
# normal project files (API token docs, /etc-style names under $HOME, etc.).
SECRET_NAMES = {
".netrc",
".pgpass",
Expand All @@ -68,17 +71,14 @@
"id_ed25519_sk",
"authorized_keys",
"known_hosts",
"credentials",
"credentials.json",
"credentials.csv",
"service_account.json",
"secrets.json",
"secrets.yaml",
"secrets.yml",
"secrets.toml",
"token",
"token.json",
"cookies",
"cookies.sqlite",
"login data",
"logins.json",
Expand All @@ -87,13 +87,6 @@
"shadow",
"gshadow",
"master_password.aes",
# UNIX system files: prevented from leaking via any path collision
"passwd",
"group",
"sudoers",
"fstab",
"hosts",
"crontab",
"master.passwd",
"ssh_host_rsa_key",
"ssh_host_ecdsa_key",
Expand Down Expand Up @@ -308,3 +301,20 @@ def deny_reason(path: Path, scope_root: Path | None = None) -> str:
if is_secret_path(p):
return "Blocked: sensitive credentials or session data"
return ""


def check_cli(path_str: str) -> int:
"""Exit 0 if path is allowed for open/reveal; print reason on deny."""
reason = deny_reason(Path(path_str))
if reason:
print(reason, file=sys.stderr)
return 1
print("ok")
return 0


if __name__ == "__main__":
if len(sys.argv) >= 3 and sys.argv[1] == "check":
raise SystemExit(check_cli(sys.argv[2]))
print("usage: path_safety.py check <path>", file=sys.stderr)
raise SystemExit(2)
46 changes: 41 additions & 5 deletions src/backend/ranking.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
DATA_DIR = Path.home() / ".local/share/omnicast"
DATA_FILE = DATA_DIR / "ranking.json"

MAX_SCORES = 2000
MAX_ALIASES = 200
MAX_FAVORITES = 100
MAX_ID_LEN = 256

DEFAULT = {
"favorites": [],
"aliases": {},
Expand All @@ -16,6 +21,31 @@
}


def _valid_id(item_id: str) -> bool:
s = str(item_id or "")
return 0 < len(s) <= MAX_ID_LEN and "\0" not in s


def _prune(data: dict) -> dict:
scores = data.get("scores") or {}
if len(scores) > MAX_SCORES:
# Drop lowest score, then oldest last-used.
ranked = sorted(
scores.items(),
key=lambda kv: (float(kv[1].get("score", 0)), float(kv[1].get("last", 0))),
)
keep = dict(ranked[-MAX_SCORES:])
data["scores"] = keep
aliases = data.get("aliases") or {}
if len(aliases) > MAX_ALIASES:
# Arbitrary but bounded: keep insertion order of first N keys.
data["aliases"] = dict(list(aliases.items())[:MAX_ALIASES])
favs = data.get("favorites") or []
if len(favs) > MAX_FAVORITES:
data["favorites"] = favs[:MAX_FAVORITES]
return data


def load():
DATA_DIR.mkdir(parents=True, exist_ok=True)
if not DATA_FILE.exists():
Expand Down Expand Up @@ -44,6 +74,9 @@ def save(data):


def bump(item_id: str):
if not _valid_id(item_id):
print(json.dumps({"ok": False, "error": "invalid id"}))
return
data = load()
now = time.time()
entry = data["scores"].get(item_id, {"count": 0, "last": 0, "score": 0})
Expand All @@ -56,11 +89,14 @@ def bump(item_id: str):
recent.remove(item_id)
recent.insert(0, item_id)
data["recent"] = recent[:40]
save(data)
save(_prune(data))
print(json.dumps({"ok": True, "entry": entry}))


def toggle_favorite(item_id: str):
if not _valid_id(item_id):
print(json.dumps({"ok": False, "error": "invalid id"}))
return
data = load()
favs = data.setdefault("favorites", [])
if item_id in favs:
Expand All @@ -69,18 +105,18 @@ def toggle_favorite(item_id: str):
else:
favs.insert(0, item_id)
state = True
save(data)
save(_prune(data))
print(json.dumps({"ok": True, "favorited": state, "favorites": favs}))


def set_alias(alias: str, item_id: str):
data = load()
alias = alias.strip().lower()
if not alias:
print(json.dumps({"ok": False, "error": "empty alias"}))
if not alias or len(alias) > 64 or not _valid_id(item_id):
print(json.dumps({"ok": False, "error": "invalid alias or id"}))
return
data.setdefault("aliases", {})[alias] = item_id
save(data)
save(_prune(data))
print(json.dumps({"ok": True, "alias": alias, "id": item_id}))


Expand Down
11 changes: 9 additions & 2 deletions src/backend/window_snap.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,14 @@
from __future__ import annotations

import json
import re
import subprocess
import sys
from typing import Any

# Hyprland window addresses are hex pointers (e.g. 0x56a1b2c3).
_ADDR_RE = re.compile(r"^0x[0-9a-fA-F]{1,16}$")

# Small inset so snapped floats don't kiss the bar / edges.
GAP = 8

Expand Down Expand Up @@ -115,8 +119,8 @@ def apply(preset: str) -> dict[str, Any]:
return {"ok": False, "error": f"unknown preset: {preset}", "presets": sorted(PRESETS)}

win = _hypr_json("activewindow")
addr = win.get("address")
if not addr or addr in ("0x0", "0"):
addr = str(win.get("address") or "")
if not addr or addr in ("0x0", "0") or not _ADDR_RE.fullmatch(addr):
return {"ok": False, "error": "no active window"}

monitors = _hypr_json("monitors")
Expand All @@ -130,6 +134,9 @@ def apply(preset: str) -> dict[str, Any]:
# Keep last pixel inside usable rect
w = max(80, min(w, ux + uw - x))
h = max(80, min(h, uy + uh - y))
# Geometry is int-only in Lua strings below — never interpolate raw strings
# except the already-validated hex address.
x, y, w, h = int(x), int(y), int(w), int(h)

window = f"address:{addr}"

Expand Down
Loading
Loading