Compare commits
No commits in common. "7ebff0d2a47dc32e0f976a6286dc7b7aec7eda01" and "9640e09677b3f94891c1c0f3c4403e2aa661984b" have entirely different histories.
7ebff0d2a4
...
9640e09677
|
|
@ -4,21 +4,6 @@ Same pattern as orbit-menu/config.py and horizon-dock/config.py. Deliberately
|
||||||
separate from settings.py's Settings class: that one persists quad enable/
|
separate from settings.py's Settings class: that one persists quad enable/
|
||||||
feature toggles and favorites (module-scoped, read via ctx.feature()), while
|
feature toggles and favorites (module-scoped, read via ctx.feature()), while
|
||||||
this is a single whole-window flag read once at startup.
|
this is a single whole-window flag read once at startup.
|
||||||
|
|
||||||
"layouts" narrows/customises the workspace-layout picker in the taskbar panel
|
|
||||||
(ui/taskbar.py) on top of what hypr/layouts auto-discovers and writes to
|
|
||||||
~/.cache/astro-menu/layouts.json:
|
|
||||||
"enabled": list of layout names to offer, in that order (tab order follows
|
|
||||||
it) — omit/null to show everything hypr/layouts discovered.
|
|
||||||
"overrides": {layout_name: {partial layout spec}} shallow-merged onto the
|
|
||||||
discovered spec for that layout — e.g. trim "dirs" to fewer
|
|
||||||
directions, or set "fit_method"/"stepper" to false to hide
|
|
||||||
those controls for that layout, without touching its .lua file.
|
|
||||||
Example:
|
|
||||||
"layouts": {
|
|
||||||
"enabled": ["scrolling", "master", "monocle"],
|
|
||||||
"overrides": {"scrolling": {"dirs": ["down", "right"]}}
|
|
||||||
}
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
@ -27,7 +12,7 @@ import json
|
||||||
|
|
||||||
from paths import CONFIG_FILE, ensure_dirs
|
from paths import CONFIG_FILE, ensure_dirs
|
||||||
|
|
||||||
_DEFAULTS = {"hologram": True, "layouts": {"enabled": None, "overrides": {}}}
|
_DEFAULTS = {"hologram": True}
|
||||||
|
|
||||||
|
|
||||||
def _load() -> dict:
|
def _load() -> dict:
|
||||||
|
|
@ -44,23 +29,3 @@ def _load() -> dict:
|
||||||
|
|
||||||
def hologram_enabled() -> bool:
|
def hologram_enabled() -> bool:
|
||||||
return bool(_load().get("hologram", True))
|
return bool(_load().get("hologram", True))
|
||||||
|
|
||||||
|
|
||||||
def apply_layout_config(layouts: list[dict]) -> list[dict]:
|
|
||||||
"""Filter/reorder/override the layouts hypr/layouts discovered, per the
|
|
||||||
"layouts" config key. `layouts` is the parsed layouts.json (or its
|
|
||||||
hardcoded fallback) — see Taskbar._load_layouts."""
|
|
||||||
cfg = _load().get("layouts") or {}
|
|
||||||
enabled = cfg.get("enabled")
|
|
||||||
overrides = cfg.get("overrides") or {}
|
|
||||||
|
|
||||||
by_name = {ly["name"]: ly for ly in layouts}
|
|
||||||
names = enabled if enabled else [ly["name"] for ly in layouts]
|
|
||||||
|
|
||||||
out = []
|
|
||||||
for name in names:
|
|
||||||
ly = by_name.get(name)
|
|
||||||
if ly is None:
|
|
||||||
continue # config names a layout hypr/layouts never discovered
|
|
||||||
out.append({**ly, **overrides.get(name, {})})
|
|
||||||
return out
|
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,7 @@ so it *covers* the 2x2 quads rather than pushing anything down. The panel holds:
|
||||||
* workspace/layout controls — pick the current workspace's layout (scrolling /
|
* workspace/layout controls — pick the current workspace's layout (scrolling /
|
||||||
dwindle / master / monocle, enumerated from ~/.cache/astro-menu/layouts.json,
|
dwindle / master / monocle, enumerated from ~/.cache/astro-menu/layouts.json,
|
||||||
written by hypr/layouts) and, for directional layouts, its direction. Applied
|
written by hypr/layouts) and, for directional layouts, its direction. Applied
|
||||||
live via `hyprctl eval 'layouts.set(ws, name, dir)'`. The set of layouts
|
live via `hyprctl eval 'layouts.set(ws, name, dir)'`.
|
||||||
offered (and their per-layout options) can be narrowed/customised via
|
|
||||||
config.py's "layouts" key — see config.apply_layout_config.
|
|
||||||
* a per-window row list: [icon + title → focus/jump] [⇤ pull to this workspace].
|
* a per-window row list: [icon + title → focus/jump] [⇤ pull to this workspace].
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
@ -21,9 +19,8 @@ import gi
|
||||||
|
|
||||||
gi.require_version("Gtk", "4.0")
|
gi.require_version("Gtk", "4.0")
|
||||||
gi.require_version("AstalApps", "0.1")
|
gi.require_version("AstalApps", "0.1")
|
||||||
from gi.repository import AstalApps, Gtk, GLib # noqa: E402
|
from gi.repository import AstalApps, Gtk # noqa: E402
|
||||||
|
|
||||||
import config
|
|
||||||
from lib.proc import run_json, run_text
|
from lib.proc import run_json, run_text
|
||||||
from paths import CACHE_DIR
|
from paths import CACHE_DIR
|
||||||
|
|
||||||
|
|
@ -54,7 +51,6 @@ class Taskbar(Gtk.Box):
|
||||||
self._clients: list = []
|
self._clients: list = []
|
||||||
self._dir_dds = {}
|
self._dir_dds = {}
|
||||||
self._fit_sws = {}
|
self._fit_sws = {}
|
||||||
self._poll_id: int | None = None
|
|
||||||
|
|
||||||
# The workspace/window panel. It is NOT appended here: the menu window mounts
|
# The workspace/window panel. It is NOT appended here: the menu window mounts
|
||||||
# `panel_widget` into the quad region so that expanding COLLAPSES this strip's
|
# `panel_widget` into the quad region so that expanding COLLAPSES this strip's
|
||||||
|
|
@ -125,44 +121,24 @@ class Taskbar(Gtk.Box):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _load_layouts() -> list:
|
def _load_layouts() -> list:
|
||||||
try:
|
try:
|
||||||
layouts = json.loads(_LAYOUTS_MANIFEST.read_text())
|
return json.loads(_LAYOUTS_MANIFEST.read_text())
|
||||||
except (FileNotFoundError, json.JSONDecodeError):
|
except (FileNotFoundError, json.JSONDecodeError):
|
||||||
# fallback if hypr/layouts hasn't written the manifest yet
|
# fallback if hypr/layouts hasn't written the manifest yet
|
||||||
layouts = [{"name": "scrolling", "label": "Scrolling", "directional": True,
|
return [{"name": "scrolling", "label": "Scrolling", "directional": True,
|
||||||
"dirs": ["down", "up", "right", "left"], "default_dir": "down",
|
"dirs": ["down", "up", "right", "left"], "default_dir": "down",
|
||||||
"fit_method": True},
|
"fit_method": True},
|
||||||
{"name": "columns", "label": "Columns", "directional": True,
|
{"name": "columns", "label": "Columns", "directional": True,
|
||||||
"dirs": ["right", "down"], "dir_labels": ["Left / Right", "Up / Down"],
|
"dirs": ["right", "down"], "dir_labels": ["Left / Right", "Up / Down"],
|
||||||
"default_dir": "right", "fit_method": True},
|
"default_dir": "right", "fit_method": True},
|
||||||
{"name": "dwindle", "label": "Dwindle", "directional": False, "dirs": []},
|
{"name": "dwindle", "label": "Dwindle", "directional": False, "dirs": []},
|
||||||
{"name": "master", "label": "Master", "directional": False, "dirs": []},
|
{"name": "master", "label": "Master", "directional": False, "dirs": []},
|
||||||
{"name": "monocle", "label": "Monocle", "directional": False, "dirs": []}]
|
{"name": "monocle", "label": "Monocle", "directional": False, "dirs": []}]
|
||||||
return config.apply_layout_config(layouts)
|
|
||||||
|
|
||||||
# -- populate ----------------------------------------------------------
|
# -- populate ----------------------------------------------------------
|
||||||
def refresh(self) -> None:
|
def refresh(self) -> None:
|
||||||
run_json(["hyprctl", "clients", "-j"], self._on_clients)
|
run_json(["hyprctl", "clients", "-j"], self._on_clients)
|
||||||
run_json(["hyprctl", "activeworkspace", "-j"], self._on_ws)
|
run_json(["hyprctl", "activeworkspace", "-j"], self._on_ws)
|
||||||
|
|
||||||
# -- keep the panel live while the menu is open -------------------------
|
|
||||||
# Hyprland doesn't push workspace-switch events to us, and switching
|
|
||||||
# workspaces isn't blocked by the menu being open — so without this the
|
|
||||||
# "Workspace N layout" panel goes stale the moment you change workspace
|
|
||||||
# while astro-menu is still up. Poll instead; the window starts/stops
|
|
||||||
# this alongside show_menu/hide_menu.
|
|
||||||
def start_polling(self) -> None:
|
|
||||||
if self._poll_id is None:
|
|
||||||
self._poll_id = GLib.timeout_add(1000, self._on_poll)
|
|
||||||
|
|
||||||
def stop_polling(self) -> None:
|
|
||||||
if self._poll_id is not None:
|
|
||||||
GLib.source_remove(self._poll_id)
|
|
||||||
self._poll_id = None
|
|
||||||
|
|
||||||
def _on_poll(self) -> bool:
|
|
||||||
self.refresh()
|
|
||||||
return True # keep polling every second while the menu is open
|
|
||||||
|
|
||||||
def _on_ws(self, ok: bool, data) -> None:
|
def _on_ws(self, ok: bool, data) -> None:
|
||||||
if ok and isinstance(data, dict):
|
if ok and isinstance(data, dict):
|
||||||
self._active_ws = data.get("id")
|
self._active_ws = data.get("id")
|
||||||
|
|
|
||||||
|
|
@ -192,7 +192,6 @@ class MenuWindow(Gtk.ApplicationWindow):
|
||||||
def show_menu(self, focus_appdrawer: bool = False) -> None:
|
def show_menu(self, focus_appdrawer: bool = False) -> None:
|
||||||
self.appdrawer.set_expanded(False)
|
self.appdrawer.set_expanded(False)
|
||||||
self.taskbar.refresh()
|
self.taskbar.refresh()
|
||||||
self.taskbar.start_polling()
|
|
||||||
self.grid.on_show()
|
self.grid.on_show()
|
||||||
self.appdrawer.on_show()
|
self.appdrawer.on_show()
|
||||||
self.set_visible(True)
|
self.set_visible(True)
|
||||||
|
|
@ -205,7 +204,6 @@ class MenuWindow(Gtk.ApplicationWindow):
|
||||||
self._hologram.start_intro()
|
self._hologram.start_intro()
|
||||||
|
|
||||||
def hide_menu(self) -> None:
|
def hide_menu(self) -> None:
|
||||||
self.taskbar.stop_polling()
|
|
||||||
self.grid.on_hide()
|
self.grid.on_hide()
|
||||||
self.taskbar.collapse_panel()
|
self.taskbar.collapse_panel()
|
||||||
self.grid.hide_takeover()
|
self.grid.hide_takeover()
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,5 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# screenrec.sh — toggle screen recording via wf-recorder.
|
|
||||||
# Press the bound key once to start (region picked via slurp): a
|
|
||||||
# "Stop Recording" button appears on the notification and the script blocks
|
|
||||||
# on it. Either clicking that button, or pressing the same keybind again,
|
|
||||||
# ends the recording.
|
|
||||||
|
|
||||||
endrec () {
|
endrec () {
|
||||||
killall -s SIGINT wf-recorder
|
killall -s SIGINT wf-recorder
|
||||||
|
|
@ -18,22 +14,16 @@ endrec () {
|
||||||
mkdir -p ~/Videos
|
mkdir -p ~/Videos
|
||||||
statecon=$( pidof wf-recorder )
|
statecon=$( pidof wf-recorder )
|
||||||
|
|
||||||
|
|
||||||
outfile="$HOME/Videos/$(date +'%Y%m%d%H%M%S').mp4"
|
outfile="$HOME/Videos/$(date +'%Y%m%d%H%M%S').mp4"
|
||||||
|
#$outfile
|
||||||
nid=""
|
nid=""
|
||||||
|
if [ "$statecon" == '' ]; then
|
||||||
if [ -z "$statecon" ]; then
|
wf-recorder -g "$(slurp)" -f $outfile &
|
||||||
wf-recorder -g "$(slurp)" -f "$outfile" &
|
$nid=$(notify-send -p -u critical -t 0 "recording started" --action="goto endrecplace"
|
||||||
|
)
|
||||||
# --id-fd keeps the notification id off stdout so -A's chosen action
|
|
||||||
# (the only thing we read back) isn't mixed in with it.
|
|
||||||
nidfile=$(mktemp)
|
|
||||||
action=$(notify-send -u critical -t 0 --id-fd=3 \
|
|
||||||
-A "stop=Stop Recording" \
|
|
||||||
"recording started" "output: $outfile" 3>"$nidfile")
|
|
||||||
nid=$(<"$nidfile")
|
|
||||||
rm -f "$nidfile"
|
|
||||||
|
|
||||||
[ "$action" == "stop" ] && endrec
|
|
||||||
else
|
else
|
||||||
endrec
|
endrec
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
pidof wf-recorder && endrec
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# timer-run <total_seconds> [label]
|
# timer-run <total_seconds> [label]
|
||||||
# Runs entirely in background. No terminal needed after launch.
|
# Runs entirely in background. No terminal needed after launch.
|
||||||
# Sends a desktop notification (with Dismiss / Snooze 2m / Snooze 5m action
|
# Sends a dunst notification + audio beep when done.
|
||||||
# buttons, via beacon) + audio beep when done.
|
|
||||||
#
|
#
|
||||||
# Install: ~/.config/scripts/timer-run (companion: timer-pick in same dir)
|
# Install: ~/.config/scripts/timer-run (companion: timer-pick in same dir)
|
||||||
|
|
||||||
|
|
@ -61,41 +60,20 @@ if [[ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ]]; then
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── audio alert (rings on a loop until dismissed/snoozed) ─────────────────────
|
|
||||||
_ALARM_SOUND="/usr/share/sounds/freedesktop/stereo/alarm-clock-elapsed.oga"
|
|
||||||
|
|
||||||
play_alarm() {
|
|
||||||
if command -v paplay &>/dev/null && [[ -f "$_ALARM_SOUND" ]]; then
|
|
||||||
paplay "$_ALARM_SOUND" 2>/dev/null
|
|
||||||
elif command -v canberra-gtk-play &>/dev/null; then
|
|
||||||
canberra-gtk-play --id=alarm-clock-elapsed 2>/dev/null
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
( while true; do play_alarm; sleep 1; done ) &
|
|
||||||
RING_PID=$!
|
|
||||||
|
|
||||||
# ── notification with action buttons ──────────────────────────────────────────
|
|
||||||
# -A implies --wait: notify-send blocks until the card is closed or an action
|
|
||||||
# is invoked, printing the chosen action's id to stdout. beacon supports the
|
|
||||||
# spec's Actions/ActionInvoked, so this works without any custom hint.
|
|
||||||
ACTION=""
|
|
||||||
if command -v notify-send &>/dev/null; then
|
if command -v notify-send &>/dev/null; then
|
||||||
ACTION=$(notify-send \
|
notify-send \
|
||||||
--urgency=critical \
|
--urgency=critical \
|
||||||
--expire-time=0 \
|
--expire-time=0 \
|
||||||
--icon=alarm-timer \
|
--icon=alarm-timer \
|
||||||
-A "dismiss=Dismiss" \
|
|
||||||
-A "snooze2=Snooze 2m" \
|
|
||||||
-A "snooze5=Snooze 5m" \
|
|
||||||
"$NOTIF_SUMMARY" \
|
"$NOTIF_SUMMARY" \
|
||||||
"$NOTIF_BODY")
|
"$NOTIF_BODY"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
kill "$RING_PID" 2>/dev/null
|
# ── audio alert ───────────────────────────────────────────────────────────────
|
||||||
wait "$RING_PID" 2>/dev/null
|
_ALARM_SOUND="/usr/share/sounds/freedesktop/stereo/alarm-clock-elapsed.oga"
|
||||||
|
|
||||||
case "$ACTION" in
|
if command -v paplay &>/dev/null && [[ -f "$_ALARM_SOUND" ]]; then
|
||||||
snooze2) exec bash "$0" 120 "$LABEL" ;;
|
paplay "$_ALARM_SOUND" 2>/dev/null
|
||||||
snooze5) exec bash "$0" 300 "$LABEL" ;;
|
elif command -v canberra-gtk-play &>/dev/null; then
|
||||||
esac
|
canberra-gtk-play --id=alarm-clock-elapsed 2>/dev/null
|
||||||
|
fi
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,25 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# screenrec.sh — toggle screen recording via wf-recorder.
|
|
||||||
# Press the bound key once to start (region picked via slurp): a
|
|
||||||
# "Stop Recording" button appears on the notification and the script blocks
|
|
||||||
# on it. Either clicking that button, or pressing the same keybind again,
|
|
||||||
# ends the recording.
|
|
||||||
|
|
||||||
endrec () {
|
endrec () {
|
||||||
killall -s SIGINT wf-recorder
|
killall -s SIGINT wf-recorder
|
||||||
# mnotifd (the notif daemon) has no dunstctl; close the "recording started"
|
dunstctl close $nid
|
||||||
# card via the freedesktop CloseNotification method instead.
|
|
||||||
[ -n "$nid" ] && gdbus call --session --dest org.freedesktop.Notifications \
|
|
||||||
--object-path /org/freedesktop/Notifications \
|
|
||||||
--method org.freedesktop.Notifications.CloseNotification "$nid" >/dev/null 2>&1
|
|
||||||
notify-send "recording ended - output to $outfile"
|
notify-send "recording ended - output to $outfile"
|
||||||
}
|
}
|
||||||
|
|
||||||
mkdir -p ~/Videos
|
mkdir -p ~/Videos
|
||||||
statecon=$( pidof wf-recorder )
|
statecon=$( pidof wf-recorder )
|
||||||
|
|
||||||
|
|
||||||
outfile="$HOME/Videos/$(date +'%Y%m%d%H%M%S').mp4"
|
outfile="$HOME/Videos/$(date +'%Y%m%d%H%M%S').mp4"
|
||||||
|
#$outfile
|
||||||
nid=""
|
nid=""
|
||||||
|
if [ "$statecon" == '' ]; then
|
||||||
if [ -z "$statecon" ]; then
|
wf-recorder -g "$(slurp)" -f $outfile &
|
||||||
wf-recorder -g "$(slurp)" -f "$outfile" &
|
$nid=$(notify-send -p -u critical -t 0 "recording started" --action="goto endrecplace"
|
||||||
|
)
|
||||||
# --id-fd keeps the notification id off stdout so -A's chosen action
|
|
||||||
# (the only thing we read back) isn't mixed in with it.
|
|
||||||
nidfile=$(mktemp)
|
|
||||||
action=$(notify-send -u critical -t 0 --id-fd=3 \
|
|
||||||
-A "stop=Stop Recording" \
|
|
||||||
"recording started" "output: $outfile" 3>"$nidfile")
|
|
||||||
nid=$(<"$nidfile")
|
|
||||||
rm -f "$nidfile"
|
|
||||||
|
|
||||||
[ "$action" == "stop" ] && endrec
|
|
||||||
else
|
else
|
||||||
endrec
|
endrec
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
pidof wf-recorder && endrec
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# timer-run <total_seconds> [label]
|
# timer-run <total_seconds> [label]
|
||||||
# Runs entirely in background. No terminal needed after launch.
|
# Runs entirely in background. No terminal needed after launch.
|
||||||
# Sends a desktop notification (with Dismiss / Snooze 2m / Snooze 5m action
|
# Sends a dunst notification + audio beep when done.
|
||||||
# buttons, via mnotifd) + audio beep when done.
|
|
||||||
#
|
#
|
||||||
# Install: ~/.config/scripts/timer-run (companion: timer-pick in same dir)
|
# Install: ~/.config/scripts/timer-run (companion: timer-pick in same dir)
|
||||||
|
|
||||||
|
|
@ -43,13 +42,13 @@ DURATION_STR=$(fmt_dur "$TOTAL")
|
||||||
NOTIF_SUMMARY="⏰ Timer done${LABEL:+ — $LABEL}"
|
NOTIF_SUMMARY="⏰ Timer done${LABEL:+ — $LABEL}"
|
||||||
NOTIF_BODY="Set for ${DURATION_STR}. Finished at $(date '+%H:%M:%S')."
|
NOTIF_BODY="Set for ${DURATION_STR}. Finished at $(date '+%H:%M:%S')."
|
||||||
|
|
||||||
# ── desktop notification ──────────────────────────────────────────────────────
|
# ── dunst notification ────────────────────────────────────────────────────────
|
||||||
# notify-send needs DBUS_SESSION_BUS_ADDRESS. If not in env, find it via the
|
# notify-send needs DBUS_SESSION_BUS_ADDRESS. If not in env, find it via the
|
||||||
# running mnotifd notif daemon (reliable on single-user Wayland/Hyprland setups).
|
# running dunst process (reliable on single-user Wayland/Hyprland setups).
|
||||||
if [[ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ]]; then
|
if [[ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ]]; then
|
||||||
local_uid=$(id -u)
|
local_uid=$(id -u)
|
||||||
# try to grab it from the environment of any process owned by this user
|
# try to grab it from the environment of any process owned by this user
|
||||||
for pid in $(pgrep -u "$local_uid" -f '[m]notifd/main.py' 2>/dev/null); do
|
for pid in $(pgrep -u "$local_uid" dunst 2>/dev/null); do
|
||||||
addr=$(cat /proc/$pid/environ 2>/dev/null \
|
addr=$(cat /proc/$pid/environ 2>/dev/null \
|
||||||
| tr '\0' '\n' \
|
| tr '\0' '\n' \
|
||||||
| grep '^DBUS_SESSION_BUS_ADDRESS=' \
|
| grep '^DBUS_SESSION_BUS_ADDRESS=' \
|
||||||
|
|
@ -61,41 +60,20 @@ if [[ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ]]; then
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── audio alert (rings on a loop until dismissed/snoozed) ─────────────────────
|
|
||||||
_ALARM_SOUND="/usr/share/sounds/freedesktop/stereo/alarm-clock-elapsed.oga"
|
|
||||||
|
|
||||||
play_alarm() {
|
|
||||||
if command -v paplay &>/dev/null && [[ -f "$_ALARM_SOUND" ]]; then
|
|
||||||
paplay "$_ALARM_SOUND" 2>/dev/null
|
|
||||||
elif command -v canberra-gtk-play &>/dev/null; then
|
|
||||||
canberra-gtk-play --id=alarm-clock-elapsed 2>/dev/null
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
( while true; do play_alarm; sleep 1; done ) &
|
|
||||||
RING_PID=$!
|
|
||||||
|
|
||||||
# ── notification with action buttons ──────────────────────────────────────────
|
|
||||||
# -A implies --wait: notify-send blocks until the card is closed or an action
|
|
||||||
# is invoked, printing the chosen action's id to stdout. mnotifd supports the
|
|
||||||
# spec's Actions/ActionInvoked, so this works without any custom hint.
|
|
||||||
ACTION=""
|
|
||||||
if command -v notify-send &>/dev/null; then
|
if command -v notify-send &>/dev/null; then
|
||||||
ACTION=$(notify-send \
|
notify-send \
|
||||||
--urgency=critical \
|
--urgency=critical \
|
||||||
--expire-time=0 \
|
--expire-time=0 \
|
||||||
--icon=alarm-timer \
|
--icon=alarm-timer \
|
||||||
-A "dismiss=Dismiss" \
|
|
||||||
-A "snooze2=Snooze 2m" \
|
|
||||||
-A "snooze5=Snooze 5m" \
|
|
||||||
"$NOTIF_SUMMARY" \
|
"$NOTIF_SUMMARY" \
|
||||||
"$NOTIF_BODY")
|
"$NOTIF_BODY"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
kill "$RING_PID" 2>/dev/null
|
# ── audio alert ───────────────────────────────────────────────────────────────
|
||||||
wait "$RING_PID" 2>/dev/null
|
_ALARM_SOUND="/usr/share/sounds/freedesktop/stereo/alarm-clock-elapsed.oga"
|
||||||
|
|
||||||
case "$ACTION" in
|
if command -v paplay &>/dev/null && [[ -f "$_ALARM_SOUND" ]]; then
|
||||||
snooze2) exec bash "$0" 120 "$LABEL" ;;
|
paplay "$_ALARM_SOUND" 2>/dev/null
|
||||||
snooze5) exec bash "$0" 300 "$LABEL" ;;
|
elif command -v canberra-gtk-play &>/dev/null; then
|
||||||
esac
|
canberra-gtk-play --id=alarm-clock-elapsed 2>/dev/null
|
||||||
|
fi
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue